Skip to content

Commit 3a73892

Browse files
authored
Merge branch 'main' into main
2 parents 9da411f + d9d449b commit 3a73892

File tree

11 files changed

+1375
-593
lines changed

11 files changed

+1375
-593
lines changed

build/bundle-schemas.cjs

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
const fs = require('fs').promises;
7+
const Bundler = require("@hyperjump/json-schema-bundle");
8+
9+
(async function () {
10+
bundle(`https://json-schema.org/draft/2019-09/schema`, 'draft-2019-09', 'https://json-schema.org/draft/2019-09');
11+
bundle(`https://json-schema.org/draft/2020-12/schema`, 'draft-2020-12', 'https://json-schema.org/draft/2020-12');
12+
}());
13+
14+
async function bundle(uri, filename, derivedURL) {
15+
const metaSchema = await Bundler.get(uri);
16+
let bundle = await Bundler.bundle(metaSchema);
17+
bundle = JSON.parse(JSON.stringify(bundle, null, 2).replace(/"undefined": ""/g, '"$dynamicAnchor": "meta"'));
18+
fs.writeFile(`./${filename}.json`, JSON.stringify(bundle, null, 2), 'utf8');
19+
bundle = flattenDraftMetaSchema(bundle);
20+
const jsified = getCopyright(derivedURL) + 'export default ' + printObject(bundle);
21+
fs.writeFile(`./${filename}-flat.json`, JSON.stringify(bundle, null, 2), 'utf8');
22+
fs.writeFile(`./src/services/schemas/${filename}-flat.ts`, jsified, 'utf8');
23+
}
24+
function getCopyright(derivedURL) {
25+
return [
26+
'/*---------------------------------------------------------------------------------------------',
27+
' * Copyright (c) Microsoft Corporation. All rights reserved.',
28+
' * Licensed under the MIT License. See License.txt in the project root for license information.',
29+
' *--------------------------------------------------------------------------------------------*/',
30+
'',
31+
'// This file is generated - do not edit directly!',
32+
'// Derived from ' + derivedURL,
33+
].join('\n') + '\n\n';
34+
}
35+
36+
37+
function printLiteral(value) {
38+
if (typeof value === 'string') {
39+
return `'${value}'`;
40+
}
41+
return value;
42+
}
43+
44+
function printKey(value) {
45+
if (value.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/)) {
46+
return `${value}`;
47+
}
48+
return `'${value}'`;
49+
}
50+
51+
function indent(level) {
52+
return '\t'.repeat(level);
53+
}
54+
55+
function printObject(obj, indentLevel = 0) {
56+
const result = [];
57+
if (Array.isArray(obj)) {
58+
result.push(`[`);
59+
for (const item of obj) {
60+
if (typeof item === 'object' && item !== null) {
61+
result.push(`${indent(indentLevel + 1)}${printObject(item, indentLevel + 1)},`);
62+
} else {
63+
result.push(`${indent(indentLevel + 1)}${printLiteral(item)},`);
64+
}
65+
}
66+
result.push(`${indent(indentLevel)}]`);
67+
return result.join('\n');
68+
}
69+
if (obj === null) {
70+
result.push(`null`);
71+
return result.join('\n');
72+
}
73+
74+
result.push(`{`);
75+
for (const [key, value] of Object.entries(obj)) {
76+
if (typeof value === 'object' && value !== null) {
77+
result.push(`${indent(indentLevel + 1)}${printKey(key)}: ${printObject(value, indentLevel + 1)},`);
78+
} else {
79+
result.push(`${indent(indentLevel + 1)}${printKey(key)}: ${printLiteral(value)},`);
80+
}
81+
}
82+
result.push(`${indent(indentLevel)}}`);
83+
return result.join('\n');
84+
}
85+
// flatten
86+
87+
const DEFAULT_ANCHOR = 'meta';
88+
89+
function visit(node, callback) {
90+
if (!node || typeof node !== 'object') return;
91+
if (Array.isArray(node)) {
92+
for (const item of node) {
93+
visit(item, callback);
94+
}
95+
return;
96+
}
97+
98+
for (const key of Object.keys(node)) {
99+
callback(node, key);
100+
visit(node[key], callback);
101+
}
102+
}
103+
104+
/** Recursively replace $dynamicRef:#meta with $ref:#meta */
105+
function replaceDynamicRefs(node, anchorName = DEFAULT_ANCHOR) {
106+
visit(node, (n, k) => {
107+
const v = n[k];
108+
if (k === '$dynamicRef' && v === '#' + anchorName) {
109+
n['$ref'] = '#';
110+
delete n['$dynamicRef'];
111+
};
112+
});
113+
}
114+
115+
/** Recursively replace $dynamicRef:#meta with $ref:#meta */
116+
function replaceRecursiveRefs(node, anchorName = DEFAULT_ANCHOR) {
117+
visit(node, (n, k) => {
118+
const v = n[k];
119+
if (k === '$recursiveRef') {
120+
n['$ref'] = v;
121+
delete n['$recursiveRef'];
122+
};
123+
});
124+
}
125+
126+
/** Replace refs that point to a vocabulary */
127+
function replaceOldRefs(node, anchorName = DEFAULT_ANCHOR) {
128+
visit(node, (n, k) => {
129+
const v = n[k];
130+
if (k === '$ref' && typeof v === 'string' && v.startsWith(anchorName + '/')) {
131+
const segments = v.split('#');
132+
if (segments.length === 2) {
133+
n['$ref'] = `#${segments[1]}`;
134+
}
135+
}
136+
});
137+
}
138+
139+
/** Remove all $dynamicAnchor occurrences (except keep keyword definition property) */
140+
function stripDynamicAnchors(node) {
141+
visit(node, (n, k) => {
142+
if (k === '$dynamicAnchor') {
143+
delete n[k];
144+
}
145+
});
146+
}
147+
148+
/** Collect vocabulary object definitions from $defs */
149+
function collectVocabularies(schema) {
150+
const vocabularies = [];
151+
const defs = schema.$defs || {};
152+
for (const [key, value] of Object.entries(defs)) {
153+
if (value && typeof value === 'object' && !Array.isArray(value) && value.$id && value.$dynamicAnchor === DEFAULT_ANCHOR && value.properties) {
154+
vocabularies.push(value);
155+
}
156+
}
157+
return vocabularies;
158+
}
159+
160+
/** Merge properties from each vocabulary into root.properties (shallow) */
161+
function mergeVocabularyProperties(root, vocabularies) {
162+
if (!root.properties) root.properties = {};
163+
replaceOldRefs(root);
164+
for (const vocab of vocabularies) {
165+
for (const [propName, propSchema] of Object.entries(vocab.properties || {})) {
166+
if (!(propName in root.properties)) {
167+
root.properties[propName] = propSchema;
168+
} else {
169+
// Simple heuristic: if both are objects, attempt shallow merge, else keep existing
170+
const existing = root.properties[propName];
171+
if (isPlainObject(existing) && isPlainObject(propSchema)) {
172+
root.properties[propName] = { ...existing, ...propSchema };
173+
}
174+
}
175+
}
176+
}
177+
}
178+
179+
function isPlainObject(o) {
180+
return !!o && typeof o === 'object' && !Array.isArray(o);
181+
}
182+
183+
/** Gather unified $defs from vocab $defs (only specific keys) */
184+
function buildUnifiedDefs(schema, vocabularies) {
185+
const unified = schema.$defs && !referencesVocabulary(schema.$defs) ? { ...schema.$defs } : {};
186+
187+
function harvest(defsObj) {
188+
if (!defsObj || typeof defsObj !== 'object') return;
189+
for (const [k, v] of Object.entries(defsObj)) {
190+
if (!(k in unified)) {
191+
unified[k] = v;
192+
} else {
193+
console.warn(`Warning: duplicate definition for key ${k} found while building unified $defs. Keeping the first occurrence.`);
194+
}
195+
}
196+
}
197+
198+
for (const vocab of vocabularies) harvest(vocab.$defs);
199+
200+
// Adjust schemaArray items dynamicRef->ref later with global replacement
201+
return unified;
202+
}
203+
204+
function referencesVocabulary(defs) {
205+
return Object.keys(defs).some(k => k.startsWith('https://json-schema.org/draft/'));
206+
}
207+
208+
function flattenDraftMetaSchema(original) {
209+
// Clone to avoid mutating input reference
210+
const schema = JSON.parse(JSON.stringify(original));
211+
212+
const anchorName = schema.$dynamicAnchor || DEFAULT_ANCHOR;
213+
214+
// 1. Collect vocabulary schemas
215+
const vocabularies = collectVocabularies(schema);
216+
217+
// 2. Merge vocabulary properties into root
218+
mergeVocabularyProperties(schema, vocabularies);
219+
220+
// 3. Build unified $defs
221+
const unifiedDefs = buildUnifiedDefs(schema, vocabularies);
222+
223+
// 4. Remove top-level allOf (flatten composition)
224+
delete schema.allOf;
225+
226+
// 5. Remove vocabulary objects from $defs
227+
if (schema.$defs) {
228+
for (const k of Object.keys(schema.$defs)) {
229+
if (schema.$defs[k] && schema.$defs[k].$id && schema.$defs[k].$dynamicAnchor === anchorName) {
230+
delete schema.$defs[k];
231+
}
232+
}
233+
}
234+
235+
// 6. Assign unified defs
236+
schema.$defs = unifiedDefs;
237+
238+
// 7. Convert dynamic recursion markers
239+
replaceDynamicRefs(schema, anchorName);
240+
replaceRecursiveRefs(schema, anchorName);
241+
stripDynamicAnchors(schema);
242+
243+
// 8. Add static anchor at root
244+
delete schema.$dynamicAnchor;
245+
246+
// 9. Update title to signal flattening
247+
if (schema.title) {
248+
schema.title = '(Flattened static) ' + schema.title;
249+
} else {
250+
schema.title = 'Flattened Draft 2020-12 meta-schema';
251+
}
252+
253+
return schema;
254+
}

0 commit comments

Comments
 (0)