@flink-app/ts-source-to-json-schema 0.1.0
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/LICENSE +21 -0
- package/README.md +574 -0
- package/dist/ast.d.ts +102 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +5 -0
- package/dist/ast.js.map +1 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +276 -0
- package/dist/cli.js.map +1 -0
- package/dist/emitter.d.ts +120 -0
- package/dist/emitter.d.ts.map +1 -0
- package/dist/emitter.js +808 -0
- package/dist/emitter.js.map +1 -0
- package/dist/import-parser.d.ts +18 -0
- package/dist/import-parser.d.ts.map +1 -0
- package/dist/import-parser.js +117 -0
- package/dist/import-parser.js.map +1 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +177 -0
- package/dist/index.js.map +1 -0
- package/dist/module-resolver.d.ts +29 -0
- package/dist/module-resolver.d.ts.map +1 -0
- package/dist/module-resolver.js +95 -0
- package/dist/module-resolver.js.map +1 -0
- package/dist/parser.d.ts +38 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +464 -0
- package/dist/parser.js.map +1 -0
- package/dist/path-utils.d.ts +16 -0
- package/dist/path-utils.d.ts.map +1 -0
- package/dist/path-utils.js +63 -0
- package/dist/path-utils.js.map +1 -0
- package/dist/tokenizer.d.ts +9 -0
- package/dist/tokenizer.d.ts.map +1 -0
- package/dist/tokenizer.js +192 -0
- package/dist/tokenizer.js.map +1 -0
- package/package.json +59 -0
package/dist/emitter.js
ADDED
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Emitter - Transforms AST nodes into JSON Schema (2020-12 draft)
|
|
3
|
+
// ============================================================================
|
|
4
|
+
export class Emitter {
|
|
5
|
+
declarations = new Map();
|
|
6
|
+
options;
|
|
7
|
+
constructor(declarations, options = {}) {
|
|
8
|
+
for (const decl of declarations) {
|
|
9
|
+
this.declarations.set(decl.name, decl);
|
|
10
|
+
}
|
|
11
|
+
this.options = {
|
|
12
|
+
includeSchema: options.includeSchema ?? true,
|
|
13
|
+
schemaVersion: options.schemaVersion ?? "https://json-schema.org/draft/2020-12/schema",
|
|
14
|
+
strictObjects: options.strictObjects ?? false,
|
|
15
|
+
rootType: options.rootType ?? "",
|
|
16
|
+
includeJSDoc: options.includeJSDoc ?? true,
|
|
17
|
+
additionalProperties: options.additionalProperties,
|
|
18
|
+
followImports: options.followImports ?? "none",
|
|
19
|
+
baseDir: options.baseDir ?? "",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
emit() {
|
|
23
|
+
const defs = {};
|
|
24
|
+
// Emit all declarations into $defs
|
|
25
|
+
for (const [name, decl] of this.declarations) {
|
|
26
|
+
defs[name] = this.emitDeclaration(decl);
|
|
27
|
+
}
|
|
28
|
+
// If a root type is specified, use it as the root schema
|
|
29
|
+
if (this.options.rootType && defs[this.options.rootType]) {
|
|
30
|
+
const root = defs[this.options.rootType];
|
|
31
|
+
// Check if root type is self-referential (directly or transitively)
|
|
32
|
+
const isSelfReferential = this.isTransitivelySelfReferential(this.options.rootType, defs);
|
|
33
|
+
if (isSelfReferential) {
|
|
34
|
+
// Keep root in $defs and make root a $ref to it
|
|
35
|
+
const result = {
|
|
36
|
+
$ref: `#/$defs/${this.options.rootType}`,
|
|
37
|
+
};
|
|
38
|
+
if (this.options.includeSchema) {
|
|
39
|
+
result.$schema = this.options.schemaVersion;
|
|
40
|
+
}
|
|
41
|
+
result.$defs = defs;
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
// Not self-referential, emit normally
|
|
45
|
+
delete defs[this.options.rootType];
|
|
46
|
+
const result = { ...root };
|
|
47
|
+
if (this.options.includeSchema) {
|
|
48
|
+
result.$schema = this.options.schemaVersion;
|
|
49
|
+
}
|
|
50
|
+
if (Object.keys(defs).length > 0) {
|
|
51
|
+
result.$defs = defs;
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
// Otherwise wrap everything under $defs
|
|
56
|
+
const result = {};
|
|
57
|
+
if (this.options.includeSchema) {
|
|
58
|
+
result.$schema = this.options.schemaVersion;
|
|
59
|
+
}
|
|
60
|
+
result.$defs = defs;
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Emits schemas for all declarations at once.
|
|
65
|
+
* More efficient than calling emit() multiple times.
|
|
66
|
+
* Each schema is standalone with only its transitively referenced types in definitions.
|
|
67
|
+
*/
|
|
68
|
+
emitAll() {
|
|
69
|
+
const schemas = {};
|
|
70
|
+
for (const [typeName, decl] of this.declarations) {
|
|
71
|
+
schemas[typeName] = this.emitDeclarationStandalone(decl);
|
|
72
|
+
}
|
|
73
|
+
return schemas;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Emits a declaration as a standalone schema with minimal definitions.
|
|
77
|
+
* Only includes types that are transitively referenced.
|
|
78
|
+
* Uses "definitions" (draft-07 style) instead of "$defs" for compatibility.
|
|
79
|
+
*/
|
|
80
|
+
emitDeclarationStandalone(decl) {
|
|
81
|
+
// Find direct references from this declaration (not including the type itself)
|
|
82
|
+
const directRefs = this.findDirectReferences(decl);
|
|
83
|
+
// Collect all types transitively referenced (excluding the starting type)
|
|
84
|
+
const referencedTypes = new Set();
|
|
85
|
+
for (const ref of directRefs) {
|
|
86
|
+
this.collectTransitiveReferences(ref, referencedTypes);
|
|
87
|
+
}
|
|
88
|
+
// Check if this type is self-referential
|
|
89
|
+
const selfReferenced = referencedTypes.has(decl.name);
|
|
90
|
+
// Emit the main schema
|
|
91
|
+
const schema = this.emitDeclaration(decl);
|
|
92
|
+
// Build minimal definitions object
|
|
93
|
+
const definitions = {};
|
|
94
|
+
// If type is self-referential, include it in its own definitions
|
|
95
|
+
if (selfReferenced) {
|
|
96
|
+
definitions[decl.name] = schema;
|
|
97
|
+
}
|
|
98
|
+
// Add all referenced types
|
|
99
|
+
for (const typeName of referencedTypes) {
|
|
100
|
+
if (typeName !== decl.name) {
|
|
101
|
+
const referencedDecl = this.declarations.get(typeName);
|
|
102
|
+
if (referencedDecl) {
|
|
103
|
+
definitions[typeName] = this.emitDeclaration(referencedDecl);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Convert $ref paths from #/$defs/ to #/definitions/
|
|
108
|
+
const schemaWithDefinitions = this.convertRefsToDefinitions(schema);
|
|
109
|
+
const convertedDefinitions = {};
|
|
110
|
+
for (const [name, def] of Object.entries(definitions)) {
|
|
111
|
+
convertedDefinitions[name] = this.convertRefsToDefinitions(def);
|
|
112
|
+
}
|
|
113
|
+
// Build result schema with $schema if enabled
|
|
114
|
+
const result = {
|
|
115
|
+
...schemaWithDefinitions,
|
|
116
|
+
definitions: Object.keys(convertedDefinitions).length > 0 ? convertedDefinitions : {}
|
|
117
|
+
};
|
|
118
|
+
// Add $schema if enabled (default: true)
|
|
119
|
+
if (this.options.includeSchema !== false) {
|
|
120
|
+
result.$schema = this.options.schemaVersion || "https://json-schema.org/draft/2020-12/schema";
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Recursively collects all type names that are transitively referenced by a given type.
|
|
126
|
+
*/
|
|
127
|
+
collectTransitiveReferences(startTypeName, visited = new Set()) {
|
|
128
|
+
if (visited.has(startTypeName)) {
|
|
129
|
+
return visited;
|
|
130
|
+
}
|
|
131
|
+
visited.add(startTypeName);
|
|
132
|
+
const decl = this.declarations.get(startTypeName);
|
|
133
|
+
if (!decl)
|
|
134
|
+
return visited;
|
|
135
|
+
// Find all direct references in this declaration
|
|
136
|
+
const directRefs = this.findDirectReferences(decl);
|
|
137
|
+
// Recursively collect references from those types
|
|
138
|
+
for (const ref of directRefs) {
|
|
139
|
+
this.collectTransitiveReferences(ref, visited);
|
|
140
|
+
}
|
|
141
|
+
return visited;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Finds all direct type references in a declaration.
|
|
145
|
+
*/
|
|
146
|
+
findDirectReferences(decl) {
|
|
147
|
+
const refs = [];
|
|
148
|
+
const collectRefs = (typeNode) => {
|
|
149
|
+
switch (typeNode.kind) {
|
|
150
|
+
case "reference":
|
|
151
|
+
// Skip built-in types
|
|
152
|
+
if (!this.isBuiltInType(typeNode.name)) {
|
|
153
|
+
refs.push(typeNode.name);
|
|
154
|
+
}
|
|
155
|
+
// Also collect from type arguments
|
|
156
|
+
if (typeNode.typeArgs) {
|
|
157
|
+
typeNode.typeArgs.forEach(arg => collectRefs(arg));
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
case "object":
|
|
161
|
+
typeNode.properties.forEach(p => collectRefs(p.type));
|
|
162
|
+
if (typeNode.indexSignature) {
|
|
163
|
+
collectRefs(typeNode.indexSignature.keyType);
|
|
164
|
+
collectRefs(typeNode.indexSignature.valueType);
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
case "array":
|
|
168
|
+
collectRefs(typeNode.element);
|
|
169
|
+
break;
|
|
170
|
+
case "tuple":
|
|
171
|
+
typeNode.elements.forEach(e => collectRefs(e.type));
|
|
172
|
+
break;
|
|
173
|
+
case "union":
|
|
174
|
+
typeNode.members.forEach(m => collectRefs(m));
|
|
175
|
+
break;
|
|
176
|
+
case "intersection":
|
|
177
|
+
typeNode.members.forEach(m => collectRefs(m));
|
|
178
|
+
break;
|
|
179
|
+
case "parenthesized":
|
|
180
|
+
collectRefs(typeNode.inner);
|
|
181
|
+
break;
|
|
182
|
+
case "record":
|
|
183
|
+
collectRefs(typeNode.keyType);
|
|
184
|
+
collectRefs(typeNode.valueType);
|
|
185
|
+
break;
|
|
186
|
+
case "mapped":
|
|
187
|
+
collectRefs(typeNode.constraint);
|
|
188
|
+
collectRefs(typeNode.valueType);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
if (decl.kind === "interface") {
|
|
193
|
+
decl.properties.forEach(p => collectRefs(p.type));
|
|
194
|
+
if (decl.extends) {
|
|
195
|
+
decl.extends.forEach(e => collectRefs(e));
|
|
196
|
+
}
|
|
197
|
+
if (decl.indexSignature) {
|
|
198
|
+
collectRefs(decl.indexSignature.keyType);
|
|
199
|
+
collectRefs(decl.indexSignature.valueType);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else if (decl.kind === "type_alias") {
|
|
203
|
+
collectRefs(decl.type);
|
|
204
|
+
}
|
|
205
|
+
// Enums don't have type references
|
|
206
|
+
return refs;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Checks if a type name refers to a built-in type that shouldn't be in definitions.
|
|
210
|
+
*/
|
|
211
|
+
isBuiltInType(name) {
|
|
212
|
+
const builtIns = [
|
|
213
|
+
"Date", "Promise", "Array", "Set", "Map", "Record",
|
|
214
|
+
"Partial", "Required", "Pick", "Omit", "Readonly", "NonNullable"
|
|
215
|
+
];
|
|
216
|
+
return builtIns.includes(name);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Recursively converts $ref paths from #/$defs/ to #/definitions/ in a schema.
|
|
220
|
+
*/
|
|
221
|
+
convertRefsToDefinitions(schema) {
|
|
222
|
+
if (typeof schema !== "object" || schema === null) {
|
|
223
|
+
return schema;
|
|
224
|
+
}
|
|
225
|
+
const converted = {};
|
|
226
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
227
|
+
if (key === "$ref" && typeof value === "string") {
|
|
228
|
+
// Convert #/$defs/TypeName to #/definitions/TypeName
|
|
229
|
+
converted[key] = value.replace(/^#\/\$defs\//, "#/definitions/");
|
|
230
|
+
}
|
|
231
|
+
else if (Array.isArray(value)) {
|
|
232
|
+
converted[key] = value.map(item => typeof item === "object" ? this.convertRefsToDefinitions(item) : item);
|
|
233
|
+
}
|
|
234
|
+
else if (typeof value === "object" && value !== null) {
|
|
235
|
+
converted[key] = this.convertRefsToDefinitions(value);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
converted[key] = value;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return converted;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Checks if a schema contains a reference to a specific type name.
|
|
245
|
+
* Used to detect self-referential types.
|
|
246
|
+
*/
|
|
247
|
+
containsReference(schema, typeName) {
|
|
248
|
+
if (typeof schema !== "object" || schema === null) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
// Check if this is a direct reference to the type
|
|
252
|
+
if (schema.$ref === `#/$defs/${typeName}`) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
// Recursively check all properties
|
|
256
|
+
for (const value of Object.values(schema)) {
|
|
257
|
+
if (Array.isArray(value)) {
|
|
258
|
+
for (const item of value) {
|
|
259
|
+
if (typeof item === "object" && this.containsReference(item, typeName)) {
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else if (typeof value === "object" && value !== null) {
|
|
265
|
+
if (this.containsReference(value, typeName)) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Checks if a type is transitively self-referential.
|
|
274
|
+
* This includes both direct recursion (A → A) and mutual recursion (A → B → A).
|
|
275
|
+
*/
|
|
276
|
+
isTransitivelySelfReferential(typeName, defs) {
|
|
277
|
+
// Helper to check if typeName is reachable from startType
|
|
278
|
+
const canReach = (startType, targetType, visited = new Set()) => {
|
|
279
|
+
if (startType === targetType) {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
if (visited.has(startType)) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
visited.add(startType);
|
|
286
|
+
const schema = defs[startType];
|
|
287
|
+
if (!schema) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
// Get all types referenced by startType
|
|
291
|
+
const refs = this.getReferencedTypes(schema);
|
|
292
|
+
for (const ref of refs) {
|
|
293
|
+
if (ref === targetType) {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
if (canReach(ref, targetType, visited)) {
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return false;
|
|
301
|
+
};
|
|
302
|
+
// Check if typeName can reach itself (directly or transitively)
|
|
303
|
+
const refs = this.getReferencedTypes(defs[typeName]);
|
|
304
|
+
for (const ref of refs) {
|
|
305
|
+
if (canReach(ref, typeName)) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Extracts all type names referenced in a schema.
|
|
313
|
+
*/
|
|
314
|
+
getReferencedTypes(schema) {
|
|
315
|
+
const refs = [];
|
|
316
|
+
const collectRefs = (obj) => {
|
|
317
|
+
if (typeof obj !== "object" || obj === null) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (obj.$ref && typeof obj.$ref === "string") {
|
|
321
|
+
const match = obj.$ref.match(/^#\/\$defs\/(.+)$/);
|
|
322
|
+
if (match) {
|
|
323
|
+
refs.push(match[1]);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
for (const value of Object.values(obj)) {
|
|
327
|
+
if (Array.isArray(value)) {
|
|
328
|
+
value.forEach(collectRefs);
|
|
329
|
+
}
|
|
330
|
+
else if (typeof value === "object" && value !== null) {
|
|
331
|
+
collectRefs(value);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
collectRefs(schema);
|
|
336
|
+
return refs;
|
|
337
|
+
}
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
// Declaration emission
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
emitDeclaration(decl) {
|
|
342
|
+
switch (decl.kind) {
|
|
343
|
+
case "interface": return this.emitInterface(decl);
|
|
344
|
+
case "type_alias": return this.emitTypeAlias(decl);
|
|
345
|
+
case "enum": return this.emitEnum(decl);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
emitInterface(decl) {
|
|
349
|
+
const schema = this.emitObjectType(decl.properties, decl.indexSignature, decl.tags);
|
|
350
|
+
// Handle extends - merge parent properties via allOf
|
|
351
|
+
if (decl.extends && decl.extends.length > 0) {
|
|
352
|
+
const allOf = decl.extends.map((typeNode) => {
|
|
353
|
+
// If it's a simple reference, use $ref
|
|
354
|
+
if (typeNode.kind === "reference" && !typeNode.typeArgs) {
|
|
355
|
+
return { $ref: `#/$defs/${typeNode.name}` };
|
|
356
|
+
}
|
|
357
|
+
// If it's a utility type or complex type, resolve it inline
|
|
358
|
+
return this.emitType(typeNode);
|
|
359
|
+
});
|
|
360
|
+
// Only add the interface's own properties if it has any
|
|
361
|
+
const hasOwnProperties = decl.properties.length > 0 || decl.indexSignature !== undefined;
|
|
362
|
+
if (hasOwnProperties) {
|
|
363
|
+
allOf.push(schema);
|
|
364
|
+
}
|
|
365
|
+
// If only one schema in allOf, unwrap it
|
|
366
|
+
if (allOf.length === 1) {
|
|
367
|
+
const result = allOf[0];
|
|
368
|
+
if (this.options.includeJSDoc && decl.description)
|
|
369
|
+
result.description = decl.description;
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
const result = { allOf };
|
|
373
|
+
if (this.options.includeJSDoc && decl.description)
|
|
374
|
+
result.description = decl.description;
|
|
375
|
+
return result;
|
|
376
|
+
}
|
|
377
|
+
if (this.options.includeJSDoc && decl.description)
|
|
378
|
+
schema.description = decl.description;
|
|
379
|
+
return schema;
|
|
380
|
+
}
|
|
381
|
+
emitTypeAlias(decl) {
|
|
382
|
+
const schema = this.emitType(decl.type);
|
|
383
|
+
if (this.options.includeJSDoc) {
|
|
384
|
+
if (decl.description)
|
|
385
|
+
schema.description = decl.description;
|
|
386
|
+
// Apply @additionalProperties tag if this is an object type
|
|
387
|
+
if (decl.tags?.additionalProperties !== undefined && schema.type === "object") {
|
|
388
|
+
const value = decl.tags.additionalProperties.toLowerCase();
|
|
389
|
+
if (value === "true") {
|
|
390
|
+
schema.additionalProperties = true;
|
|
391
|
+
}
|
|
392
|
+
else if (value === "false") {
|
|
393
|
+
schema.additionalProperties = false;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return schema;
|
|
398
|
+
}
|
|
399
|
+
emitEnum(decl) {
|
|
400
|
+
const schema = {
|
|
401
|
+
enum: decl.members.map(m => m.value),
|
|
402
|
+
};
|
|
403
|
+
if (this.options.includeJSDoc && decl.description)
|
|
404
|
+
schema.description = decl.description;
|
|
405
|
+
// If all values are strings, add type: "string"
|
|
406
|
+
if (decl.members.every(m => typeof m.value === "string")) {
|
|
407
|
+
schema.type = "string";
|
|
408
|
+
}
|
|
409
|
+
else if (decl.members.every(m => typeof m.value === "number")) {
|
|
410
|
+
schema.type = "number";
|
|
411
|
+
}
|
|
412
|
+
return schema;
|
|
413
|
+
}
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
// Type node emission
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
emitType(node) {
|
|
418
|
+
switch (node.kind) {
|
|
419
|
+
case "primitive": return this.emitPrimitive(node.value);
|
|
420
|
+
case "literal_string": return { const: node.value };
|
|
421
|
+
case "literal_number": return { const: node.value };
|
|
422
|
+
case "literal_boolean": return { const: node.value };
|
|
423
|
+
case "object":
|
|
424
|
+
return this.emitObjectType(node.properties, node.indexSignature);
|
|
425
|
+
case "array":
|
|
426
|
+
return { type: "array", items: this.emitType(node.element) };
|
|
427
|
+
case "tuple":
|
|
428
|
+
return this.emitTuple(node);
|
|
429
|
+
case "union":
|
|
430
|
+
return this.emitUnion(node.members);
|
|
431
|
+
case "intersection":
|
|
432
|
+
return this.emitIntersection(node.members);
|
|
433
|
+
case "reference":
|
|
434
|
+
return this.emitReference(node);
|
|
435
|
+
case "parenthesized":
|
|
436
|
+
return this.emitType(node.inner);
|
|
437
|
+
case "record":
|
|
438
|
+
return this.emitRecord(node.keyType, node.valueType);
|
|
439
|
+
case "template_literal":
|
|
440
|
+
return { type: "string" }; // Best we can do without regex generation
|
|
441
|
+
case "mapped":
|
|
442
|
+
return { type: "object" }; // Fallback
|
|
443
|
+
default:
|
|
444
|
+
return {};
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
emitPrimitive(value) {
|
|
448
|
+
switch (value) {
|
|
449
|
+
case "string": return { type: "string" };
|
|
450
|
+
case "number": return { type: "number" };
|
|
451
|
+
case "boolean": return { type: "boolean" };
|
|
452
|
+
case "null": return { type: "null" };
|
|
453
|
+
case "undefined": return {}; // no JSON Schema equivalent
|
|
454
|
+
case "bigint": return { type: "integer" };
|
|
455
|
+
case "any": return {}; // accepts anything
|
|
456
|
+
case "unknown": return {}; // accepts anything
|
|
457
|
+
case "void": return {}; // no value
|
|
458
|
+
case "never": return { not: {} }; // matches nothing
|
|
459
|
+
case "object": return { type: "object" };
|
|
460
|
+
default: return {};
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
emitObjectType(properties, indexSignature, tags) {
|
|
464
|
+
const schema = { type: "object" };
|
|
465
|
+
const props = {};
|
|
466
|
+
const required = [];
|
|
467
|
+
for (const prop of properties) {
|
|
468
|
+
const propSchema = this.emitType(prop.type);
|
|
469
|
+
// Apply JSDoc tags
|
|
470
|
+
if (this.options.includeJSDoc) {
|
|
471
|
+
if (prop.description)
|
|
472
|
+
propSchema.description = prop.description;
|
|
473
|
+
if (prop.tags)
|
|
474
|
+
this.applyJSDocTags(propSchema, prop.tags);
|
|
475
|
+
}
|
|
476
|
+
if (prop.readonly)
|
|
477
|
+
propSchema.readOnly = true;
|
|
478
|
+
props[prop.name] = propSchema;
|
|
479
|
+
if (!prop.optional)
|
|
480
|
+
required.push(prop.name);
|
|
481
|
+
}
|
|
482
|
+
if (Object.keys(props).length > 0) {
|
|
483
|
+
schema.properties = props;
|
|
484
|
+
}
|
|
485
|
+
if (required.length > 0) {
|
|
486
|
+
schema.required = required;
|
|
487
|
+
}
|
|
488
|
+
// Handle additionalProperties in order of precedence:
|
|
489
|
+
// 1. Index signature
|
|
490
|
+
// 2. @additionalProperties JSDoc tag
|
|
491
|
+
// 3. strictObjects option
|
|
492
|
+
// 4. additionalProperties option
|
|
493
|
+
if (indexSignature) {
|
|
494
|
+
schema.additionalProperties = this.emitType(indexSignature.valueType);
|
|
495
|
+
}
|
|
496
|
+
else if (this.options.includeJSDoc && tags?.additionalProperties !== undefined) {
|
|
497
|
+
const value = tags.additionalProperties.toLowerCase();
|
|
498
|
+
if (value === "true") {
|
|
499
|
+
schema.additionalProperties = true;
|
|
500
|
+
}
|
|
501
|
+
else if (value === "false") {
|
|
502
|
+
schema.additionalProperties = false;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
else if (this.options.strictObjects) {
|
|
506
|
+
schema.additionalProperties = false;
|
|
507
|
+
}
|
|
508
|
+
else if (this.options.additionalProperties !== undefined) {
|
|
509
|
+
schema.additionalProperties = this.options.additionalProperties;
|
|
510
|
+
}
|
|
511
|
+
return schema;
|
|
512
|
+
}
|
|
513
|
+
emitTuple(node) {
|
|
514
|
+
const schema = { type: "array" };
|
|
515
|
+
const requiredCount = node.elements.filter(e => !e.optional && !e.rest).length;
|
|
516
|
+
const hasRest = node.elements.some(e => e.rest);
|
|
517
|
+
if (hasRest) {
|
|
518
|
+
// Separate fixed elements and rest element
|
|
519
|
+
const fixed = node.elements.filter(e => !e.rest);
|
|
520
|
+
const rest = node.elements.find(e => e.rest);
|
|
521
|
+
schema.prefixItems = fixed.map(e => this.emitType(e.type));
|
|
522
|
+
schema.minItems = requiredCount;
|
|
523
|
+
if (rest) {
|
|
524
|
+
schema.items = this.emitType(rest.type);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
schema.prefixItems = node.elements.map(e => this.emitType(e.type));
|
|
529
|
+
schema.minItems = requiredCount;
|
|
530
|
+
schema.maxItems = node.elements.length;
|
|
531
|
+
}
|
|
532
|
+
return schema;
|
|
533
|
+
}
|
|
534
|
+
emitUnion(members) {
|
|
535
|
+
// Flatten nested unions
|
|
536
|
+
const flat = this.flattenUnion(members);
|
|
537
|
+
// Check if all members are string/number literals → use enum
|
|
538
|
+
const allStringLiterals = flat.every(m => m.kind === "literal_string");
|
|
539
|
+
if (allStringLiterals) {
|
|
540
|
+
return {
|
|
541
|
+
type: "string",
|
|
542
|
+
enum: flat.map(m => m.value),
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
const allNumberLiterals = flat.every(m => m.kind === "literal_number");
|
|
546
|
+
if (allNumberLiterals) {
|
|
547
|
+
return {
|
|
548
|
+
type: "number",
|
|
549
|
+
enum: flat.map(m => m.value),
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
// Check for nullable: T | null → make T nullable
|
|
553
|
+
const nullIndex = flat.findIndex(m => m.kind === "primitive" && m.value === "null");
|
|
554
|
+
const undefinedIndex = flat.findIndex(m => m.kind === "primitive" && m.value === "undefined");
|
|
555
|
+
const nonNullMembers = flat.filter(m => !(m.kind === "primitive" && (m.value === "null" || m.value === "undefined")));
|
|
556
|
+
if ((nullIndex !== -1 || undefinedIndex !== -1) && nonNullMembers.length === 1) {
|
|
557
|
+
// Simple nullable: string | null
|
|
558
|
+
const schema = this.emitType(nonNullMembers[0]);
|
|
559
|
+
if (typeof schema.type === "string") {
|
|
560
|
+
schema.type = [schema.type, "null"];
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
564
|
+
}
|
|
565
|
+
return schema;
|
|
566
|
+
}
|
|
567
|
+
// General union → anyOf
|
|
568
|
+
const schemas = flat.map(m => this.emitType(m));
|
|
569
|
+
return { anyOf: schemas };
|
|
570
|
+
}
|
|
571
|
+
flattenUnion(members) {
|
|
572
|
+
const result = [];
|
|
573
|
+
for (const m of members) {
|
|
574
|
+
if (m.kind === "union") {
|
|
575
|
+
result.push(...this.flattenUnion(m.members));
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
result.push(m);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return result;
|
|
582
|
+
}
|
|
583
|
+
emitIntersection(members) {
|
|
584
|
+
// If all members are objects, try to merge them
|
|
585
|
+
const schemas = members.map(m => this.emitType(m));
|
|
586
|
+
if (schemas.length === 1)
|
|
587
|
+
return schemas[0];
|
|
588
|
+
return { allOf: schemas };
|
|
589
|
+
}
|
|
590
|
+
emitReference(node) {
|
|
591
|
+
// Handle built-in Date type
|
|
592
|
+
if (node.name === "Date" && !node.typeArgs) {
|
|
593
|
+
return { type: "string", format: "date-time" };
|
|
594
|
+
}
|
|
595
|
+
// Handle well-known utility types
|
|
596
|
+
if (node.typeArgs && node.typeArgs.length > 0) {
|
|
597
|
+
const resolved = this.resolveUtilityType(node.name, node.typeArgs);
|
|
598
|
+
if (resolved)
|
|
599
|
+
return resolved;
|
|
600
|
+
}
|
|
601
|
+
// If the declaration exists and is simple, we could inline it,
|
|
602
|
+
// but using $ref is more correct and handles circular refs
|
|
603
|
+
return { $ref: `#/$defs/${node.name}` };
|
|
604
|
+
}
|
|
605
|
+
emitRecord(keyType, valueType) {
|
|
606
|
+
const schema = { type: "object" };
|
|
607
|
+
// If key is a union of string literals, emit explicit properties
|
|
608
|
+
if (keyType.kind === "union") {
|
|
609
|
+
const allStringLiterals = keyType.members.every(m => m.kind === "literal_string");
|
|
610
|
+
if (allStringLiterals) {
|
|
611
|
+
const valueSchema = this.emitType(valueType);
|
|
612
|
+
schema.properties = {};
|
|
613
|
+
schema.required = [];
|
|
614
|
+
for (const m of keyType.members) {
|
|
615
|
+
const key = m.value;
|
|
616
|
+
schema.properties[key] = { ...valueSchema };
|
|
617
|
+
schema.required.push(key);
|
|
618
|
+
}
|
|
619
|
+
return schema;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (keyType.kind === "literal_string") {
|
|
623
|
+
const valueSchema = this.emitType(valueType);
|
|
624
|
+
schema.properties = { [keyType.value]: valueSchema };
|
|
625
|
+
schema.required = [keyType.value];
|
|
626
|
+
return schema;
|
|
627
|
+
}
|
|
628
|
+
// General Record<string, V>
|
|
629
|
+
schema.additionalProperties = this.emitType(valueType);
|
|
630
|
+
return schema;
|
|
631
|
+
}
|
|
632
|
+
// ---------------------------------------------------------------------------
|
|
633
|
+
// Utility type resolution
|
|
634
|
+
// ---------------------------------------------------------------------------
|
|
635
|
+
resolveUtilityType(name, typeArgs) {
|
|
636
|
+
switch (name) {
|
|
637
|
+
case "Partial":
|
|
638
|
+
return this.resolvePartial(typeArgs[0]);
|
|
639
|
+
case "Required":
|
|
640
|
+
return this.resolveRequired(typeArgs[0]);
|
|
641
|
+
case "Pick":
|
|
642
|
+
if (typeArgs.length === 2)
|
|
643
|
+
return this.resolvePick(typeArgs[0], typeArgs[1]);
|
|
644
|
+
return null;
|
|
645
|
+
case "Omit":
|
|
646
|
+
if (typeArgs.length === 2)
|
|
647
|
+
return this.resolveOmit(typeArgs[0], typeArgs[1]);
|
|
648
|
+
return null;
|
|
649
|
+
case "Readonly":
|
|
650
|
+
return this.emitType(typeArgs[0]); // Schema doesn't enforce readonly
|
|
651
|
+
case "NonNullable":
|
|
652
|
+
return this.emitType(typeArgs[0]); // Already non-null in JSON
|
|
653
|
+
case "Set":
|
|
654
|
+
return { type: "array", items: this.emitType(typeArgs[0]), uniqueItems: true };
|
|
655
|
+
case "Map":
|
|
656
|
+
if (typeArgs.length === 2) {
|
|
657
|
+
return { type: "object", additionalProperties: this.emitType(typeArgs[1]) };
|
|
658
|
+
}
|
|
659
|
+
return null;
|
|
660
|
+
default:
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
resolvePartial(target) {
|
|
665
|
+
// For references, look up the declaration and make all properties optional
|
|
666
|
+
if (target.kind === "reference") {
|
|
667
|
+
const decl = this.declarations.get(target.name);
|
|
668
|
+
if (decl && (decl.kind === "interface" || (decl.kind === "type_alias" && decl.type.kind === "object"))) {
|
|
669
|
+
const props = decl.kind === "interface" ? decl.properties : decl.type.properties;
|
|
670
|
+
const schema = this.emitObjectType(props.map((p) => ({ ...p, optional: true })));
|
|
671
|
+
return schema;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// For inline objects
|
|
675
|
+
if (target.kind === "object") {
|
|
676
|
+
return this.emitObjectType(target.properties.map(p => ({ ...p, optional: true })));
|
|
677
|
+
}
|
|
678
|
+
// Fallback: emit as-is
|
|
679
|
+
return this.emitType(target);
|
|
680
|
+
}
|
|
681
|
+
resolveRequired(target) {
|
|
682
|
+
if (target.kind === "reference") {
|
|
683
|
+
const decl = this.declarations.get(target.name);
|
|
684
|
+
if (decl && (decl.kind === "interface" || (decl.kind === "type_alias" && decl.type.kind === "object"))) {
|
|
685
|
+
const props = decl.kind === "interface" ? decl.properties : decl.type.properties;
|
|
686
|
+
return this.emitObjectType(props.map((p) => ({ ...p, optional: false })));
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (target.kind === "object") {
|
|
690
|
+
return this.emitObjectType(target.properties.map(p => ({ ...p, optional: false })));
|
|
691
|
+
}
|
|
692
|
+
return this.emitType(target);
|
|
693
|
+
}
|
|
694
|
+
resolvePick(target, keys) {
|
|
695
|
+
const keyNames = this.extractKeyNames(keys);
|
|
696
|
+
if (!keyNames)
|
|
697
|
+
return this.emitType(target);
|
|
698
|
+
const props = this.getProperties(target);
|
|
699
|
+
if (!props)
|
|
700
|
+
return this.emitType(target);
|
|
701
|
+
return this.emitObjectType(props.filter(p => keyNames.has(p.name)));
|
|
702
|
+
}
|
|
703
|
+
resolveOmit(target, keys) {
|
|
704
|
+
const keyNames = this.extractKeyNames(keys);
|
|
705
|
+
if (!keyNames)
|
|
706
|
+
return this.emitType(target);
|
|
707
|
+
const props = this.getProperties(target);
|
|
708
|
+
if (!props)
|
|
709
|
+
return this.emitType(target);
|
|
710
|
+
return this.emitObjectType(props.filter(p => !keyNames.has(p.name)));
|
|
711
|
+
}
|
|
712
|
+
extractKeyNames(node) {
|
|
713
|
+
if (node.kind === "literal_string")
|
|
714
|
+
return new Set([node.value]);
|
|
715
|
+
if (node.kind === "union") {
|
|
716
|
+
const names = new Set();
|
|
717
|
+
for (const m of node.members) {
|
|
718
|
+
if (m.kind === "literal_string")
|
|
719
|
+
names.add(m.value);
|
|
720
|
+
else
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
return names;
|
|
724
|
+
}
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
getProperties(node) {
|
|
728
|
+
if (node.kind === "object")
|
|
729
|
+
return node.properties;
|
|
730
|
+
if (node.kind === "reference") {
|
|
731
|
+
const decl = this.declarations.get(node.name);
|
|
732
|
+
if (!decl)
|
|
733
|
+
return null;
|
|
734
|
+
if (decl.kind === "interface")
|
|
735
|
+
return decl.properties;
|
|
736
|
+
if (decl.kind === "type_alias" && decl.type.kind === "object")
|
|
737
|
+
return decl.type.properties;
|
|
738
|
+
}
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
// ---------------------------------------------------------------------------
|
|
742
|
+
// JSDoc tag application
|
|
743
|
+
// ---------------------------------------------------------------------------
|
|
744
|
+
applyJSDocTags(schema, tags) {
|
|
745
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
746
|
+
switch (key) {
|
|
747
|
+
case "minimum":
|
|
748
|
+
schema.minimum = Number(value);
|
|
749
|
+
break;
|
|
750
|
+
case "maximum":
|
|
751
|
+
schema.maximum = Number(value);
|
|
752
|
+
break;
|
|
753
|
+
case "minLength":
|
|
754
|
+
schema.minLength = Number(value);
|
|
755
|
+
break;
|
|
756
|
+
case "maxLength":
|
|
757
|
+
schema.maxLength = Number(value);
|
|
758
|
+
break;
|
|
759
|
+
case "pattern":
|
|
760
|
+
schema.pattern = value;
|
|
761
|
+
break;
|
|
762
|
+
case "format":
|
|
763
|
+
schema.format = value;
|
|
764
|
+
break;
|
|
765
|
+
case "default":
|
|
766
|
+
try {
|
|
767
|
+
schema.default = JSON.parse(value);
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
schema.default = value;
|
|
771
|
+
}
|
|
772
|
+
break;
|
|
773
|
+
case "example":
|
|
774
|
+
case "examples":
|
|
775
|
+
try {
|
|
776
|
+
if (!schema.examples)
|
|
777
|
+
schema.examples = [];
|
|
778
|
+
schema.examples.push(JSON.parse(value));
|
|
779
|
+
}
|
|
780
|
+
catch {
|
|
781
|
+
if (!schema.examples)
|
|
782
|
+
schema.examples = [];
|
|
783
|
+
schema.examples.push(value);
|
|
784
|
+
}
|
|
785
|
+
break;
|
|
786
|
+
case "deprecated":
|
|
787
|
+
schema.deprecated = true;
|
|
788
|
+
break;
|
|
789
|
+
case "title":
|
|
790
|
+
schema.title = value;
|
|
791
|
+
break;
|
|
792
|
+
case "additionalProperties":
|
|
793
|
+
// Only apply to object types
|
|
794
|
+
if (schema.type === "object") {
|
|
795
|
+
const lowerValue = value.toLowerCase();
|
|
796
|
+
if (lowerValue === "true") {
|
|
797
|
+
schema.additionalProperties = true;
|
|
798
|
+
}
|
|
799
|
+
else if (lowerValue === "false") {
|
|
800
|
+
schema.additionalProperties = false;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
//# sourceMappingURL=emitter.js.map
|