@oceanum/eidos 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +8 -0
- package/dist/index.js +4631 -0
- package/package.json +4 -1
- package/index.html +0 -13
- package/project.json +0 -4
- package/scripts/debug-schema.js +0 -30
- package/scripts/generate-interfaces.js +0 -48
- package/scripts/schema-bundler.js +0 -609
- package/scripts/schema-to-typescript.js +0 -604
- package/src/index.ts +0 -7
- package/src/lib/eidosmodel.ts +0 -46
- package/src/lib/react.tsx +0 -70
- package/src/lib/render.ts +0 -176
- package/src/schema/interfaces.ts +0 -1171
- package/test-example.js +0 -64
- package/tsconfig.json +0 -13
- package/tsconfig.lib.json +0 -25
- package/tsconfig.spec.json +0 -31
- package/vite.config.ts +0 -44
- /package/{public → dist}/favicon.ico +0 -0
|
@@ -1,609 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import crypto from "crypto";
|
|
4
|
-
import https from "https";
|
|
5
|
-
import fs from "fs";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Schema bundler for EIDOS schemas
|
|
9
|
-
*
|
|
10
|
-
* Key objectives:
|
|
11
|
-
* - Bundle: Combine a root schema and all its referenced external schemas into a single file
|
|
12
|
-
* - Deduplicate: Each unique definition appears only once in the final $defs section
|
|
13
|
-
* - Generate Keys: Create unique PascalCase keys derived from title/filename + path hash for uniqueness
|
|
14
|
-
* - Rewrite Refs: Update all $ref pointers to use the new canonical $defs keys
|
|
15
|
-
* - Exclude Vega: Replace Vega/Vega-Lite schemas with simple PlotSpec placeholder
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
class SchemaBundler {
|
|
19
|
-
constructor() {
|
|
20
|
-
this.definitions = new Map(); // newKey -> definition
|
|
21
|
-
this.keyMapping = new Map(); // original ref -> new key
|
|
22
|
-
this.seenKeys = new Set(); // track used keys to avoid collisions
|
|
23
|
-
this.definitionHashes = new Map(); // hash -> key for deduplication by content
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Generate a hash for a definition to check for duplicates
|
|
28
|
-
* @param {Object} definition - The schema definition
|
|
29
|
-
* @returns {string} - Hash of the definition
|
|
30
|
-
*/
|
|
31
|
-
hashDefinition(definition) {
|
|
32
|
-
// Create a stable hash by stringifying the definition in a consistent way
|
|
33
|
-
return crypto.createHash('md5').update(JSON.stringify(definition, Object.keys(definition).sort())).digest('hex');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Generate a unique PascalCase key for a definition, with proper deduplication
|
|
38
|
-
* @param {string} originalKey - The original definition key
|
|
39
|
-
* @param {Object} definition - The schema definition
|
|
40
|
-
* @param {string} path - The path where this definition was found
|
|
41
|
-
* @returns {string} - Unique PascalCase key or existing key if duplicate
|
|
42
|
-
*/
|
|
43
|
-
generateKey(originalKey, definition, path) {
|
|
44
|
-
// First check if we've already seen this exact definition
|
|
45
|
-
const defHash = this.hashDefinition(definition);
|
|
46
|
-
if (this.definitionHashes.has(defHash)) {
|
|
47
|
-
// This is a duplicate definition, return the existing key
|
|
48
|
-
return this.definitionHashes.get(defHash);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
let baseName = '';
|
|
52
|
-
|
|
53
|
-
// First try to get the title from the definition
|
|
54
|
-
if (definition && definition.title) {
|
|
55
|
-
baseName = definition.title;
|
|
56
|
-
} else {
|
|
57
|
-
// Use the original key as fallback
|
|
58
|
-
baseName = originalKey;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Convert to PascalCase
|
|
62
|
-
let pascalCase = baseName
|
|
63
|
-
.replace(/[^a-zA-Z0-9]/g, ' ')
|
|
64
|
-
.split(' ')
|
|
65
|
-
.filter(Boolean)
|
|
66
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
67
|
-
.join('');
|
|
68
|
-
|
|
69
|
-
// Ensure it starts with a letter
|
|
70
|
-
if (!/^[A-Z]/.test(pascalCase)) {
|
|
71
|
-
pascalCase = 'Schema' + pascalCase;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Only add hash if we have a key name conflict (different definition, same name)
|
|
75
|
-
if (this.seenKeys.has(pascalCase)) {
|
|
76
|
-
const pathHash = crypto.createHash('md5').update(path).digest('hex').substring(0, 8);
|
|
77
|
-
pascalCase = `${pascalCase}${pathHash.charAt(0).toUpperCase()}${pathHash.slice(1)}`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
this.seenKeys.add(pascalCase);
|
|
81
|
-
this.definitionHashes.set(defHash, pascalCase);
|
|
82
|
-
return pascalCase;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Check if a reference is related to Vega/Vega-Lite
|
|
87
|
-
* @param {string} ref - Reference string
|
|
88
|
-
* @returns {boolean}
|
|
89
|
-
*/
|
|
90
|
-
isVegaSchema(ref) {
|
|
91
|
-
if (typeof ref !== 'string') {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const lower = ref.toLowerCase();
|
|
96
|
-
return lower.includes('vega-lite') ||
|
|
97
|
-
lower.includes('vega/') ||
|
|
98
|
-
lower.includes('vega.json') ||
|
|
99
|
-
lower.includes('vega-schema') ||
|
|
100
|
-
lower.includes('vega-lite-schema') ||
|
|
101
|
-
lower.includes('/vega') ||
|
|
102
|
-
lower.includes('vegaspec') ||
|
|
103
|
-
lower.includes('spec') && lower.includes('vega');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Create a placeholder for Vega schemas
|
|
108
|
-
* @returns {Object} - PlotSpec placeholder definition
|
|
109
|
-
*/
|
|
110
|
-
createVegaPlaceholder() {
|
|
111
|
-
return {
|
|
112
|
-
type: 'object',
|
|
113
|
-
title: 'PlotSpec',
|
|
114
|
-
description: 'Placeholder for Vega/Vega-Lite plot specifications',
|
|
115
|
-
properties: {
|
|
116
|
-
$schema: {
|
|
117
|
-
type: 'string',
|
|
118
|
-
description: 'The Vega/Vega-Lite schema URL'
|
|
119
|
-
},
|
|
120
|
-
data: {
|
|
121
|
-
type: 'object',
|
|
122
|
-
description: 'The data specification'
|
|
123
|
-
},
|
|
124
|
-
mark: {
|
|
125
|
-
type: ['string', 'object'],
|
|
126
|
-
description: 'The mark type or definition'
|
|
127
|
-
},
|
|
128
|
-
encoding: {
|
|
129
|
-
type: 'object',
|
|
130
|
-
description: 'The encoding specification'
|
|
131
|
-
},
|
|
132
|
-
config: {
|
|
133
|
-
type: 'object',
|
|
134
|
-
description: 'The configuration options'
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
additionalProperties: true
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Check if we should recurse into a definition based on its path/context
|
|
143
|
-
* @param {string} path - Definition path
|
|
144
|
-
* @param {string} key - Definition key
|
|
145
|
-
* @returns {boolean}
|
|
146
|
-
*/
|
|
147
|
-
shouldRecurseIntoDefinition(path, key) {
|
|
148
|
-
// Don't recurse into Vega schemas at all
|
|
149
|
-
if (this.isVegaSchema(key) || this.isVegaSchema(path)) {
|
|
150
|
-
return false;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Only recurse into EIDOS-specific definitions that might contain nested node types
|
|
154
|
-
// This includes paths like /$defs/node which contains the oneOf with World, Plot, etc.
|
|
155
|
-
return true;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Recursively extract all definitions from a schema object
|
|
160
|
-
* @param {Object} obj - Schema object to search
|
|
161
|
-
* @param {Map} collectedDefs - Map to collect definitions
|
|
162
|
-
* @param {string} currentPath - Current path for tracking definition locations
|
|
163
|
-
*/
|
|
164
|
-
extractDefinitions(obj, collectedDefs, currentPath = '') {
|
|
165
|
-
if (!obj || typeof obj !== 'object') {
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (Array.isArray(obj)) {
|
|
170
|
-
obj.forEach((item, index) => {
|
|
171
|
-
this.extractDefinitions(item, collectedDefs, `${currentPath}[${index}]`);
|
|
172
|
-
});
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Check for $defs or definitions at this level
|
|
177
|
-
if (obj.$defs) {
|
|
178
|
-
for (const [key, def] of Object.entries(obj.$defs)) {
|
|
179
|
-
const defPath = `${currentPath}/$defs/${key}`;
|
|
180
|
-
const normalizedKey = decodeURIComponent(key);
|
|
181
|
-
collectedDefs.set(defPath, {
|
|
182
|
-
definition: def,
|
|
183
|
-
originalKey: normalizedKey,
|
|
184
|
-
fullPath: defPath
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// Only recursively extract from EIDOS-specific definitions
|
|
188
|
-
if (this.shouldRecurseIntoDefinition(defPath, normalizedKey)) {
|
|
189
|
-
this.extractDefinitions(def, collectedDefs, defPath);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (obj.definitions) {
|
|
195
|
-
for (const [key, def] of Object.entries(obj.definitions)) {
|
|
196
|
-
const defPath = `${currentPath}/definitions/${key}`;
|
|
197
|
-
const normalizedKey = decodeURIComponent(key);
|
|
198
|
-
collectedDefs.set(defPath, {
|
|
199
|
-
definition: def,
|
|
200
|
-
originalKey: normalizedKey,
|
|
201
|
-
fullPath: defPath
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
// Only recursively extract from EIDOS-specific definitions
|
|
205
|
-
if (this.shouldRecurseIntoDefinition(defPath, normalizedKey)) {
|
|
206
|
-
this.extractDefinitions(def, collectedDefs, defPath);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Recursively search all properties
|
|
212
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
213
|
-
if (key !== '$defs' && key !== 'definitions') {
|
|
214
|
-
this.extractDefinitions(value, collectedDefs, `${currentPath}/${key}`);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Recursively collect all $refs in an object
|
|
221
|
-
* @param {*} obj - Object to search
|
|
222
|
-
* @param {Set} refs - Set to collect refs into
|
|
223
|
-
*/
|
|
224
|
-
collectRefs(obj, refs) {
|
|
225
|
-
if (!obj || typeof obj !== 'object') {
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (Array.isArray(obj)) {
|
|
230
|
-
obj.forEach(item => this.collectRefs(item, refs));
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
235
|
-
if (key === '$ref' && typeof value === 'string') {
|
|
236
|
-
refs.add(decodeURIComponent(value));
|
|
237
|
-
} else {
|
|
238
|
-
this.collectRefs(value, refs);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Rewrite all $ref pointers in a schema to use new keys
|
|
245
|
-
* @param {Object} obj - Object to process
|
|
246
|
-
* @returns {Object} - Processed object with updated refs
|
|
247
|
-
*/
|
|
248
|
-
rewriteRefs(obj) {
|
|
249
|
-
if (!obj || typeof obj !== 'object') {
|
|
250
|
-
return obj;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (Array.isArray(obj)) {
|
|
254
|
-
return obj.map(item => this.rewriteRefs(item));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const result = {};
|
|
258
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
259
|
-
if (key === '$ref' && typeof value === 'string') {
|
|
260
|
-
const normalizedRef = decodeURIComponent(value);
|
|
261
|
-
const newKey = this.keyMapping.get(normalizedRef);
|
|
262
|
-
|
|
263
|
-
if (newKey) {
|
|
264
|
-
result[key] = `#/$defs/${newKey}`;
|
|
265
|
-
} else {
|
|
266
|
-
// Keep original ref if no mapping found
|
|
267
|
-
result[key] = value;
|
|
268
|
-
}
|
|
269
|
-
} else {
|
|
270
|
-
result[key] = this.rewriteRefs(value);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return result;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Fetch a schema from URL or file path
|
|
279
|
-
* @param {string} schemaPath - URL or file path
|
|
280
|
-
* @returns {Promise<Object>} - Parsed schema
|
|
281
|
-
*/
|
|
282
|
-
async fetchSchema(schemaPath) {
|
|
283
|
-
if (schemaPath.startsWith('http://') || schemaPath.startsWith('https://')) {
|
|
284
|
-
return new Promise((resolve, reject) => {
|
|
285
|
-
https.get(schemaPath, (res) => {
|
|
286
|
-
let data = '';
|
|
287
|
-
res.on('data', (chunk) => data += chunk);
|
|
288
|
-
res.on('end', () => {
|
|
289
|
-
try {
|
|
290
|
-
resolve(JSON.parse(data));
|
|
291
|
-
} catch (err) {
|
|
292
|
-
reject(err);
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
}).on('error', reject);
|
|
296
|
-
});
|
|
297
|
-
} else {
|
|
298
|
-
const data = fs.readFileSync(schemaPath, 'utf8');
|
|
299
|
-
return JSON.parse(data);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Recursively collect all external schemas referenced by the root schema
|
|
305
|
-
* @param {Object} schema - Schema to analyze
|
|
306
|
-
* @param {string} baseUrl - Base URL for resolving relative refs
|
|
307
|
-
* @param {Map} schemas - Map to store all loaded schemas
|
|
308
|
-
* @param {Set} visited - Set to track visited URLs to prevent loops
|
|
309
|
-
* @returns {Promise<void>}
|
|
310
|
-
*/
|
|
311
|
-
async collectSchemas(schema, baseUrl, schemas, visited = new Set()) {
|
|
312
|
-
if (!schema || typeof schema !== 'object') {
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (Array.isArray(schema)) {
|
|
317
|
-
for (const item of schema) {
|
|
318
|
-
await this.collectSchemas(item, baseUrl, schemas, visited);
|
|
319
|
-
}
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
for (const [key, value] of Object.entries(schema)) {
|
|
324
|
-
if (key === '$ref' && typeof value === 'string' && !value.startsWith('#/')) {
|
|
325
|
-
// External reference
|
|
326
|
-
const refUrl = new URL(value, baseUrl).href;
|
|
327
|
-
|
|
328
|
-
// Skip Vega schemas entirely - don't even fetch them
|
|
329
|
-
if (this.isVegaSchema(refUrl)) {
|
|
330
|
-
console.log(`Skipping Vega schema: ${refUrl}`);
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (!visited.has(refUrl) && !schemas.has(refUrl)) {
|
|
335
|
-
visited.add(refUrl);
|
|
336
|
-
try {
|
|
337
|
-
const refSchema = await this.fetchSchema(refUrl);
|
|
338
|
-
schemas.set(refUrl, refSchema);
|
|
339
|
-
// Recursively collect schemas from this schema (but not if it's Vega)
|
|
340
|
-
if (!this.isVegaSchema(refUrl)) {
|
|
341
|
-
await this.collectSchemas(refSchema, refUrl, schemas, visited);
|
|
342
|
-
}
|
|
343
|
-
} catch (err) {
|
|
344
|
-
console.warn(`Failed to fetch ${refUrl}: ${err.message}`);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
} else if (typeof value === 'object') {
|
|
348
|
-
await this.collectSchemas(value, baseUrl, schemas, visited);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Extract a type name from a schema URL
|
|
355
|
-
* @param {string} url - Schema URL
|
|
356
|
-
* @returns {string} - Type name in PascalCase
|
|
357
|
-
*/
|
|
358
|
-
extractTypeNameFromUrl(url) {
|
|
359
|
-
const parts = url.split('/');
|
|
360
|
-
const filename = parts[parts.length - 1];
|
|
361
|
-
if (filename.endsWith('.json')) {
|
|
362
|
-
const typeName = filename.replace('.json', '');
|
|
363
|
-
// Convert to PascalCase
|
|
364
|
-
return typeName.charAt(0).toUpperCase() + typeName.slice(1);
|
|
365
|
-
}
|
|
366
|
-
return 'UnknownType';
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Inline external references in a schema and create named definitions for inlined content
|
|
371
|
-
* @param {Object} schema - Schema to process
|
|
372
|
-
* @param {string} baseUrl - Base URL for resolving references
|
|
373
|
-
* @param {Map} allSchemas - Map of all loaded schemas
|
|
374
|
-
* @param {Map} inlinedDefs - Map to collect inlined definitions
|
|
375
|
-
* @param {Set} visited - Set of visited URLs to prevent circular references
|
|
376
|
-
* @returns {Object} - Schema with inlined references
|
|
377
|
-
*/
|
|
378
|
-
inlineExternalRefs(schema, baseUrl, allSchemas, inlinedDefs = new Map(), visited = new Set()) {
|
|
379
|
-
if (!schema || typeof schema !== 'object') {
|
|
380
|
-
return schema;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (Array.isArray(schema)) {
|
|
384
|
-
return schema.map(item => this.inlineExternalRefs(item, baseUrl, allSchemas, inlinedDefs, visited));
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const result = {};
|
|
388
|
-
for (const [key, value] of Object.entries(schema)) {
|
|
389
|
-
if (key === '$ref' && typeof value === 'string' && !value.startsWith('#/')) {
|
|
390
|
-
// External reference - replace with actual schema content
|
|
391
|
-
const refUrl = new URL(value, baseUrl).href;
|
|
392
|
-
|
|
393
|
-
// Skip Vega schemas - replace with PlotSpec placeholder
|
|
394
|
-
if (this.isVegaSchema(refUrl)) {
|
|
395
|
-
return this.createVegaPlaceholder();
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Prevent circular references
|
|
399
|
-
if (visited.has(refUrl)) {
|
|
400
|
-
console.warn(`Warning: Circular reference detected: ${refUrl}`);
|
|
401
|
-
// Create a reference to the type name instead
|
|
402
|
-
const typeName = this.extractTypeNameFromUrl(refUrl);
|
|
403
|
-
return { $ref: `#/$defs/${typeName}` };
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
if (allSchemas.has(refUrl)) {
|
|
407
|
-
const referencedSchema = allSchemas.get(refUrl);
|
|
408
|
-
const typeName = this.extractTypeNameFromUrl(refUrl);
|
|
409
|
-
|
|
410
|
-
// Store the inlined definition with the type name
|
|
411
|
-
if (!inlinedDefs.has(typeName)) {
|
|
412
|
-
const newVisited = new Set(visited);
|
|
413
|
-
newVisited.add(refUrl);
|
|
414
|
-
const inlinedContent = this.inlineExternalRefs(referencedSchema, refUrl, allSchemas, inlinedDefs, newVisited);
|
|
415
|
-
inlinedDefs.set(typeName, inlinedContent);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Return a reference to the newly created definition
|
|
419
|
-
return { $ref: `#/$defs/${typeName}` };
|
|
420
|
-
} else {
|
|
421
|
-
console.warn(`Warning: External reference not found: ${refUrl}`);
|
|
422
|
-
// Keep the reference as-is if we can't resolve it
|
|
423
|
-
result[key] = value;
|
|
424
|
-
}
|
|
425
|
-
} else {
|
|
426
|
-
result[key] = this.inlineExternalRefs(value, baseUrl, allSchemas, inlinedDefs, visited);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
return result;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* Bundle a schema from a root schema URL/path
|
|
435
|
-
* @param {string} rootSchemaPath - Path or URL to the root schema
|
|
436
|
-
* @returns {Promise<Object>} - Bundled schema
|
|
437
|
-
*/
|
|
438
|
-
async bundle(rootSchemaPath) {
|
|
439
|
-
console.log(`📦 Starting schema bundling from: ${rootSchemaPath}`);
|
|
440
|
-
|
|
441
|
-
try {
|
|
442
|
-
// First, load the root schema and collect all external schemas
|
|
443
|
-
console.log('📥 Loading root schema and collecting external references...');
|
|
444
|
-
const rootSchema = await this.fetchSchema(rootSchemaPath);
|
|
445
|
-
const baseUrl = rootSchemaPath.startsWith('http') ? rootSchemaPath : new URL(rootSchemaPath, import.meta.url).href;
|
|
446
|
-
|
|
447
|
-
const allSchemas = new Map();
|
|
448
|
-
allSchemas.set(baseUrl, rootSchema);
|
|
449
|
-
await this.collectSchemas(rootSchema, baseUrl, allSchemas);
|
|
450
|
-
|
|
451
|
-
console.log(`Collected ${allSchemas.size} schemas`);
|
|
452
|
-
|
|
453
|
-
// Inline all external references into the root schema
|
|
454
|
-
console.log('🔄 Inlining external references...');
|
|
455
|
-
const inlinedDefs = new Map();
|
|
456
|
-
const inlinedSchema = this.inlineExternalRefs(rootSchema, baseUrl, allSchemas, inlinedDefs);
|
|
457
|
-
|
|
458
|
-
// Extract ALL definitions from all schemas (original approach)
|
|
459
|
-
console.log('🔄 Processing definitions from all schemas...');
|
|
460
|
-
|
|
461
|
-
const collectedDefs = new Map();
|
|
462
|
-
for (const [schemaUrl, schema] of allSchemas) {
|
|
463
|
-
this.extractDefinitions(schema, collectedDefs, schemaUrl);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Also add the inlined definitions (World, Plot, Document, etc.) as separate named definitions
|
|
467
|
-
for (const [typeName, typeDef] of inlinedDefs) {
|
|
468
|
-
collectedDefs.set(`/$defs/${typeName}`, {
|
|
469
|
-
definition: typeDef,
|
|
470
|
-
originalKey: typeName,
|
|
471
|
-
fullPath: `/$defs/${typeName}`
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
console.log(`Found ${collectedDefs.size} definitions from all schemas + inlined types`);
|
|
476
|
-
|
|
477
|
-
// Collect all refs in the inlined schema (we'll rewrite these)
|
|
478
|
-
const allRefs = new Set();
|
|
479
|
-
this.collectRefs(inlinedSchema, allRefs);
|
|
480
|
-
console.log(`Found ${allRefs.size} total references in inlined schema`);
|
|
481
|
-
|
|
482
|
-
// Process all definitions and create mappings
|
|
483
|
-
for (const [defPath, { definition, originalKey, fullPath }] of collectedDefs) {
|
|
484
|
-
if (this.isVegaSchema(originalKey)) {
|
|
485
|
-
// Handle Vega schemas with PlotSpec placeholder
|
|
486
|
-
if (!this.definitions.has('PlotSpec')) {
|
|
487
|
-
this.keyMapping.set(`#/$defs/${originalKey}`, 'PlotSpec');
|
|
488
|
-
this.keyMapping.set(`#/definitions/${originalKey}`, 'PlotSpec');
|
|
489
|
-
this.definitions.set('PlotSpec', this.createVegaPlaceholder());
|
|
490
|
-
}
|
|
491
|
-
} else {
|
|
492
|
-
// Generate unique PascalCase key for this definition (handles deduplication)
|
|
493
|
-
const newKey = this.generateKey(originalKey, definition, fullPath);
|
|
494
|
-
|
|
495
|
-
// Map all possible reference formats for this definition
|
|
496
|
-
this.keyMapping.set(`#/$defs/${originalKey}`, newKey);
|
|
497
|
-
this.keyMapping.set(`#/definitions/${originalKey}`, newKey);
|
|
498
|
-
|
|
499
|
-
// Only store the definition if it's not already stored (deduplication)
|
|
500
|
-
if (!this.definitions.has(newKey)) {
|
|
501
|
-
this.definitions.set(newKey, definition);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// Handle any unmapped refs by trying to find their definitions
|
|
507
|
-
for (const ref of allRefs) {
|
|
508
|
-
if (!this.keyMapping.has(ref)) {
|
|
509
|
-
if (this.isVegaSchema(ref)) {
|
|
510
|
-
this.keyMapping.set(ref, 'PlotSpec');
|
|
511
|
-
if (!this.definitions.has('PlotSpec')) {
|
|
512
|
-
this.definitions.set('PlotSpec', this.createVegaPlaceholder());
|
|
513
|
-
}
|
|
514
|
-
} else {
|
|
515
|
-
// Try to find the definition for this ref
|
|
516
|
-
const refParts = ref.split('/');
|
|
517
|
-
const refKey = refParts[refParts.length - 1];
|
|
518
|
-
const cleanRefKey = decodeURIComponent(refKey);
|
|
519
|
-
|
|
520
|
-
// Look for a matching definition
|
|
521
|
-
let foundDef = null;
|
|
522
|
-
for (const [defPath, { definition, originalKey }] of collectedDefs) {
|
|
523
|
-
if (originalKey === cleanRefKey || decodeURIComponent(originalKey) === cleanRefKey) {
|
|
524
|
-
foundDef = definition;
|
|
525
|
-
break;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if (foundDef) {
|
|
530
|
-
const newKey = this.generateKey(cleanRefKey, foundDef, ref);
|
|
531
|
-
this.keyMapping.set(ref, newKey);
|
|
532
|
-
// Only store if not already stored (deduplication)
|
|
533
|
-
if (!this.definitions.has(newKey)) {
|
|
534
|
-
this.definitions.set(newKey, foundDef);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
console.log(`✅ Created ${this.definitions.size} unique definitions with ${this.keyMapping.size} reference mappings`);
|
|
542
|
-
|
|
543
|
-
// Create the final schema with rewritten references
|
|
544
|
-
console.log('🔧 Rewriting references...');
|
|
545
|
-
|
|
546
|
-
const finalSchema = this.rewriteRefs({
|
|
547
|
-
...inlinedSchema,
|
|
548
|
-
$defs: undefined, // Remove original $defs
|
|
549
|
-
definitions: undefined // Remove original definitions
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
// Add the new flattened $defs section with PascalCase keys
|
|
553
|
-
finalSchema.$defs = {};
|
|
554
|
-
for (const [key, definition] of this.definitions) {
|
|
555
|
-
finalSchema.$defs[key] = this.rewriteRefs(definition);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// Always ensure PlotSpec placeholder exists
|
|
559
|
-
if (!finalSchema.$defs.PlotSpec) {
|
|
560
|
-
console.log('Adding PlotSpec placeholder definition');
|
|
561
|
-
finalSchema.$defs.PlotSpec = this.createVegaPlaceholder();
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
console.log(`✅ Schema bundling completed! Generated ${Object.keys(finalSchema.$defs).length} definitions.`);
|
|
565
|
-
|
|
566
|
-
return finalSchema;
|
|
567
|
-
|
|
568
|
-
} catch (error) {
|
|
569
|
-
console.error('❌ Error during schema bundling:', error);
|
|
570
|
-
throw error;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* Export function for bundling schemas
|
|
577
|
-
* @param {string} rootSchemaPath - Path or URL to the root schema
|
|
578
|
-
* @returns {Promise<Object>} - Bundled schema
|
|
579
|
-
*/
|
|
580
|
-
export async function bundle(rootSchemaPath) {
|
|
581
|
-
const bundler = new SchemaBundler();
|
|
582
|
-
return await bundler.bundle(rootSchemaPath);
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// CLI support - run bundler if called directly
|
|
586
|
-
if (import.meta.url === `file://${process.argv[1]}` ||
|
|
587
|
-
import.meta.url.endsWith(process.argv[1])) {
|
|
588
|
-
|
|
589
|
-
const rootSchema = process.argv[2] || 'https://schemas.oceanum.io/eidos/root.json';
|
|
590
|
-
const outputFile = process.argv[3];
|
|
591
|
-
|
|
592
|
-
console.log('🚀 Running schema bundler CLI...');
|
|
593
|
-
|
|
594
|
-
try {
|
|
595
|
-
const result = await bundle(rootSchema);
|
|
596
|
-
|
|
597
|
-
if (outputFile) {
|
|
598
|
-
const fs = await import('fs');
|
|
599
|
-
fs.writeFileSync(outputFile, JSON.stringify(result, null, 2));
|
|
600
|
-
console.log(`📄 Bundled schema written to: ${outputFile}`);
|
|
601
|
-
} else {
|
|
602
|
-
console.log('📄 Bundled schema:');
|
|
603
|
-
console.log(JSON.stringify(result, null, 2));
|
|
604
|
-
}
|
|
605
|
-
} catch (error) {
|
|
606
|
-
console.error('❌ CLI bundling failed:', error);
|
|
607
|
-
process.exit(1);
|
|
608
|
-
}
|
|
609
|
-
}
|