@webda/content-mapper 0.0.0-stage → 4.0.0-beta.4

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.
Files changed (115) hide show
  1. package/LICENSE +166 -0
  2. package/lib/coercions.d.ts +22 -0
  3. package/lib/coercions.d.ts.map +1 -0
  4. package/lib/coercions.js +10 -0
  5. package/lib/coercions.js.map +1 -0
  6. package/lib/context.d.ts +56 -0
  7. package/lib/context.d.ts.map +1 -0
  8. package/lib/context.js +111 -0
  9. package/lib/context.js.map +1 -0
  10. package/lib/defaults.d.ts +31 -0
  11. package/lib/defaults.d.ts.map +1 -0
  12. package/lib/defaults.js +33 -0
  13. package/lib/defaults.js.map +1 -0
  14. package/lib/generators/accessors.d.ts +15 -0
  15. package/lib/generators/accessors.d.ts.map +1 -0
  16. package/lib/generators/accessors.js +567 -0
  17. package/lib/generators/accessors.js.map +1 -0
  18. package/lib/generators/behaviors.d.ts +21 -0
  19. package/lib/generators/behaviors.d.ts.map +1 -0
  20. package/lib/generators/behaviors.js +292 -0
  21. package/lib/generators/behaviors.js.map +1 -0
  22. package/lib/generators/imports.d.ts +86 -0
  23. package/lib/generators/imports.d.ts.map +1 -0
  24. package/lib/generators/imports.js +205 -0
  25. package/lib/generators/imports.js.map +1 -0
  26. package/lib/generators/loadparameters.d.ts +7 -0
  27. package/lib/generators/loadparameters.d.ts.map +1 -0
  28. package/lib/generators/loadparameters.js +175 -0
  29. package/lib/generators/loadparameters.js.map +1 -0
  30. package/lib/generators/qlvalidator.d.ts +53 -0
  31. package/lib/generators/qlvalidator.d.ts.map +1 -0
  32. package/lib/generators/qlvalidator.js +238 -0
  33. package/lib/generators/qlvalidator.js.map +1 -0
  34. package/lib/index.d.ts +53 -0
  35. package/lib/index.d.ts.map +1 -0
  36. package/lib/index.js +36 -0
  37. package/lib/index.js.map +1 -0
  38. package/lib/module-discovery.d.ts +211 -0
  39. package/lib/module-discovery.d.ts.map +1 -0
  40. package/lib/module-discovery.js +662 -0
  41. package/lib/module-discovery.js.map +1 -0
  42. package/lib/module.d.ts +213 -0
  43. package/lib/module.d.ts.map +1 -0
  44. package/lib/module.js +1243 -0
  45. package/lib/module.js.map +1 -0
  46. package/lib/plan.d.ts +101 -0
  47. package/lib/plan.d.ts.map +1 -0
  48. package/lib/plan.js +61 -0
  49. package/lib/plan.js.map +1 -0
  50. package/lib/property-order.d.ts +37 -0
  51. package/lib/property-order.d.ts.map +1 -0
  52. package/lib/property-order.js +141 -0
  53. package/lib/property-order.js.map +1 -0
  54. package/lib/schema/action.d.ts +46 -0
  55. package/lib/schema/action.d.ts.map +1 -0
  56. package/lib/schema/action.js +190 -0
  57. package/lib/schema/action.js.map +1 -0
  58. package/lib/schema/annotations.d.ts +63 -0
  59. package/lib/schema/annotations.d.ts.map +1 -0
  60. package/lib/schema/annotations.js +116 -0
  61. package/lib/schema/annotations.js.map +1 -0
  62. package/lib/schema/converter.d.ts +363 -0
  63. package/lib/schema/converter.d.ts.map +1 -0
  64. package/lib/schema/converter.js +1377 -0
  65. package/lib/schema/converter.js.map +1 -0
  66. package/lib/schema/jsdoc.d.ts +34 -0
  67. package/lib/schema/jsdoc.d.ts.map +1 -0
  68. package/lib/schema/jsdoc.js +206 -0
  69. package/lib/schema/jsdoc.js.map +1 -0
  70. package/lib/schema/model.d.ts +27 -0
  71. package/lib/schema/model.d.ts.map +1 -0
  72. package/lib/schema/model.js +86 -0
  73. package/lib/schema/model.js.map +1 -0
  74. package/lib/schema/project.d.ts +34 -0
  75. package/lib/schema/project.d.ts.map +1 -0
  76. package/lib/schema/project.js +305 -0
  77. package/lib/schema/project.js.map +1 -0
  78. package/lib/schema/service.d.ts +62 -0
  79. package/lib/schema/service.d.ts.map +1 -0
  80. package/lib/schema/service.js +130 -0
  81. package/lib/schema/service.js.map +1 -0
  82. package/lib/schema/types.d.ts +61 -0
  83. package/lib/schema/types.d.ts.map +1 -0
  84. package/lib/schema/types.js +36 -0
  85. package/lib/schema/types.js.map +1 -0
  86. package/lib/schema/worker-cli.d.ts +3 -0
  87. package/lib/schema/worker-cli.d.ts.map +1 -0
  88. package/lib/schema/worker-cli.js +7 -0
  89. package/lib/schema/worker-cli.js.map +1 -0
  90. package/lib/schema/worker.d.ts +81 -0
  91. package/lib/schema/worker.d.ts.map +1 -0
  92. package/lib/schema/worker.js +186 -0
  93. package/lib/schema/worker.js.map +1 -0
  94. package/lib/server.d.ts +2 -0
  95. package/lib/server.d.ts.map +1 -0
  96. package/lib/server.js +133 -0
  97. package/lib/server.js.map +1 -0
  98. package/lib/session.d.ts +84 -0
  99. package/lib/session.d.ts.map +1 -0
  100. package/lib/session.js +141 -0
  101. package/lib/session.js.map +1 -0
  102. package/lib/spans.d.ts +46 -0
  103. package/lib/spans.d.ts.map +1 -0
  104. package/lib/spans.js +64 -0
  105. package/lib/spans.js.map +1 -0
  106. package/lib/twopass.d.ts +45 -0
  107. package/lib/twopass.d.ts.map +1 -0
  108. package/lib/twopass.js +87 -0
  109. package/lib/twopass.js.map +1 -0
  110. package/lib/vite.d.ts +20 -0
  111. package/lib/vite.d.ts.map +1 -0
  112. package/lib/vite.js +41 -0
  113. package/lib/vite.js.map +1 -0
  114. package/package.json +89 -3
  115. package/README.md +0 -3
@@ -0,0 +1,1377 @@
1
+ /**
2
+ * A purpose-built TypeScript type -> JSON Schema converter on the 7.1 checker.
3
+ *
4
+ * This is stage 7 of the TypeScript 7 migration. Two alternatives were
5
+ * rejected on evidence (see `docs/contribute/TypeScript 7 Content Mappers.md`,
6
+ * "Stage 7 plan"): porting `@webda/schema`'s 2,200 lines, which target the
7
+ * vega corpus and blacklist fixtures they cannot match; and adopting
8
+ * `ts-json-schema-generator-go`, which today emits no accessor-backed
9
+ * properties at all.
10
+ *
11
+ * What makes a purpose-built converter tractable is how narrow the committed
12
+ * corpus is: 4,154 schema nodes using twelve keywords, no `oneOf`, no
13
+ * conditionals. Webda service parameters and models are ordinary object
14
+ * shapes, not arbitrary TypeScript.
15
+ *
16
+ * **The converter throws on any type it cannot convert.** There is no
17
+ * best-effort path — see {@link SchemaConversionError}.
18
+ *
19
+ * ### Fidelity
20
+ *
21
+ * The target is byte-identical output against the committed
22
+ * `webda.module.json`, verified by `tools/schema-diff.mjs`. That makes this a
23
+ * behavioural reimplementation, not a redesign: where `@webda/schema` does
24
+ * something odd and the oddity reached the committed artefact, it is
25
+ * reproduced and commented rather than corrected. Changing any of it is a
26
+ * separate, visible commit.
27
+ */
28
+ import { IndexKind, ObjectFlags, SignatureKind, SymbolFlags, TypeFlags } from "typescript/unstable/sync";
29
+ import { ModifierFlags, SyntaxKind } from "typescript/unstable/ast";
30
+ import * as is from "typescript/unstable/ast/is";
31
+ import { applyDocs } from "./annotations.js";
32
+ import { SchemaConversionError } from "./types.js";
33
+ /** JSON Schema dialect every generated document declares. */
34
+ const DRAFT_07 = "http://json-schema.org/draft-07/schema#";
35
+ const KEEP = { optional: false, decision: "keep" };
36
+ const SKIP = { optional: false, decision: "skip" };
37
+ /**
38
+ * Converts TypeScript types into JSON Schema draft-07 documents.
39
+ *
40
+ * One instance may serve many conversions; `definitions` are reset per
41
+ * document.
42
+ */
43
+ export class SchemaConverter {
44
+ project;
45
+ checker;
46
+ maxDepth;
47
+ mode;
48
+ booleanDefaultFalse;
49
+ bufferStrategy;
50
+ /** Sub-schemas hoisted out of the current document. */
51
+ definitions = {};
52
+ /** Fallback resolution context when a property has no declaration. */
53
+ targetNode;
54
+ /**
55
+ * @param options - project, checker and conversion settings
56
+ */
57
+ constructor(options) {
58
+ this.project = options.project;
59
+ this.checker = options.checker;
60
+ this.maxDepth = options.maxDepth ?? 10;
61
+ this.mode = options.mode ?? "input";
62
+ this.booleanDefaultFalse = !options.disableBooleanDefaultToFalse;
63
+ this.bufferStrategy = options.bufferStrategy ?? "base64";
64
+ }
65
+ // ---------------------------------------------------------------- entry --
66
+ /**
67
+ * Convert the type at a node into a complete schema document.
68
+ *
69
+ * Mirrors `SchemaGenerator.getSchemaFromNodes` with a single node, which is
70
+ * how `@webda/compiler` always calls it.
71
+ * @param node - a declaration or type node
72
+ * @returns a self-contained draft-07 document
73
+ */
74
+ fromNode(node) {
75
+ const type = this.checker.getTypeAtLocation(node);
76
+ return this.document(type, this.declaredName(node) ?? this.checker.typeToString(type), node);
77
+ }
78
+ /**
79
+ * Convert a type into a complete schema document.
80
+ *
81
+ * Mirrors `SchemaGenerator.getSchemaFromType`.
82
+ * @param type - the type to convert
83
+ * @param contextNode - resolution context for properties without declarations
84
+ * @returns a self-contained draft-07 document
85
+ */
86
+ fromType(type, contextNode) {
87
+ return this.document(type, this.checker.typeToString(type), contextNode);
88
+ }
89
+ /**
90
+ * Build one document: convert, hoist, inline the root, sort.
91
+ * @param type - the type to convert
92
+ * @param typeName - key the root definition is registered under
93
+ * @param node - resolution context
94
+ * @returns the finished document
95
+ */
96
+ document(type, typeName, node) {
97
+ this.definitions = {};
98
+ this.targetNode = node;
99
+ const root = {};
100
+ this.property(type, root, "/", node, 0);
101
+ const result = { $schema: DRAFT_07, $ref: `#/definitions/${encodeURIComponent(typeName)}` };
102
+ result.definitions = { ...this.definitions };
103
+ result.definitions[typeName] ??= root;
104
+ this.inlineRootRef(result);
105
+ return sortKeys(result);
106
+ }
107
+ /**
108
+ * Follow the root `$ref` into the document body.
109
+ *
110
+ * A definition can itself be a bare `$ref` — an anonymous object registered
111
+ * under a union-branch path, for instance — so the chain is followed to the
112
+ * end and every consumed definition removed.
113
+ * @param result - the document to flatten, mutated in place
114
+ */
115
+ inlineRootRef(result) {
116
+ while (result.$ref && result.definitions) {
117
+ const key = decodeURIComponent(result.$ref.replace("#/definitions/", ""));
118
+ const target = result.definitions[key];
119
+ if (!target)
120
+ break;
121
+ delete result.$ref;
122
+ delete result.definitions[key];
123
+ Object.assign(result, target);
124
+ }
125
+ if (result.definitions && Object.keys(result.definitions).length === 0)
126
+ delete result.definitions;
127
+ }
128
+ // ------------------------------------------------------------ dispatch --
129
+ /**
130
+ * Convert one type into `definition`, in place.
131
+ *
132
+ * The branch order matters and is taken from `@webda/schema`: literals
133
+ * before their widened forms, `Date` and `RegExp` before the generic
134
+ * class/interface handling, arrays after unions. The final `else` throws.
135
+ * @param type - the type to convert
136
+ * @param definition - schema to populate
137
+ * @param path - schema path, used for definition keys and error messages
138
+ * @param node - declaration the type came from, when there is one
139
+ * @param depth - recursion depth
140
+ * @returns whether the parent should keep the property, and its optionality
141
+ */
142
+ property(type, definition, path, node, depth) {
143
+ if (depth > this.maxDepth)
144
+ return KEEP;
145
+ const typeName = this.checker.typeToString(type);
146
+ // JSDoc from three sources, nearest last so it wins: the type's own
147
+ // symbol, the alias it was reached through, and the declaration node.
148
+ applyDocs(definition, type.getSymbol(), this.checker, this.project);
149
+ const aliasSymbol = type.getAliasSymbol();
150
+ if (aliasSymbol)
151
+ applyDocs(definition, aliasSymbol, this.checker, this.project);
152
+ if (node)
153
+ applyDocs(definition, this.checker.getSymbolAtLocation(node), this.checker, this.project);
154
+ this.inheritAliasDocs(definition, node);
155
+ // Checked before the shape dispatch so it also covers array-typed
156
+ // attributes whose element class carries the marker — `Binaries<T>`
157
+ // extends `Array`, so the class branch below never sees it.
158
+ if (path !== "/" && this.mode === "dto-in") {
159
+ const context = node ?? this.targetNode;
160
+ if (this.hasReadOnlyTag(type))
161
+ return SKIP;
162
+ if (context && this.resolveFromDto(type, context) === "skip")
163
+ return SKIP;
164
+ }
165
+ const flags = type.flags;
166
+ if (flags & TypeFlags.StringLiteral) {
167
+ definition.type = "string";
168
+ definition.const = type.value;
169
+ return KEEP;
170
+ }
171
+ if (flags & TypeFlags.String) {
172
+ definition.type = "string";
173
+ const literal = literalOf(this.initializerFor(type, node));
174
+ if (literal?.type === "string")
175
+ definition.default = literal.const;
176
+ return KEEP;
177
+ }
178
+ // A template literal (`` `pre-${string}` ``) or a string mapping
179
+ // (`Uppercase<T>`) constrains a string in a way draft-07 cannot express
180
+ // without deriving a regex. `type: "string"` would accept values the type
181
+ // rejects, and `isArrayLike` below would otherwise class them as arrays —
182
+ // `string` carries a numeric index signature — which is worse still.
183
+ if (flags & (TypeFlags.TemplateLiteral | TypeFlags.StringMapping)) {
184
+ throw new SchemaConversionError("a template literal or mapped string type has no JSON Schema form; widen it to `string` or add a `@pattern`", path, typeName);
185
+ }
186
+ if (flags & TypeFlags.NumberLiteral) {
187
+ definition.type = "number";
188
+ definition.const = type.value;
189
+ return KEEP;
190
+ }
191
+ if (flags & TypeFlags.BigIntLiteral) {
192
+ definition.type = "number";
193
+ return KEEP;
194
+ }
195
+ // `Function` is matched by name because the checker models it as an
196
+ // interface, so the class/interface branch would otherwise emit an object.
197
+ if (typeName === "Function")
198
+ return SKIP;
199
+ if (flags & TypeFlags.NumberLike) {
200
+ definition.type = "number";
201
+ return KEEP;
202
+ }
203
+ if (flags & TypeFlags.BooleanLiteral) {
204
+ definition.type = "boolean";
205
+ // 7.1 carries the value on the literal type; 6.x only exposed
206
+ // `intrinsicName`, which is empty here and would read as `false`.
207
+ definition.const = type.value === true;
208
+ return KEEP;
209
+ }
210
+ if (flags & TypeFlags.BooleanLike) {
211
+ definition.type = "boolean";
212
+ return KEEP;
213
+ }
214
+ if (flags & TypeFlags.BigIntLike) {
215
+ definition.type = "integer";
216
+ return KEEP;
217
+ }
218
+ if (flags & TypeFlags.Null) {
219
+ definition.type = "null";
220
+ return KEEP;
221
+ }
222
+ if (flags & (TypeFlags.Undefined | TypeFlags.Void)) {
223
+ definition.not = {};
224
+ return { optional: true, decision: "keep" };
225
+ }
226
+ if (flags & TypeFlags.Never) {
227
+ definition.not = {};
228
+ return KEEP;
229
+ }
230
+ if (this.isBuffer(type, typeName)) {
231
+ this.applyBuffer(definition);
232
+ return KEEP;
233
+ }
234
+ if (typeName === "RegExp" || type.getSymbol()?.name === "RegExp") {
235
+ // draft-07 has no regex-object format; `format: "regex"` is the
236
+ // convention downstream consumers already read.
237
+ definition.type = "string";
238
+ definition.format = "regex";
239
+ return KEEP;
240
+ }
241
+ if (typeName === "Date" || type.getSymbol()?.name === "Date") {
242
+ definition.type = "string";
243
+ // lib.es5.d.ts documents the `Date` interface itself; that text is not a
244
+ // description of the property, so it is dropped again.
245
+ applyDocs(definition, type.getSymbol(), this.checker, this.project);
246
+ delete definition.description;
247
+ definition.format ??= "date-time";
248
+ return KEEP;
249
+ }
250
+ if (flags & (TypeFlags.Any | TypeFlags.Unknown)) {
251
+ // Deliberately unconstrained: `any` permits anything, and saying so is
252
+ // accurate rather than best-effort.
253
+ return KEEP;
254
+ }
255
+ if (type.isClassOrInterface())
256
+ return this.objectLike(type, definition, path, typeName, node, depth);
257
+ if (type.isUnionType())
258
+ return this.union(type, definition, path, node, depth);
259
+ if (type.isIntersectionType())
260
+ return this.intersection(type, definition, path, depth);
261
+ if (this.isArrayLike(type))
262
+ return this.array(type, definition, path, depth);
263
+ if (type.isTypeParameter()) {
264
+ // An unresolved type parameter constrains nothing at this point.
265
+ return KEEP;
266
+ }
267
+ const objectFlags = objectFlagsOf(type);
268
+ if (objectFlags & ObjectFlags.Reference)
269
+ return this.objectLike(type, definition, path, typeName, node, depth);
270
+ if (objectFlags & ObjectFlags.Anonymous) {
271
+ if (type.getCallSignatures().length > 0) {
272
+ // A callable is not serialisable. At the root the caller asked for a
273
+ // function's schema, which service parameters never do.
274
+ if (depth > 0)
275
+ return SKIP;
276
+ throw new SchemaConversionError("cannot convert a callable type at the schema root", path, typeName);
277
+ }
278
+ return this.objectLike(type, definition, path, typeName, node, depth);
279
+ }
280
+ if (objectFlags && (type.getSymbol()?.flags ?? 0) & SymbolFlags.TypeLiteral) {
281
+ return this.objectLike(type, definition, path, typeName, node, depth);
282
+ }
283
+ if (flags & TypeFlags.NonPrimitive) {
284
+ definition.type = "object";
285
+ return KEEP;
286
+ }
287
+ if (flags & (TypeFlags.ESSymbol | TypeFlags.UniqueESSymbol)) {
288
+ // Symbols have no JSON representation, so the property cannot exist in
289
+ // the schema at all.
290
+ return SKIP;
291
+ }
292
+ const apparent = this.checker.getApparentType(type);
293
+ if (apparent.id !== type.id)
294
+ return this.property(apparent, definition, path, node, depth + 1);
295
+ throw new SchemaConversionError("no JSON Schema representation for this type", path, typeName);
296
+ }
297
+ /**
298
+ * Merge annotations from a type alias tagged `@schemaInherits`.
299
+ *
300
+ * `type Email = string` with `@format email` on the alias only reaches the
301
+ * property when the alias opts in, otherwise every use of a documented alias
302
+ * would inherit its prose.
303
+ * @param definition - schema to enrich
304
+ * @param node - the property declaration
305
+ */
306
+ inheritAliasDocs(definition, node) {
307
+ if (!node || !is.isPropertySignatureDeclaration(node))
308
+ return;
309
+ const typeNode = node.type;
310
+ if (!typeNode || !is.isTypeReferenceNode(typeNode))
311
+ return;
312
+ const aliasSymbol = this.checker.getSymbolAtLocation(typeNode.typeName);
313
+ if (!aliasSymbol)
314
+ return;
315
+ const inherited = {};
316
+ applyDocs(inherited, aliasSymbol, this.checker, this.project);
317
+ if (!inherited.schemaInherits)
318
+ return;
319
+ for (const [key, value] of Object.entries(inherited)) {
320
+ if (key === "schemaInherits")
321
+ continue;
322
+ definition[key] ??= value;
323
+ }
324
+ }
325
+ // -------------------------------------------------------------- objects --
326
+ /**
327
+ * Convert a class, interface, anonymous object or generic reference.
328
+ *
329
+ * The shape is registered in `definitions` and the caller's slot replaced
330
+ * with a `$ref`, except at the document root where an anonymous object is
331
+ * inlined instead — otherwise the root would sit behind a definition keyed
332
+ * by its own stringified shape.
333
+ * @param type - the object-like type
334
+ * @param definition - schema slot to populate
335
+ * @param path - schema path
336
+ * @param typeName - stringified type, used as the definition key
337
+ * @param node - declaration context
338
+ * @param depth - recursion depth
339
+ * @returns keep or skip
340
+ */
341
+ objectLike(type, definition, path, typeName, node, depth) {
342
+ // A nested class states its own serialised form, and that beats its
343
+ // structural shape. Which method is consulted depends on the direction
344
+ // the schema describes.
345
+ if (path !== "/") {
346
+ const context = node ?? this.targetNode;
347
+ if (this.mode === "dto-in" && context) {
348
+ // A class tagged `@readOnly`, or one whose `fromDto` takes `never`,
349
+ // is never user-supplied — the attribute leaves the Input schema.
350
+ if (this.hasReadOnlyTag(type))
351
+ return SKIP;
352
+ const accepted = this.resolveFromDto(type, context);
353
+ if (accepted === "skip")
354
+ return SKIP;
355
+ if (accepted) {
356
+ this.property(accepted, definition, path, node, depth + 1);
357
+ return KEEP;
358
+ }
359
+ }
360
+ if (this.mode === "dto-out" && context) {
361
+ const produced = this.methodReturnType(type, ["toDTO", "toDto"], context);
362
+ if (produced) {
363
+ if (produced.flags & (TypeFlags.Void | TypeFlags.Undefined | TypeFlags.Null | TypeFlags.Never))
364
+ return SKIP;
365
+ this.property(this.simplifyIntersection(produced), definition, path, node, depth + 1);
366
+ return KEEP;
367
+ }
368
+ }
369
+ const serialized = context ? this.methodReturnType(type, ["toJSON"], context) : undefined;
370
+ if (serialized) {
371
+ const flags = serialized.flags;
372
+ if (flags & (TypeFlags.Void | TypeFlags.Undefined | TypeFlags.Null | TypeFlags.Never))
373
+ return SKIP;
374
+ const returned = this.checker.typeToString(serialized);
375
+ // A `toJSON(): this` says nothing new; only a concrete return type is
376
+ // worth substituting for the class shape.
377
+ if (returned !== typeName && !(flags & TypeFlags.Any)) {
378
+ this.property(this.simplifyIntersection(serialized), definition, path, node, depth + 1);
379
+ return KEEP;
380
+ }
381
+ }
382
+ }
383
+ // For alias-based utility types (`Partial<T>`, `Pick<T, K>`) the apparent
384
+ // type is the one with enumerable properties.
385
+ const structural = this.structuralType(type);
386
+ const shape = this.objectShape(structural, path);
387
+ // Annotations already collected outranks the structural keywords.
388
+ const merged = { ...shape, ...definition };
389
+ if (path === "/" && (typeName.trim().startsWith("{") || isAnonymous(structural))) {
390
+ for (const key of Object.keys(definition))
391
+ delete definition[key];
392
+ Object.assign(definition, merged);
393
+ return KEEP;
394
+ }
395
+ this.registerDefinition(typeName, merged, definition, path);
396
+ return KEEP;
397
+ }
398
+ /**
399
+ * Prefer the apparent type when a type alias hides the concrete shape.
400
+ * @param type - the declared type
401
+ * @returns the type whose properties should be enumerated
402
+ */
403
+ structuralType(type) {
404
+ if (!type.getAliasSymbol() && type.getAliasTypeArguments().length === 0)
405
+ return type;
406
+ const apparent = this.checker.getApparentType(type);
407
+ if (this.checker.typeToString(apparent) === this.checker.typeToString(type))
408
+ return type;
409
+ if (this.checker.getPropertiesOfType(apparent).length < this.checker.getPropertiesOfType(type).length)
410
+ return type;
411
+ return apparent;
412
+ }
413
+ /**
414
+ * Build the `type` / `properties` / `required` / `additionalProperties`
415
+ * portion of an object schema.
416
+ *
417
+ * Properties are read off the **type**, not the declaration's own members,
418
+ * so inherited attributes are included.
419
+ * @param type - the object type
420
+ * @param path - schema path of the object
421
+ * @returns the object schema
422
+ */
423
+ objectShape(type, path) {
424
+ const schema = { type: "object" };
425
+ const depth = (path.match(/\//g) ?? []).length;
426
+ const indexType = this.checker.getIndexTypeOfType(type, IndexKind.String);
427
+ if (indexType) {
428
+ const indexSchema = {};
429
+ this.property(indexType, indexSchema, joinPath(path, "[key: string]"), undefined, depth + 1);
430
+ schema.additionalProperties = indexSchema;
431
+ }
432
+ else {
433
+ schema.additionalProperties = false;
434
+ }
435
+ for (const prop of this.checker.getPropertiesOfType(type)) {
436
+ const declaration = this.declarationOf(prop);
437
+ if (this.isSymbolKeyed(prop, declaration))
438
+ continue;
439
+ const location = declaration ?? this.targetNode;
440
+ if (!location)
441
+ continue;
442
+ const modifiers = declaration ? modifierFlagsOf(declaration) : 0;
443
+ if (modifiers & (ModifierFlags.Private | ModifierFlags.Protected))
444
+ continue;
445
+ if (declaration && isMethodLike(declaration))
446
+ continue;
447
+ const propertyType = this.readableType(prop, declaration, location);
448
+ if (!propertyType)
449
+ continue;
450
+ const propSchema = {};
451
+ const propPath = joinPath(path, prop.name);
452
+ const outcome = this.property(propertyType, propSchema, propPath, location, depth + 1);
453
+ applyDocs(propSchema, prop, this.checker, this.project);
454
+ if (outcome.decision === "skip" || propSchema.SchemaIgnore === true)
455
+ continue;
456
+ // `@readOnly` on the property is equivalent to the modifier — and so is
457
+ // `@readonly`, which is the spelling most of the corpus actually uses.
458
+ // Only the first is a JSON Schema keyword; the second arrives through
459
+ // the generic tag handling, so both have to be checked.
460
+ const readonly = (modifiers & ModifierFlags.Readonly) !== 0 || propSchema.readOnly === true || propSchema.readonly === true;
461
+ if (readonly && this.isInputMode())
462
+ continue;
463
+ let optional = outcome.optional;
464
+ if (hasQuestionToken(declaration) || writesUndefined(declaration) || propSchema.SchemaOptional) {
465
+ optional = true;
466
+ }
467
+ // `@param` documents a callback's arguments, not the property, and is
468
+ // not a JSON Schema keyword. It reaches the schema through the generic
469
+ // tag handling and is dropped here, as `@webda/schema` does.
470
+ delete propSchema.param;
471
+ schema.properties ??= {};
472
+ schema.properties[prop.name] = propSchema;
473
+ const initializer = declaration && !is.isPropertyAssignment(declaration) ? initializerOf(declaration) : undefined;
474
+ if (initializer) {
475
+ // A field with an initializer need not be supplied.
476
+ optional = true;
477
+ const literal = literalOf(initializer);
478
+ if (literal)
479
+ propSchema.default = literal.const;
480
+ }
481
+ if (propSchema.type === "boolean" && this.booleanDefaultFalse && propSchema.default === undefined) {
482
+ propSchema.default = false;
483
+ }
484
+ const declaredOptional = (prop.flags & SymbolFlags.Optional) !== 0;
485
+ if (!optional && !declaredOptional && propSchema.default === undefined) {
486
+ schema.required ??= [];
487
+ schema.required.push(prop.name);
488
+ }
489
+ }
490
+ schema.required?.sort();
491
+ return schema;
492
+ }
493
+ /**
494
+ * The type a property contributes to this schema, honouring accessors.
495
+ *
496
+ * For an input schema a getter-only property cannot be supplied at all; when
497
+ * a setter exists its parameter type is the accepted one, which is what
498
+ * makes the asymmetric accessors the content mapper generates meaningful.
499
+ * @param prop - the property symbol
500
+ * @param declaration - its primary declaration
501
+ * @param location - resolution context
502
+ * @returns the type to convert, or undefined to drop the property
503
+ */
504
+ readableType(prop, declaration, location) {
505
+ if (declaration && is.isGetAccessorDeclaration(declaration) && this.isInputMode()) {
506
+ const setter = prop.declarations
507
+ .map(handle => handle.resolve(this.project))
508
+ .find(node => node && is.isSetAccessorDeclaration(node));
509
+ if (!setter)
510
+ return undefined;
511
+ const parameter = setter.parameters[0];
512
+ if (!parameter)
513
+ return undefined;
514
+ return this.checker.getTypeAtLocation(parameter);
515
+ }
516
+ return this.checker.getTypeOfSymbolAtLocation(prop, location);
517
+ }
518
+ /**
519
+ * Register a schema under a definition key and rewrite the slot as a `$ref`.
520
+ *
521
+ * Inline object types stringify to their whole shape, which makes a useless
522
+ * key, so those fall back to the schema path.
523
+ * @param rawKey - stringified type
524
+ * @param source - the schema to store
525
+ * @param slot - the caller's schema object, emptied and replaced by a `$ref`
526
+ * @param path - schema path, used when the key is unusable
527
+ */
528
+ registerDefinition(rawKey, source, slot, path) {
529
+ const key = rawKey.startsWith("{") || rawKey.endsWith('">') ? path.substring(1).replace(/\//g, "$") : rawKey;
530
+ this.definitions[key] = { ...source };
531
+ for (const existing of Object.keys(slot))
532
+ delete slot[existing];
533
+ slot.$ref = `#/definitions/${encodeURIComponent(key)}`;
534
+ }
535
+ // ------------------------------------------------------------ dto modes --
536
+ /**
537
+ * The return type of the first of `methodNames` the type declares.
538
+ *
539
+ * The apparent type is consulted first so a generic instantiation carries
540
+ * its type arguments into the signature.
541
+ * @param type - the type to inspect
542
+ * @param methodNames - names to try, in order
543
+ * @param location - resolution context
544
+ * @returns the return type, or undefined when no method matches
545
+ */
546
+ methodReturnType(type, methodNames, location) {
547
+ const apparent = this.checker.getApparentType(type);
548
+ const parentArguments = apparent.isTypeReference() ? this.checker.getTypeArguments(apparent) : [];
549
+ for (const name of methodNames) {
550
+ const symbol = apparent.getProperty(name) ?? type.getProperty(name);
551
+ if (!symbol)
552
+ continue;
553
+ const methodType = this.checker.getTypeOfPropertyOfType(apparent, name) ??
554
+ this.checker.getTypeOfSymbolAtLocation(symbol, location);
555
+ const signatures = this.checker.getSignaturesOfType(methodType, SignatureKind.Call);
556
+ if (signatures.length === 0)
557
+ continue;
558
+ const returned = this.checker.getReturnTypeOfSignature(signatures[0]);
559
+ // Depending on traversal order the method type can keep the parent's
560
+ // own type parameter (`BinaryFileInfo<T>`) rather than the substituted
561
+ // argument. There is no usable return type then, so fall back to the
562
+ // parent's structural shape instead of emitting a schema for `T`.
563
+ if (parentArguments.length > 0 && this.mentionsTypeParameter(returned))
564
+ return undefined;
565
+ return returned;
566
+ }
567
+ return undefined;
568
+ }
569
+ /**
570
+ * Whether a type still contains an unsubstituted type parameter.
571
+ * @param type - the type to inspect
572
+ * @param seen - types already visited, guarding cycles
573
+ * @returns true when a type parameter is reachable
574
+ */
575
+ mentionsTypeParameter(type, seen = new Set()) {
576
+ if (seen.has(type.id))
577
+ return false;
578
+ seen.add(type.id);
579
+ if (type.flags & TypeFlags.TypeParameter)
580
+ return true;
581
+ if (type.isTypeReference()) {
582
+ if (this.checker.getTypeArguments(type).some(argument => this.mentionsTypeParameter(argument, seen)))
583
+ return true;
584
+ }
585
+ if (type.flags & (TypeFlags.Union | TypeFlags.Intersection)) {
586
+ return type.getTypes().some(member => this.mentionsTypeParameter(member, seen));
587
+ }
588
+ return false;
589
+ }
590
+ /**
591
+ * The value a class accepts through `fromDto`, for the Input schema.
592
+ *
593
+ * Static and instance declarations both count, because `fromDto` is
594
+ * normally static. A first parameter of `never` is the explicit way to say
595
+ * "this attribute is never supplied".
596
+ * @param type - the type to inspect
597
+ * @param location - resolution context
598
+ * @returns the accepted type, `"skip"`, or undefined when absent
599
+ */
600
+ resolveFromDto(type, location) {
601
+ for (const name of ["fromDTO", "fromDto"]) {
602
+ let symbol = type.getProperty(name);
603
+ if (!symbol) {
604
+ const classSymbol = type.getSymbol();
605
+ const declaration = this.declarationOf(classSymbol);
606
+ if (classSymbol && declaration) {
607
+ symbol = this.checker.getTypeOfSymbolAtLocation(classSymbol, declaration).getProperty(name);
608
+ }
609
+ }
610
+ if (!symbol)
611
+ continue;
612
+ const methodType = this.checker.getTypeOfSymbolAtLocation(symbol, location);
613
+ const signatures = this.checker.getSignaturesOfType(methodType, SignatureKind.Call);
614
+ if (signatures.length === 0)
615
+ continue;
616
+ const parameters = signatures[0].getParameters();
617
+ if (parameters.length === 0)
618
+ continue;
619
+ const parameterDeclaration = this.declarationOf(parameters[0]);
620
+ const parameterType = parameterDeclaration
621
+ ? this.checker.getTypeAtLocation(parameterDeclaration)
622
+ : this.checker.getParameterType(signatures[0], 0);
623
+ return parameterType.flags & TypeFlags.Never ? "skip" : parameterType;
624
+ }
625
+ return undefined;
626
+ }
627
+ /**
628
+ * Whether a class is tagged `@readOnly`, opting its attributes out of Input.
629
+ * @param type - the type to inspect
630
+ * @returns true when the tag is present
631
+ */
632
+ hasReadOnlyTag(type) {
633
+ for (const symbol of [type.getSymbol(), type.getAliasSymbol()]) {
634
+ if (!symbol)
635
+ continue;
636
+ if (symbol.getJsDocTags(this.checker).some(tag => tag.name === "readOnly"))
637
+ return true;
638
+ }
639
+ return false;
640
+ }
641
+ /**
642
+ * Reduce an intersection to the constituent that carries the data.
643
+ *
644
+ * `string & { __brand: x }` is a string, and `T & { toString(): string }`
645
+ * is `T` — an `allOf` of the parts would be technically true and useless.
646
+ * @param type - the type to simplify
647
+ * @returns the simplified type, or the original
648
+ */
649
+ simplifyIntersection(type) {
650
+ if (!type.isIntersectionType())
651
+ return type;
652
+ const members = type.getTypes();
653
+ const primitives = TypeFlags.String |
654
+ TypeFlags.Number |
655
+ TypeFlags.Boolean |
656
+ TypeFlags.StringLiteral |
657
+ TypeFlags.NumberLiteral |
658
+ TypeFlags.BooleanLiteral;
659
+ const primitive = members.find(member => member.flags & primitives);
660
+ if (primitive)
661
+ return primitive;
662
+ const withData = members.filter(member => this.checker.getPropertiesOfType(member).some(property => {
663
+ const declaration = this.declarationOf(property);
664
+ return !declaration || !isMethodLike(declaration);
665
+ }));
666
+ return withData.length === 1 ? withData[0] : type;
667
+ }
668
+ // --------------------------------------------------------------- unions --
669
+ /**
670
+ * Union constituents in the order `webda.module.json` records them.
671
+ *
672
+ * `UnionType.getTypes()` returns them alphabetically, so without this every
673
+ * `enum` and every `type: [..]` in the corpus is reordered.
674
+ *
675
+ * TypeScript 6 exposed a union's constituents sorted by type id, and ids
676
+ * are issued as types are created. That has two regimes, and reproducing
677
+ * both is what makes this correct rather than lucky:
678
+ *
679
+ * - **Primitives** are interned when the checker starts, in a fixed order,
680
+ * so they always sort ahead of everything else and among themselves by
681
+ * that order. `number | string` is recorded as `["string", "number"]`.
682
+ * - **Literals** are interned on first use, so their ids follow the source.
683
+ *
684
+ * Sorting on 7.1's ids reproduces neither reliably: they are assigned as
685
+ * the session happens to query, so `"SUCCESS" | "ERROR"` came back reversed
686
+ * whenever something earlier in the batch had already interned `"ERROR"` —
687
+ * the score depended on which requests shared a worker. Primitives are
688
+ * ranked from the table instead, and literals from the syntax.
689
+ * @param type - the union type
690
+ * @param node - the declaration the union was reached through
691
+ * @returns the constituents, in the recorded order
692
+ */
693
+ unionOrder(type, node) {
694
+ const members = [...type.getTypes()];
695
+ const declared = this.declaredUnionOrder(type, node);
696
+ const idRank = new Map([...members].sort((left, right) => left.id - right.id).map((member, index) => [member.id, index]));
697
+ const rankOf = (member) => {
698
+ const primitive = primitiveRank(member);
699
+ if (primitive !== undefined)
700
+ return primitive;
701
+ const position = declared?.indexOf(member.id) ?? -1;
702
+ // Anything the syntax does not mention — a synthesised constituent with
703
+ // no declaration order to recover — falls back to id order, after
704
+ // everything it does mention.
705
+ return PRIMITIVE_RANKS + (position >= 0 ? position : PRIMITIVE_RANKS + idRank.get(member.id));
706
+ };
707
+ return members
708
+ .map((member, index) => ({ member, index }))
709
+ .sort((left, right) => rankOf(left.member) - rankOf(right.member) || left.index - right.index)
710
+ .map(entry => entry.member);
711
+ }
712
+ /**
713
+ * Type ids of a union's constituents, in the order the syntax lists them.
714
+ * @param type - the union type
715
+ * @param node - the declaration the union was reached through
716
+ * @returns the ids, or undefined when there is no syntax to read
717
+ */
718
+ declaredUnionOrder(type, node) {
719
+ const enumDeclaration = this.declarationOf(type.getSymbol());
720
+ if (enumDeclaration && is.isEnumDeclaration(enumDeclaration)) {
721
+ return enumDeclaration.members.map(member => this.checker.getTypeAtLocation(member).id);
722
+ }
723
+ const typeNode = this.unionTypeNode(type, node);
724
+ if (!typeNode)
725
+ return undefined;
726
+ const ids = [];
727
+ const collect = (union) => {
728
+ for (const member of union.types) {
729
+ const resolved = this.checker.getTypeFromTypeNode(member);
730
+ // A member may itself be a union — TypeScript flattens those, so the
731
+ // nested constituents have to be expanded to line up with `getTypes`.
732
+ if (resolved.isUnionType()) {
733
+ for (const nested of this.unionOrder(resolved, undefined))
734
+ ids.push(nested.id);
735
+ }
736
+ else {
737
+ ids.push(resolved.id);
738
+ }
739
+ }
740
+ };
741
+ collect(typeNode);
742
+ return ids;
743
+ }
744
+ /**
745
+ * The `UnionTypeNode` a union type was written as, if there is one.
746
+ * @param type - the union type
747
+ * @param node - the declaration the union was reached through
748
+ * @returns the syntax node
749
+ */
750
+ unionTypeNode(type, node) {
751
+ const aliasDeclaration = this.declarationOf(type.getAliasSymbol());
752
+ if (aliasDeclaration && is.isTypeAliasDeclaration(aliasDeclaration) && is.isUnionTypeNode(aliasDeclaration.type)) {
753
+ return aliasDeclaration.type;
754
+ }
755
+ const declared = node && node.type;
756
+ if (declared && is.isUnionTypeNode(declared))
757
+ return declared;
758
+ return undefined;
759
+ }
760
+ /**
761
+ * Convert a union into `anyOf`, then simplify.
762
+ *
763
+ * The simplifications are what turn most unions in the corpus into the
764
+ * `enum` and `type: [..]` forms that appear in `webda.module.json`, so they
765
+ * are part of the output contract rather than cosmetic.
766
+ * @param type - the union type
767
+ * @param definition - schema to populate
768
+ * @param path - schema path
769
+ * @param node - the declaration the union was reached through
770
+ * @param depth - recursion depth
771
+ * @returns keep, and whether `undefined` was a member
772
+ */
773
+ union(type, definition, path, node, depth) {
774
+ const branches = [];
775
+ let optional = false;
776
+ const unionDeclaration = this.declarationOf(type.getSymbol());
777
+ const isEnum = unionDeclaration ? is.isEnumDeclaration(unionDeclaration) : false;
778
+ // -1 before the first member, -2 once a non-numeric member breaks the run.
779
+ let lastValue = -1;
780
+ let index = 0;
781
+ for (const member of this.unionOrder(type, node)) {
782
+ index++;
783
+ if (member.flags & TypeFlags.Undefined) {
784
+ optional = true;
785
+ continue;
786
+ }
787
+ const branch = {};
788
+ const memberDeclaration = this.declarationOf(member.getSymbol()) ?? this.targetNode;
789
+ const outcome = this.property(member, branch, `${path}#${index}`, memberDeclaration, depth + 1);
790
+ if (outcome.decision === "skip")
791
+ continue;
792
+ const initializer = memberDeclaration ? initializerOf(memberDeclaration) : undefined;
793
+ if (initializer) {
794
+ const constant = this.checker.getConstantValue(memberDeclaration);
795
+ if (constant !== undefined) {
796
+ branch.const = constant;
797
+ if (isEnum)
798
+ lastValue = typeof constant === "number" ? constant : -2;
799
+ }
800
+ else {
801
+ if (isEnum)
802
+ lastValue = -2;
803
+ const literal = literalOf(initializer);
804
+ if (literal) {
805
+ branch.const = literal.const;
806
+ branch.type = literal.type;
807
+ }
808
+ }
809
+ }
810
+ else if (isEnum && lastValue !== -2) {
811
+ // Implicit enum members continue the numeric run.
812
+ lastValue += 1;
813
+ branch.const = lastValue;
814
+ }
815
+ branches.push(branch);
816
+ }
817
+ definition.anyOf = dedupe(branches);
818
+ for (const branch of definition.anyOf) {
819
+ if (branch.type === "null")
820
+ branch.const = null;
821
+ }
822
+ this.simplifyUnion(definition);
823
+ return { optional, decision: "keep" };
824
+ }
825
+ /**
826
+ * Collapse an `anyOf` into the narrowest equivalent form.
827
+ * @param definition - schema carrying `anyOf`, mutated in place
828
+ */
829
+ simplifyUnion(definition) {
830
+ let branches = definition.anyOf;
831
+ if (branches.length === 0) {
832
+ delete definition.anyOf;
833
+ return;
834
+ }
835
+ if (branches.length === 1) {
836
+ delete definition.anyOf;
837
+ Object.assign(definition, branches[0]);
838
+ return;
839
+ }
840
+ // All constants: an `enum` says the same thing and is what consumers read.
841
+ if (branches.every(branch => branch.const !== undefined)) {
842
+ delete definition.anyOf;
843
+ definition.enum = Array.from(new Set(branches.map(branch => branch.const)));
844
+ const types = [...new Set(branches.map(branch => branch.type))];
845
+ definition.type = (types.length === 1 ? types[0] : types);
846
+ // `true | false` is just `boolean`; the enum adds nothing.
847
+ if (definition.enum.length === 2 && definition.type === "boolean")
848
+ delete definition.enum;
849
+ return;
850
+ }
851
+ if (branches.every(isBareType)) {
852
+ delete definition.anyOf;
853
+ definition.type = branches.map(branch => branch.type);
854
+ return;
855
+ }
856
+ // `true | false` reached here alongside other branches.
857
+ const hasTrue = branches.some(branch => branch.type === "boolean" && branch.const === true);
858
+ const hasFalse = branches.some(branch => branch.type === "boolean" && branch.const === false);
859
+ if (hasTrue && hasFalse) {
860
+ branches = branches.filter(branch => branch.type !== "boolean");
861
+ if (branches.length === 0) {
862
+ delete definition.anyOf;
863
+ definition.type = "boolean";
864
+ return;
865
+ }
866
+ branches.push({ type: "boolean" });
867
+ definition.anyOf = branches;
868
+ }
869
+ if (definition.anyOf.every(isBareType)) {
870
+ definition.type = definition.anyOf.map(branch => branch.type);
871
+ delete definition.anyOf;
872
+ }
873
+ }
874
+ // -------------------------------------------------------- intersections --
875
+ /**
876
+ * Convert an intersection by merging object constituents.
877
+ *
878
+ * A merge is only sound when every constituent is an object; anything else
879
+ * falls back to `allOf`, which states the constraint without pretending to
880
+ * flatten it.
881
+ * @param type - the intersection type
882
+ * @param definition - schema to populate
883
+ * @param path - schema path
884
+ * @param depth - recursion depth
885
+ * @returns keep
886
+ */
887
+ intersection(type, definition, path, depth) {
888
+ const properties = {};
889
+ const required = new Set();
890
+ const parts = [];
891
+ let closed = true;
892
+ let mergeable = true;
893
+ let index = 0;
894
+ for (const member of type.getTypes()) {
895
+ index++;
896
+ const partPath = `${path}&${index}`;
897
+ const part = {};
898
+ if (member.isClassOrInterface() || this.checker.getPropertiesOfType(member).length > 0) {
899
+ Object.assign(part, this.objectShape(member, partPath));
900
+ if (part.additionalProperties !== false)
901
+ closed = false;
902
+ }
903
+ else {
904
+ const memberNode = this.declarationOf(member.getSymbol()) ?? this.targetNode;
905
+ if (this.property(member, part, partPath, memberNode, depth + 1).decision === "skip")
906
+ continue;
907
+ mergeable = false;
908
+ }
909
+ parts.push(part);
910
+ if (part.type === "object" && part.properties) {
911
+ for (const [name, schema] of Object.entries(part.properties))
912
+ properties[name] ??= schema;
913
+ for (const name of part.required ?? [])
914
+ required.add(name);
915
+ if (part.additionalProperties !== false)
916
+ closed = false;
917
+ }
918
+ else {
919
+ mergeable = false;
920
+ }
921
+ }
922
+ if (mergeable) {
923
+ definition.type = "object";
924
+ definition.properties = properties;
925
+ if (required.size > 0)
926
+ definition.required = [...required].sort();
927
+ definition.additionalProperties = closed ? false : undefined;
928
+ }
929
+ else {
930
+ definition.allOf = parts;
931
+ }
932
+ return KEEP;
933
+ }
934
+ // --------------------------------------------------------------- arrays --
935
+ /**
936
+ * Convert an array or tuple type.
937
+ * @param type - the array-like type
938
+ * @param definition - schema to populate
939
+ * @param path - schema path
940
+ * @param depth - recursion depth
941
+ * @returns keep
942
+ */
943
+ array(type, definition, path, depth) {
944
+ definition.type = "array";
945
+ if (this.checker.isTupleType(type))
946
+ return this.tuple(type, definition, path, depth);
947
+ const element = this.elementType(type);
948
+ if (!element) {
949
+ throw new SchemaConversionError("array element type could not be resolved", path, this.checker.typeToString(type));
950
+ }
951
+ const items = {};
952
+ definition.items = items;
953
+ const elementNode = this.declarationOf(element.getSymbol()) ?? this.targetNode;
954
+ if (this.property(element, items, `${path}[]`, elementNode, depth + 1).decision === "skip") {
955
+ delete definition.items;
956
+ }
957
+ return KEEP;
958
+ }
959
+ /**
960
+ * Convert a tuple into positional or homogeneous `items`.
961
+ * @param type - the tuple reference
962
+ * @param definition - schema to populate
963
+ * @param path - schema path
964
+ * @param depth - recursion depth
965
+ * @returns keep
966
+ */
967
+ tuple(type, definition, path, depth) {
968
+ const target = type.getTarget();
969
+ const elementFlags = target.elementFlags;
970
+ // ElementFlags.Rest === 1 << 2.
971
+ const openEnded = elementFlags.length > 0 && (elementFlags[elementFlags.length - 1] & 4) !== 0;
972
+ const elements = this.checker.getTypeArguments(type);
973
+ const items = [];
974
+ definition.minItems = elements.length;
975
+ elements.forEach((element, index) => {
976
+ const item = {};
977
+ const elementNode = this.declarationOf(element.getSymbol()) ?? this.targetNode;
978
+ if (this.property(element, item, `${path}[${index}]`, elementNode, depth + 1).decision === "skip") {
979
+ definition.minItems--;
980
+ return;
981
+ }
982
+ // ElementFlags.Optional === 1 << 1.
983
+ if ((elementFlags[index] & 2) !== 0)
984
+ definition.minItems--;
985
+ items.push(item);
986
+ });
987
+ definition.items = isHomogeneous(items) ? items[0] : items;
988
+ if (openEnded)
989
+ definition.minItems--;
990
+ else
991
+ definition.maxItems = elements.length;
992
+ if (target.readonly) {
993
+ // A readonly tuple of literals is a single constant value.
994
+ const constants = items.map(item => item.const).filter(value => value !== undefined);
995
+ if (constants.length === items.length)
996
+ definition.const = constants;
997
+ delete definition.items;
998
+ delete definition.minItems;
999
+ delete definition.maxItems;
1000
+ }
1001
+ return KEEP;
1002
+ }
1003
+ /**
1004
+ * Whether the type should be represented as a JSON array.
1005
+ *
1006
+ * Mapped types are excluded explicitly: the checker synthesises a numeric
1007
+ * index signature for some of them, which would otherwise read as an array.
1008
+ * @param type - the type to test
1009
+ * @returns true when array-like
1010
+ */
1011
+ isArrayLike(type) {
1012
+ if (objectFlagsOf(type) & ObjectFlags.Mapped)
1013
+ return false;
1014
+ if (this.checker.isArrayLikeType(type))
1015
+ return true;
1016
+ if (this.checker.getIndexTypeOfType(type, IndexKind.Number))
1017
+ return true;
1018
+ return false;
1019
+ }
1020
+ /**
1021
+ * The element type of an array-like type.
1022
+ * @param type - the array-like type
1023
+ * @returns the element type, or undefined when it cannot be resolved
1024
+ */
1025
+ elementType(type) {
1026
+ const byIndex = this.checker.getIndexTypeOfType(type, IndexKind.Number);
1027
+ if (byIndex)
1028
+ return byIndex;
1029
+ if (type.isTypeReference()) {
1030
+ const args = this.checker.getTypeArguments(type);
1031
+ if (args.length > 0)
1032
+ return args[0];
1033
+ }
1034
+ return undefined;
1035
+ }
1036
+ // -------------------------------------------------------------- buffers --
1037
+ /**
1038
+ * Whether the type is a binary buffer.
1039
+ * @param type - the type to test
1040
+ * @param typeName - its stringified form
1041
+ * @returns true for `Buffer`, `ArrayBuffer` or a `Buffer`-flavoured `Uint8Array`
1042
+ */
1043
+ isBuffer(type, typeName) {
1044
+ const name = type.getSymbol()?.name;
1045
+ if (!name)
1046
+ return false;
1047
+ if (name === "Buffer" || typeName === "Buffer")
1048
+ return true;
1049
+ if (name === "ArrayBuffer" || typeName === "ArrayBuffer")
1050
+ return true;
1051
+ return name === "Uint8Array" && /Buffer/.test(typeName);
1052
+ }
1053
+ /**
1054
+ * Write the configured buffer representation.
1055
+ * @param definition - schema to populate
1056
+ */
1057
+ applyBuffer(definition) {
1058
+ switch (this.bufferStrategy) {
1059
+ case "array":
1060
+ definition.type = "array";
1061
+ definition.items = { type: "integer", minimum: 0, maximum: 255 };
1062
+ return;
1063
+ case "binary":
1064
+ definition.type = "string";
1065
+ definition.format = "binary";
1066
+ definition.contentMediaType = "application/octet-stream";
1067
+ return;
1068
+ case "hex":
1069
+ definition.type = "string";
1070
+ definition.contentEncoding = "hex";
1071
+ definition.pattern = "^[0-9a-fA-F]+$";
1072
+ definition.contentMediaType = "application/octet-stream";
1073
+ return;
1074
+ case "base64":
1075
+ definition.type = "string";
1076
+ definition.contentEncoding = "base64";
1077
+ definition.contentMediaType = "application/octet-stream";
1078
+ return;
1079
+ }
1080
+ }
1081
+ // --------------------------------------------------------------- shared --
1082
+ /**
1083
+ * Whether the current mode describes values being supplied.
1084
+ * @returns true for `input` and `dto-in`
1085
+ */
1086
+ isInputMode() {
1087
+ return this.mode === "input" || this.mode === "dto-in";
1088
+ }
1089
+ /**
1090
+ * Resolve a symbol's primary declaration node.
1091
+ *
1092
+ * `Symbol.declarations` are handles, not nodes; reaching the AST needs an
1093
+ * explicit `resolve` against the project.
1094
+ * @param symbol - the symbol
1095
+ * @returns the declaration, when it has one
1096
+ */
1097
+ declarationOf(symbol) {
1098
+ if (!symbol)
1099
+ return undefined;
1100
+ return symbol.valueDeclaration?.resolve(this.project) ?? symbol.declarations[0]?.resolve(this.project);
1101
+ }
1102
+ /**
1103
+ * Whether a property is keyed by an ES symbol.
1104
+ *
1105
+ * Symbol-keyed slots are how Webda stores model internals
1106
+ * (`[WEBDA_STORAGE]`, `[WEBDA_EVENTS]`), and none of them are JSON.
1107
+ * @param prop - the property symbol
1108
+ * @param declaration - its declaration
1109
+ * @returns true when the key is an ES symbol
1110
+ */
1111
+ isSymbolKeyed(prop, declaration) {
1112
+ if (declaration && hasName(declaration)) {
1113
+ const name = declaration.name;
1114
+ if (name && is.isComputedPropertyName(name)) {
1115
+ const keyFlags = this.checker.getTypeAtLocation(name.expression).flags;
1116
+ if (keyFlags & (TypeFlags.ESSymbol | TypeFlags.UniqueESSymbol))
1117
+ return true;
1118
+ }
1119
+ }
1120
+ return String(prop.escapedName).startsWith("__@");
1121
+ }
1122
+ /**
1123
+ * The initializer expression governing a type, if any.
1124
+ * @param type - the type
1125
+ * @param node - the declaration it was read from
1126
+ * @returns the initializer expression
1127
+ */
1128
+ initializerFor(type, node) {
1129
+ if (node)
1130
+ return initializerOf(node);
1131
+ return initializerOf(this.declarationOf(type.getSymbol()));
1132
+ }
1133
+ /**
1134
+ * The name under which a declaration's schema is registered.
1135
+ * @param node - the node being converted
1136
+ * @returns the declared name, or undefined for type nodes
1137
+ */
1138
+ declaredName(node) {
1139
+ if (is.isClassDeclaration(node) || is.isInterfaceDeclaration(node) || is.isTypeAliasDeclaration(node)) {
1140
+ return node.name?.text;
1141
+ }
1142
+ return undefined;
1143
+ }
1144
+ }
1145
+ // ------------------------------------------------------------- free helpers --
1146
+ /**
1147
+ * Join a schema path segment.
1148
+ * @param base - the parent path
1149
+ * @param segment - the child segment
1150
+ * @returns the joined path
1151
+ */
1152
+ function joinPath(base, segment) {
1153
+ if (!base || base === "/")
1154
+ return `/${segment}`;
1155
+ return `${base.replace(/\/+$/, "")}/${segment.replace(/^\/+/, "")}`;
1156
+ }
1157
+ /**
1158
+ * Whether an object type is anonymous rather than declared.
1159
+ * @param type - the type to test
1160
+ * @returns true for anonymous object types
1161
+ */
1162
+ function isAnonymous(type) {
1163
+ return (objectFlagsOf(type) & ObjectFlags.Anonymous) !== 0;
1164
+ }
1165
+ /**
1166
+ * A type's object flags, or zero when it is not an object type.
1167
+ * @param type - the type
1168
+ * @returns the object flags
1169
+ */
1170
+ function objectFlagsOf(type) {
1171
+ return type.isObjectType() ? type.objectFlags : 0;
1172
+ }
1173
+ /** Ranks reserved for primitives; anything else sorts after them. */
1174
+ const PRIMITIVE_RANKS = 100;
1175
+ /**
1176
+ * Where a primitive sits in the checker's intrinsic-type creation order.
1177
+ *
1178
+ * Taken from `initializeTypeChecker`, which interns these before any user
1179
+ * type, so in TypeScript 6 they always carried the lowest ids and therefore
1180
+ * came first in a union. Boolean literals are ranked by value because 7.1
1181
+ * leaves `intrinsicName` empty on them.
1182
+ * @param type - the constituent to rank
1183
+ * @returns its rank, or undefined when it is not a primitive
1184
+ */
1185
+ function primitiveRank(type) {
1186
+ const flags = type.flags;
1187
+ if (flags & TypeFlags.Any)
1188
+ return 0;
1189
+ if (flags & TypeFlags.Unknown)
1190
+ return 1;
1191
+ if (flags & TypeFlags.Undefined)
1192
+ return 2;
1193
+ if (flags & TypeFlags.Null)
1194
+ return 3;
1195
+ if (flags & TypeFlags.StringLiteral)
1196
+ return undefined;
1197
+ if (flags & TypeFlags.String)
1198
+ return 4;
1199
+ if (flags & TypeFlags.NumberLiteral)
1200
+ return undefined;
1201
+ if (flags & TypeFlags.Number)
1202
+ return 5;
1203
+ if (flags & TypeFlags.BigIntLiteral)
1204
+ return undefined;
1205
+ if (flags & TypeFlags.BigInt)
1206
+ return 6;
1207
+ if (flags & TypeFlags.BooleanLiteral)
1208
+ return type.value === true ? 8 : 7;
1209
+ if (flags & TypeFlags.Boolean)
1210
+ return 9;
1211
+ if (flags & TypeFlags.ESSymbol)
1212
+ return 10;
1213
+ if (flags & TypeFlags.Void)
1214
+ return 11;
1215
+ if (flags & TypeFlags.Never)
1216
+ return 12;
1217
+ if (flags & TypeFlags.NonPrimitive)
1218
+ return 13;
1219
+ return undefined;
1220
+ }
1221
+ /**
1222
+ * Whether a schema says nothing but `type`.
1223
+ * @param schema - the schema to test
1224
+ * @returns true when `type` is its only keyword
1225
+ */
1226
+ function isBareType(schema) {
1227
+ return Object.keys(schema).length === 1 && schema.type !== undefined;
1228
+ }
1229
+ /**
1230
+ * Drop structurally identical branches from a union.
1231
+ * @param branches - the branch schemas
1232
+ * @returns the branches, deduplicated, in first-seen order
1233
+ */
1234
+ function dedupe(branches) {
1235
+ const seen = new Set();
1236
+ const unique = [];
1237
+ for (const branch of branches) {
1238
+ const key = JSON.stringify(branch);
1239
+ if (seen.has(key))
1240
+ continue;
1241
+ seen.add(key);
1242
+ unique.push(branch);
1243
+ }
1244
+ return unique;
1245
+ }
1246
+ /** Keywords compared when deciding whether tuple elements share a shape. */
1247
+ const SHAPE_KEYWORDS = ["type", "const", "enum", "format", "pattern", "minimum", "maximum", "minLength", "maxLength"];
1248
+ /**
1249
+ * Whether every tuple element has the same constraining keywords.
1250
+ * @param items - the element schemas
1251
+ * @returns true when a single `items` schema is equivalent
1252
+ */
1253
+ function isHomogeneous(items) {
1254
+ if (items.length === 0)
1255
+ return false;
1256
+ const first = items[0];
1257
+ const count = (schema) => Object.keys(schema).filter(key => SHAPE_KEYWORDS.includes(key)).length;
1258
+ return items.every(item => SHAPE_KEYWORDS.every(key => JSON.stringify(first[key]) === JSON.stringify(item[key])) &&
1259
+ count(item) === count(first));
1260
+ }
1261
+ /**
1262
+ * Read a declaration's initializer.
1263
+ * @param node - the declaration
1264
+ * @returns the initializer expression, when present
1265
+ */
1266
+ function initializerOf(node) {
1267
+ if (!node)
1268
+ return undefined;
1269
+ if (is.isPropertyDeclaration(node) ||
1270
+ is.isPropertySignatureDeclaration(node) ||
1271
+ is.isPropertyAssignment(node) ||
1272
+ is.isParameterDeclaration(node) ||
1273
+ is.isVariableDeclaration(node) ||
1274
+ is.isEnumMember(node)) {
1275
+ return node.initializer ?? undefined;
1276
+ }
1277
+ return undefined;
1278
+ }
1279
+ /**
1280
+ * Extract a non-numeric literal value from an initializer.
1281
+ *
1282
+ * Numbers are deliberately excluded: `@webda/schema` treats them as values
1283
+ * rather than schema defaults, and enum numbering is handled separately.
1284
+ * @param expression - the initializer
1285
+ * @returns the JSON Schema type and constant, or undefined
1286
+ */
1287
+ function literalOf(expression) {
1288
+ if (!expression)
1289
+ return undefined;
1290
+ const inner = is.skipOuterExpressions(expression);
1291
+ switch (inner.kind) {
1292
+ case SyntaxKind.TrueKeyword:
1293
+ return { type: "boolean", const: true };
1294
+ case SyntaxKind.FalseKeyword:
1295
+ return { type: "boolean", const: false };
1296
+ case SyntaxKind.NullKeyword:
1297
+ return { type: "null", const: null };
1298
+ case SyntaxKind.StringLiteral:
1299
+ return { type: "string", const: inner.text };
1300
+ default:
1301
+ return undefined;
1302
+ }
1303
+ }
1304
+ /**
1305
+ * Whether a declaration is a method rather than a data property.
1306
+ * @param node - the declaration
1307
+ * @returns true for methods and function-typed members
1308
+ */
1309
+ function isMethodLike(node) {
1310
+ return is.isMethodDeclaration(node) || is.isMethodSignatureDeclaration(node) || is.isFunctionTypeNode(node);
1311
+ }
1312
+ /**
1313
+ * Whether a member declaration carries a `?` token.
1314
+ * @param node - the declaration
1315
+ * @returns true when optional
1316
+ */
1317
+ function hasQuestionToken(node) {
1318
+ if (!node)
1319
+ return false;
1320
+ const postfix = node.postfixToken;
1321
+ if (postfix)
1322
+ return postfix.kind === SyntaxKind.QuestionToken;
1323
+ return node.questionToken !== undefined;
1324
+ }
1325
+ /**
1326
+ * Whether a property is written as `T | undefined`.
1327
+ *
1328
+ * Checked on the syntax rather than the type because most Webda packages
1329
+ * compile with `strict: false`, where the checker folds `string | undefined`
1330
+ * back to `string` and the property would otherwise be required. The author
1331
+ * wrote `| undefined`, so the schema says optional.
1332
+ * @param node - the property declaration
1333
+ * @returns true when `undefined` appears in a written union
1334
+ */
1335
+ function writesUndefined(node) {
1336
+ const typeNode = node && node.type;
1337
+ if (!typeNode || !is.isUnionTypeNode(typeNode))
1338
+ return false;
1339
+ return typeNode.types.some(member => member.kind === SyntaxKind.UndefinedKeyword);
1340
+ }
1341
+ /**
1342
+ * Whether a node has a declaration name.
1343
+ * @param node - the node
1344
+ * @returns true when it exposes a `name`
1345
+ */
1346
+ function hasName(node) {
1347
+ return "name" in node;
1348
+ }
1349
+ /**
1350
+ * A declaration's modifier flags, or zero when it has none.
1351
+ * @param node - the declaration
1352
+ * @returns the modifier flags
1353
+ */
1354
+ function modifierFlagsOf(node) {
1355
+ return node.modifierFlags ?? 0;
1356
+ }
1357
+ /**
1358
+ * Recursively sort object keys.
1359
+ *
1360
+ * The committed `webda.module.json` is key-sorted, so a stable order is part
1361
+ * of byte-identical output rather than a nicety.
1362
+ * @param value - any JSON value
1363
+ * @returns a deep copy with sorted keys
1364
+ */
1365
+ function sortKeys(value) {
1366
+ if (Array.isArray(value))
1367
+ return value.map(sortKeys);
1368
+ if (value && typeof value === "object") {
1369
+ const sorted = {};
1370
+ for (const key of Object.keys(value).sort()) {
1371
+ sorted[key] = sortKeys(value[key]);
1372
+ }
1373
+ return sorted;
1374
+ }
1375
+ return value;
1376
+ }
1377
+ //# sourceMappingURL=converter.js.map