@kubb/ast 5.0.0-beta.55 → 5.0.0-beta.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -3,6 +3,131 @@ const require_utils = require("./utils-cdQ6Pzyi.cjs");
3
3
  let node_crypto = require("node:crypto");
4
4
  let node_path = require("node:path");
5
5
  node_path = require_utils.__toESM(node_path, 1);
6
+ //#region src/node.ts
7
+ /**
8
+ * Builds a type guard that matches nodes of the given `kind`.
9
+ */
10
+ function isKind(kind) {
11
+ return (node) => node.kind === kind;
12
+ }
13
+ /**
14
+ * Updates a schema's `optional` and `nullish` flags from a parent's `required`
15
+ * value and the schema's own `nullable`. Mirrors how OpenAPI parameters and
16
+ * object properties combine "required" and "nullable" into a single AST.
17
+ *
18
+ * - Non-required + non-nullable → `optional: true`.
19
+ * - Non-required + nullable → `nullish: true`.
20
+ * - Required → both flags cleared.
21
+ */
22
+ function syncOptionality(schema, required) {
23
+ const nullable = schema.nullable ?? false;
24
+ return {
25
+ ...schema,
26
+ optional: !required && !nullable ? true : void 0,
27
+ nullish: !required && nullable ? true : void 0
28
+ };
29
+ }
30
+ /**
31
+ * Defines a node once and derives its `create` builder, `is` guard, and traversal
32
+ * metadata. `create` merges `defaults`, the `build` hook (or the raw input), and the
33
+ * `kind`, so node construction lives in one place without scattered `as` casts.
34
+ *
35
+ * Set `rebuild: true` when the `build` hook derives fields from children. After a
36
+ * transform rewrites those children, the registry reruns `create` so the derived
37
+ * fields stay correct.
38
+ *
39
+ * @example Simple node
40
+ * ```ts
41
+ * const importDef = defineNode<ImportNode>({ kind: 'Import' })
42
+ * const createImport = importDef.create
43
+ * ```
44
+ *
45
+ * @example Node with a build hook that is rerun on transform
46
+ * ```ts
47
+ * const propertyDef = defineNode<PropertyNode, UserPropertyNode>({
48
+ * kind: 'Property',
49
+ * build: (props) => ({ ...props, required: props.required ?? false }),
50
+ * children: ['schema'],
51
+ * visitorKey: 'property',
52
+ * rebuild: true,
53
+ * })
54
+ * ```
55
+ */
56
+ function defineNode(config) {
57
+ const { kind, defaults, build, children, visitorKey, rebuild } = config;
58
+ function create(input) {
59
+ const base = build ? build(input) : input;
60
+ return {
61
+ ...defaults,
62
+ ...base,
63
+ kind
64
+ };
65
+ }
66
+ return {
67
+ kind,
68
+ create,
69
+ is: isKind(kind),
70
+ children,
71
+ visitorKey,
72
+ rebuild
73
+ };
74
+ }
75
+ //#endregion
76
+ //#region src/nodes/schema.ts
77
+ /**
78
+ * Maps schema `type` to its underlying `primitive`.
79
+ * Primitive types map to themselves. Special string formats map to `'string'`.
80
+ * Complex types (`ref`, `enum`, `union`, `intersection`, `tuple`, `blob`) are left unset.
81
+ */
82
+ const TYPE_TO_PRIMITIVE = {
83
+ string: "string",
84
+ number: "number",
85
+ integer: "integer",
86
+ bigint: "bigint",
87
+ boolean: "boolean",
88
+ null: "null",
89
+ any: "any",
90
+ unknown: "unknown",
91
+ void: "void",
92
+ never: "never",
93
+ object: "object",
94
+ array: "array",
95
+ date: "date",
96
+ uuid: "string",
97
+ email: "string",
98
+ url: "string",
99
+ datetime: "string",
100
+ time: "string"
101
+ };
102
+ /**
103
+ * Definition for the {@link SchemaNode}. Object schemas default `properties` to an
104
+ * empty array, and `primitive` is inferred from `type` when not explicitly provided.
105
+ */
106
+ const schemaDef = defineNode({
107
+ kind: "Schema",
108
+ build: (props) => {
109
+ if (props.type === "object") return {
110
+ properties: [],
111
+ primitive: "object",
112
+ ...props
113
+ };
114
+ return {
115
+ primitive: TYPE_TO_PRIMITIVE[props.type],
116
+ ...props
117
+ };
118
+ },
119
+ children: [
120
+ "properties",
121
+ "items",
122
+ "members",
123
+ "additionalProperties"
124
+ ],
125
+ visitorKey: "schema"
126
+ });
127
+ function createSchema(props) {
128
+ return schemaDef.create(props);
129
+ }
130
+ //#endregion
6
131
  //#region ../../internals/utils/src/fs.ts
7
132
  /**
8
133
  * Strips the file extension from a path or file name.
@@ -60,2040 +185,1857 @@ function memoize(store, factory) {
60
185
  };
61
186
  }
62
187
  //#endregion
63
- //#region src/guards.ts
188
+ //#region src/signature.ts
64
189
  /**
65
- * Narrows a `SchemaNode` to the variant that matches `type`.
66
- *
67
- * @example
68
- * ```ts
69
- * const schema = createSchema({ type: 'string' })
70
- * const stringNode = narrowSchema(schema, 'string') // StringSchemaNode | null
71
- * ```
190
+ * The flags shared by every node kind that affect its type: `primitive`, `format`, `nullable`.
72
191
  */
73
- function narrowSchema(node, type) {
74
- return node?.type === type ? node : null;
75
- }
76
- function isKind(kind) {
77
- return (node) => node.kind === kind;
192
+ function flagsDescriptor(node) {
193
+ return `${node.primitive ?? ""};${node.format ?? ""};${node.nullable ? 1 : 0}`;
78
194
  }
79
- /**
80
- * Returns `true` when the input is an `InputNode`.
81
- *
82
- * @example
83
- * ```ts
84
- * if (isInputNode(node)) {
85
- * console.log(node.schemas.length)
86
- * }
87
- * ```
88
- */
89
- const isInputNode = isKind("Input");
90
- /**
91
- * Returns `true` when the input is an `OutputNode`.
92
- *
93
- * @example
94
- * ```ts
95
- * if (isOutputNode(node)) {
96
- * console.log(node.files.length)
97
- * }
98
- * ```
99
- */
100
- const isOutputNode = isKind("Output");
101
- /**
102
- * Returns `true` when the input is an `OperationNode`.
103
- *
104
- * @example
105
- * ```ts
106
- * if (isOperationNode(node)) {
107
- * console.log(node.operationId)
108
- * }
109
- * ```
110
- */
111
- const isOperationNode = isKind("Operation");
112
- /**
113
- * Narrows an `OperationNode` to an `HttpOperationNode`, guaranteeing `method` and `path`.
114
- *
115
- * @example
116
- * ```ts
117
- * if (isHttpOperationNode(node)) {
118
- * console.log(node.method, node.path)
119
- * }
120
- * ```
121
- */
122
- function isHttpOperationNode(node) {
123
- return node.protocol === "http" || node.method !== void 0 && node.path !== void 0;
195
+ function refTargetName(node) {
196
+ if (node.ref) return require_utils.extractRefName(node.ref);
197
+ return node.name ?? "";
124
198
  }
199
+ const arrayTupleFields = [
200
+ {
201
+ kind: "children",
202
+ key: "items",
203
+ prefix: "i"
204
+ },
205
+ {
206
+ kind: "child",
207
+ key: "rest",
208
+ prefix: "r"
209
+ },
210
+ {
211
+ kind: "scalar",
212
+ key: "min",
213
+ prefix: "mn"
214
+ },
215
+ {
216
+ kind: "scalar",
217
+ key: "max",
218
+ prefix: "mx"
219
+ },
220
+ {
221
+ kind: "bool",
222
+ key: "unique",
223
+ prefix: "u"
224
+ }
225
+ ];
226
+ const numericFields = [
227
+ {
228
+ kind: "scalar",
229
+ key: "min",
230
+ prefix: "mn"
231
+ },
232
+ {
233
+ kind: "scalar",
234
+ key: "max",
235
+ prefix: "mx"
236
+ },
237
+ {
238
+ kind: "scalar",
239
+ key: "exclusiveMinimum",
240
+ prefix: "emn"
241
+ },
242
+ {
243
+ kind: "scalar",
244
+ key: "exclusiveMaximum",
245
+ prefix: "emx"
246
+ },
247
+ {
248
+ kind: "scalar",
249
+ key: "multipleOf",
250
+ prefix: "mo"
251
+ }
252
+ ];
253
+ const rangeFields = [{
254
+ kind: "scalar",
255
+ key: "min",
256
+ prefix: "mn"
257
+ }, {
258
+ kind: "scalar",
259
+ key: "max",
260
+ prefix: "mx"
261
+ }];
125
262
  /**
126
- * Returns `true` when the input is a `SchemaNode`.
127
- *
128
- * @example
129
- * ```ts
130
- * if (isSchemaNode(node)) {
131
- * console.log(node.type)
132
- * }
133
- * ```
134
- */
135
- const isSchemaNode = isKind("Schema");
136
- //#endregion
137
- //#region src/visitor.ts
138
- /**
139
- * Creates a small async concurrency limiter.
140
- *
141
- * At most `concurrency` tasks are in flight at once. Extra tasks are queued.
142
- *
143
- * @example
144
- * ```ts
145
- * const limit = createLimit(2)
146
- * for (const task of [taskA, taskB, taskC]) {
147
- * await limit(() => task())
148
- * }
149
- * // only 2 tasks run at the same time
150
- * ```
263
+ * Maps each node `type` to its ordered shape-contributing fields. Types absent from the map
264
+ * (boolean, null, any, and other scalars) fall back to `${type}|${flags}`.
151
265
  */
152
- function createLimit(concurrency) {
153
- let active = 0;
154
- const queue = [];
155
- function next() {
156
- if (active < concurrency && queue.length > 0) {
157
- active++;
158
- queue.shift()();
266
+ const SHAPE_KEYS = {
267
+ object: [
268
+ { kind: "objectProps" },
269
+ { kind: "additionalProps" },
270
+ { kind: "patternProps" },
271
+ {
272
+ kind: "scalar",
273
+ key: "minProperties",
274
+ prefix: "mn"
275
+ },
276
+ {
277
+ kind: "scalar",
278
+ key: "maxProperties",
279
+ prefix: "mx"
159
280
  }
160
- }
161
- return function limit(fn) {
162
- return new Promise((resolve, reject) => {
163
- queue.push(() => {
164
- Promise.resolve(fn()).then(resolve, reject).finally(() => {
165
- active--;
166
- next();
167
- });
168
- });
169
- next();
170
- });
171
- };
172
- }
173
- const visitorKeysByKind = {
174
- Input: ["schemas", "operations"],
175
- Operation: [
176
- "parameters",
177
- "requestBody",
178
- "responses"
179
281
  ],
180
- RequestBody: ["content"],
181
- Content: ["schema"],
182
- Response: ["content"],
183
- Schema: [
184
- "properties",
185
- "items",
186
- "members",
187
- "additionalProperties"
282
+ array: arrayTupleFields,
283
+ tuple: arrayTupleFields,
284
+ union: [
285
+ {
286
+ kind: "scalar",
287
+ key: "strategy",
288
+ prefix: "s"
289
+ },
290
+ {
291
+ kind: "scalar",
292
+ key: "discriminatorPropertyName",
293
+ prefix: "d"
294
+ },
295
+ {
296
+ kind: "children",
297
+ key: "members",
298
+ prefix: "m"
299
+ }
300
+ ],
301
+ intersection: [{
302
+ kind: "children",
303
+ key: "members",
304
+ prefix: "m"
305
+ }],
306
+ enum: [{ kind: "enumValues" }],
307
+ ref: [{ kind: "refTarget" }],
308
+ string: [
309
+ {
310
+ kind: "scalar",
311
+ key: "min",
312
+ prefix: "mn"
313
+ },
314
+ {
315
+ kind: "scalar",
316
+ key: "max",
317
+ prefix: "mx"
318
+ },
319
+ {
320
+ kind: "scalar",
321
+ key: "pattern",
322
+ prefix: "pt"
323
+ }
324
+ ],
325
+ number: numericFields,
326
+ integer: numericFields,
327
+ bigint: numericFields,
328
+ url: [
329
+ {
330
+ kind: "scalar",
331
+ key: "path",
332
+ prefix: "path"
333
+ },
334
+ {
335
+ kind: "scalar",
336
+ key: "min",
337
+ prefix: "mn"
338
+ },
339
+ {
340
+ kind: "scalar",
341
+ key: "max",
342
+ prefix: "mx"
343
+ }
188
344
  ],
189
- Property: ["schema"],
190
- Parameter: ["schema"]
345
+ uuid: rangeFields,
346
+ email: rangeFields,
347
+ datetime: [{
348
+ kind: "bool",
349
+ key: "offset",
350
+ prefix: "o"
351
+ }, {
352
+ kind: "bool",
353
+ key: "local",
354
+ prefix: "l"
355
+ }],
356
+ date: [{
357
+ kind: "scalar",
358
+ key: "representation",
359
+ prefix: "rep"
360
+ }],
361
+ time: [{
362
+ kind: "scalar",
363
+ key: "representation",
364
+ prefix: "rep"
365
+ }]
191
366
  };
367
+ function serializeShapeField(field, node, record) {
368
+ switch (field.kind) {
369
+ case "scalar": return `${field.prefix}:${record[field.key] ?? ""}`;
370
+ case "bool": return `${field.prefix}:${record[field.key] ? 1 : 0}`;
371
+ case "child": {
372
+ const child = record[field.key];
373
+ return `${field.prefix}:${child ? signatureOf(child) : ""}`;
374
+ }
375
+ case "children": {
376
+ const children = record[field.key] ?? [];
377
+ return `${field.prefix}[${children.map((c) => signatureOf(c)).join(",")}]`;
378
+ }
379
+ case "objectProps": return `p[${(node.properties ?? []).map((prop) => `${prop.name}${prop.required ? "!" : "?"}${signatureOf(prop.schema)}`).join(",")}]`;
380
+ case "additionalProps": {
381
+ const obj = node;
382
+ if (typeof obj.additionalProperties === "boolean") return `ab:${obj.additionalProperties}`;
383
+ if (obj.additionalProperties) return `as:${signatureOf(obj.additionalProperties)}`;
384
+ return "";
385
+ }
386
+ case "patternProps": {
387
+ const obj = node;
388
+ return `pp[${obj.patternProperties ? Object.keys(obj.patternProperties).sort().map((key) => `${key}=${signatureOf(obj.patternProperties[key])}`).join(",") : ""}]`;
389
+ }
390
+ case "enumValues": {
391
+ const en = node;
392
+ let values = "";
393
+ if (en.namedEnumValues?.length) values = en.namedEnumValues.map((entry) => `${entry.name}=${entry.primitive}:${String(entry.value)}`).join(",");
394
+ else if (en.enumValues?.length) values = en.enumValues.map((value) => `${value === null ? "null" : typeof value}:${String(value)}`).join(",");
395
+ return `v[${values}]`;
396
+ }
397
+ case "refTarget": return `->${refTargetName(node)}`;
398
+ }
399
+ }
192
400
  /**
193
- * Returns `true` when `value` is an AST node (an object carrying a `kind`).
401
+ * Builds the local shape descriptor that {@link signatureOf} hashes: the node's kind, flags,
402
+ * constraints, and its children's signatures.
194
403
  */
195
- function isNode(value) {
196
- return typeof value === "object" && value !== null && "kind" in value;
404
+ function describeShape(node) {
405
+ const flags = flagsDescriptor(node);
406
+ const fields = SHAPE_KEYS[node.type];
407
+ if (!fields) return `${node.type}|${flags}`;
408
+ const record = node;
409
+ const parts = [`${node.type}|${flags}`];
410
+ for (const field of fields) parts.push(serializeShapeField(field, node, record));
411
+ return parts.join("|");
197
412
  }
198
413
  /**
199
- * Returns the immediate traversable children of `node` based on {@link VISITOR_KEYS}.
200
- *
201
- * `Schema` children are only included when `recurse` is `true`. Shallow mode skips them.
414
+ * Node digest cache, keyed by identity. A `WeakMap` so entries die with the node, and so a tree
415
+ * hashed during dedupe planning is not walked again when it is rewritten during streaming. Reuse
416
+ * is safe because a signature depends only on content, and nodes are immutable once created.
417
+ */
418
+ const signatureCache = /* @__PURE__ */ new WeakMap();
419
+ /**
420
+ * Computes a deterministic, shape-only signature (a content hash) for a schema node. Two schemas
421
+ * share a signature when they are structurally identical, ignoring documentation (`name`, `title`,
422
+ * `description`, `example`, `default`, `deprecated`) and usage-slot flags (`optional`, `nullish`,
423
+ * `readOnly`, `writeOnly`). `nullable` is kept because it changes the produced type, and `ref`
424
+ * nodes compare by target name, which also terminates on circular shapes.
202
425
  *
203
- * @example
426
+ * @example Two enums with different descriptions share a signature
204
427
  * ```ts
205
- * const children = getChildren(operationNode, true)
206
- * // returns parameters, the request body, and responses
428
+ * signatureOf(createSchema({ type: 'enum', primitive: 'string', enumValues: ['a', 'b'], description: 'x' })) ===
429
+ * signatureOf(createSchema({ type: 'enum', primitive: 'string', enumValues: ['a', 'b'] }))
207
430
  * ```
208
431
  */
209
- function* getChildren(node, recurse) {
210
- if (node.kind === "Schema" && !recurse) return;
211
- const keys = visitorKeysByKind[node.kind];
212
- if (!keys) return;
213
- const record = node;
214
- for (const key of keys) {
215
- const value = record[key];
216
- if (Array.isArray(value)) {
217
- for (const item of value) if (isNode(item)) yield item;
218
- } else if (isNode(value)) yield value;
219
- }
432
+ function signatureOf(node) {
433
+ const cached = signatureCache.get(node);
434
+ if (cached !== void 0) return cached;
435
+ const signature = (0, node_crypto.hash)("sha256", describeShape(node), "hex");
436
+ signatureCache.set(node, signature);
437
+ return signature;
220
438
  }
439
+ //#endregion
440
+ //#region src/nodes/code.ts
221
441
  /**
222
- * Maps a node `kind` to the matching visitor callback name. Only the seven
223
- * traversable node kinds have an entry. Every other kind resolves to
224
- * `undefined` and is skipped.
442
+ * Definition for the {@link ConstNode}.
225
443
  */
226
- const VISITOR_KEY_BY_KIND = {
227
- Input: "input",
228
- Output: "output",
229
- Operation: "operation",
230
- Schema: "schema",
231
- Property: "property",
232
- Parameter: "parameter",
233
- Response: "response"
234
- };
444
+ const constDef = defineNode({ kind: "Const" });
235
445
  /**
236
- * Invokes the visitor callback that matches `node.kind`, passing the traversal
237
- * context. Returns the callback's result (a replacement node, a collected
238
- * value, or `undefined` when no callback is registered for the kind).
446
+ * Creates a `ConstNode` representing a TypeScript `const` declaration.
239
447
  *
240
- * Shared by `walk`, `transform`, and `collectLazy` so node-kind dispatch lives
241
- * in one place. `TResult` is the caller's expected return: the same node type
242
- * for `transform`, the collected value type for `collectLazy`, ignored for `walk`.
448
+ * @example Exported constant with type and `as const`
449
+ * ```ts
450
+ * createConst({ name: 'pets', export: true, type: 'Pet[]', asConst: true })
451
+ * // export const pets: Pet[] = ... as const
452
+ * ```
243
453
  */
244
- function applyVisitor(node, visitor, parent) {
245
- const key = VISITOR_KEY_BY_KIND[node.kind];
246
- if (!key) return void 0;
247
- const fn = visitor[key];
248
- return fn?.(node, { parent });
249
- }
454
+ const createConst = constDef.create;
250
455
  /**
251
- * Async depth-first traversal for side effects. Visitor return values are
252
- * ignored. Use `transform` when you want to rewrite nodes.
456
+ * Definition for the {@link TypeNode}.
457
+ */
458
+ const typeDef = defineNode({ kind: "Type" });
459
+ /**
460
+ * Creates a `TypeNode` representing a TypeScript `type` alias declaration.
253
461
  *
254
- * Sibling nodes at each depth run concurrently up to `options.concurrency`
255
- * (defaults to `WALK_CONCURRENCY`). Higher values overlap I/O-bound visitor
256
- * work. Lower values reduce memory pressure.
462
+ * @example
463
+ * ```ts
464
+ * createType({ name: 'Pet', export: true })
465
+ * // export type Pet = ...
466
+ * ```
467
+ */
468
+ const createType = typeDef.create;
469
+ /**
470
+ * Definition for the {@link FunctionNode}.
471
+ */
472
+ const functionDef = defineNode({ kind: "Function" });
473
+ /**
474
+ * Creates a `FunctionNode` representing a TypeScript `function` declaration.
257
475
  *
258
- * @example Log every operation
476
+ * @example
259
477
  * ```ts
260
- * await walk(root, {
261
- * operation(node) {
262
- * console.log(node.operationId)
263
- * },
264
- * })
478
+ * createFunction({ name: 'fetchPet', export: true, async: true, returnType: 'Pet' })
479
+ * // export async function fetchPet(): Promise<Pet> { ... }
265
480
  * ```
481
+ */
482
+ const createFunction = functionDef.create;
483
+ /**
484
+ * Definition for the {@link ArrowFunctionNode}.
485
+ */
486
+ const arrowFunctionDef = defineNode({ kind: "ArrowFunction" });
487
+ /**
488
+ * Creates an `ArrowFunctionNode` representing a TypeScript arrow function.
266
489
  *
267
- * @example Only visit the root node
490
+ * @example
268
491
  * ```ts
269
- * await walk(root, { depth: 'shallow', input: () => {} })
492
+ * createArrowFunction({ name: 'double', export: true, params: 'n: number', singleLine: true })
493
+ * // export const double = (n: number) => ...
270
494
  * ```
271
495
  */
272
- async function walk(node, options) {
273
- return _walk(node, options, (options.depth ?? require_utils.visitorDepths.deep) === require_utils.visitorDepths.deep, createLimit(options.concurrency ?? 30), void 0);
274
- }
275
- async function _walk(node, visitor, recurse, limit, parent) {
276
- await limit(() => applyVisitor(node, visitor, parent));
277
- const children = Array.from(getChildren(node, recurse));
278
- if (children.length === 0) return;
279
- await Promise.all(children.map((child) => _walk(child, visitor, recurse, limit, node)));
280
- }
281
- function transform(node, options) {
282
- const { depth, parent, ...visitor } = options;
283
- const recurse = (depth ?? require_utils.visitorDepths.deep) === require_utils.visitorDepths.deep;
284
- const rebuilt = transformChildren(applyVisitor(node, visitor, parent) ?? node, options, recurse);
285
- if (rebuilt === node) return node;
286
- const finalize = nodeFinalizers[rebuilt.kind];
287
- return finalize ? finalize(rebuilt) : rebuilt;
288
- }
496
+ const createArrowFunction = arrowFunctionDef.create;
289
497
  /**
290
- * Per-kind builders rerun after children are rebuilt. `Property`/`Parameter`
291
- * resync schema optionality against their `required` flag once the schema may
292
- * have changed.
498
+ * Definition for the {@link TextNode}.
293
499
  */
294
- const nodeFinalizers = {
295
- Property: (node) => createProperty(node),
296
- Parameter: (node) => createParameter(node)
297
- };
500
+ const textDef = defineNode({
501
+ kind: "Text",
502
+ build: (value) => ({ value })
503
+ });
298
504
  /**
299
- * Immutably rebuilds a node's children using {@link VISITOR_KEYS}, transforming
300
- * each child node and leaving non-node values (e.g. `additionalProperties: true`) intact.
301
- * `Schema` children are skipped in shallow mode.
505
+ * Creates a {@link TextNode} representing a raw string fragment in the source output.
506
+ *
507
+ * @example
508
+ * ```ts
509
+ * createText('return fetch(id)')
510
+ * // { kind: 'Text', value: 'return fetch(id)' }
511
+ * ```
302
512
  */
303
- function transformChildren(node, options, recurse) {
304
- if (node.kind === "Schema" && !recurse) return node;
305
- const keys = visitorKeysByKind[node.kind];
306
- if (!keys) return node;
307
- const record = node;
308
- const childOptions = {
309
- ...options,
310
- parent: node
311
- };
312
- let updates;
313
- for (const key of keys) {
314
- if (!(key in record)) continue;
315
- const value = record[key];
316
- if (Array.isArray(value)) {
317
- let changed = false;
318
- const mapped = value.map((item) => {
319
- if (!isNode(item)) return item;
320
- const next = transform(item, childOptions);
321
- if (next !== item) changed = true;
322
- return next;
323
- });
324
- if (changed) (updates ??= {})[key] = mapped;
325
- } else if (isNode(value)) {
326
- const next = transform(value, childOptions);
327
- if (next !== value) (updates ??= {})[key] = next;
328
- }
329
- }
330
- return updates ? {
331
- ...node,
332
- ...updates
333
- } : node;
334
- }
513
+ const createText = textDef.create;
335
514
  /**
336
- * Lazy depth-first collection pass. Yields every non-null value returned by
337
- * the visitor callbacks. Use `collect` for the eager array form.
515
+ * Definition for the {@link BreakNode}.
516
+ */
517
+ const breakDef = defineNode({
518
+ kind: "Break",
519
+ build: () => ({})
520
+ });
521
+ /**
522
+ * Creates a {@link BreakNode} representing a line break in the source output.
338
523
  *
339
- * @example Collect every operationId
524
+ * @example
340
525
  * ```ts
341
- * const ids: string[] = []
342
- * for (const id of collectLazy<string>(root, {
343
- * operation(node) {
344
- * return node.operationId
345
- * },
346
- * })) {
347
- * ids.push(id)
348
- * }
526
+ * createBreak()
527
+ * // { kind: 'Break' }
349
528
  * ```
350
529
  */
351
- function* collectLazy(node, options) {
352
- const { depth, parent, ...visitor } = options;
353
- const recurse = (depth ?? require_utils.visitorDepths.deep) === require_utils.visitorDepths.deep;
354
- const v = applyVisitor(node, visitor, parent);
355
- if (v != null) yield v;
356
- for (const child of getChildren(node, recurse)) yield* collectLazy(child, {
357
- ...options,
358
- parent: node
359
- });
530
+ function createBreak() {
531
+ return breakDef.create();
360
532
  }
361
533
  /**
362
- * Eager depth-first collection pass. Returns an array of every non-null value
363
- * the visitor callbacks return.
534
+ * Definition for the {@link JsxNode}.
535
+ */
536
+ const jsxDef = defineNode({
537
+ kind: "Jsx",
538
+ build: (value) => ({ value })
539
+ });
540
+ /**
541
+ * Creates a {@link JsxNode} representing a raw JSX fragment in the source output.
364
542
  *
365
- * @example Collect every operationId
543
+ * @example
366
544
  * ```ts
367
- * const ids = collect<string>(root, {
368
- * operation(node) {
369
- * return node.operationId
370
- * },
371
- * })
545
+ * createJsx('<>\n <a href={href}>Open</a>\n</>')
546
+ * // { kind: 'Jsx', value: '<>\n <a href={href}>Open</a>\n</>' }
372
547
  * ```
373
548
  */
374
- function collect(node, options) {
375
- return Array.from(collectLazy(node, options));
376
- }
549
+ const createJsx = jsxDef.create;
377
550
  //#endregion
378
- //#region src/utils/ast.ts
379
- const plainStringTypes = new Set([
380
- "string",
381
- "uuid",
382
- "email",
383
- "url",
384
- "datetime"
385
- ]);
551
+ //#region src/nodes/content.ts
386
552
  /**
387
- * Merges a ref node with its resolved schema, giving usage-site fields precedence.
388
- *
389
- * Usage-site fields (`description`, `readOnly`, `nullable`, `deprecated`) on the ref node
390
- * override the same fields in the resolved `node.schema`. Non-ref nodes are returned unchanged.
553
+ * Definition for the {@link ContentNode}.
554
+ */
555
+ const contentDef = defineNode({
556
+ kind: "Content",
557
+ children: ["schema"]
558
+ });
559
+ /**
560
+ * Creates a `ContentNode` for a single request-body or response content type.
561
+ */
562
+ const createContent = contentDef.create;
563
+ //#endregion
564
+ //#region src/nodes/file.ts
565
+ /**
566
+ * Definition for the {@link ImportNode}.
567
+ */
568
+ const importDef = defineNode({ kind: "Import" });
569
+ /**
570
+ * Creates an `ImportNode` representing a language-agnostic import/dependency declaration.
391
571
  *
392
- * @example
572
+ * @example Named import
393
573
  * ```ts
394
- * // Ref with description override
395
- * const ref = createSchema({ type: 'ref', ref: '#/components/schemas/Pet', description: 'A cute pet' })
396
- * const merged = syncSchemaRef(ref) // merges with resolved Pet schema
574
+ * createImport({ name: ['useState'], path: 'react' })
575
+ * // import { useState } from 'react'
397
576
  * ```
398
577
  */
399
- function syncSchemaRef(node) {
400
- const ref = narrowSchema(node, "ref");
401
- if (!ref) return node;
402
- if (!ref.schema) return node;
403
- const { kind: _kind, type: _type, name: _name, ref: _ref, schema: _schema, ...overrides } = ref;
404
- const definedOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== void 0));
405
- return createSchema({
406
- ...ref.schema,
407
- ...definedOverrides
408
- });
409
- }
578
+ const createImport = importDef.create;
410
579
  /**
411
- * Type guard that returns `true` when a schema emits as a plain `string` type.
412
- *
413
- * Covers `string`, `uuid`, `email`, `url`, and `datetime` types. For `date` and `time`
414
- * types, returns `true` only when `representation` is `'string'` rather than `'date'`.
580
+ * Definition for the {@link ExportNode}.
415
581
  */
416
- function isStringType(node) {
417
- if (plainStringTypes.has(node.type)) return true;
418
- const temporal = narrowSchema(node, "date") ?? narrowSchema(node, "time");
419
- if (temporal) return temporal.representation !== "date";
420
- return false;
421
- }
582
+ const exportDef = defineNode({ kind: "Export" });
422
583
  /**
423
- * Applies casing rules to parameter names and returns a new parameter array.
584
+ * Creates an `ExportNode` representing a language-agnostic export/public API declaration.
424
585
  *
425
- * Use this before passing parameters to schema builders so output property keys match
426
- * the desired casing while preserving `OperationNode.parameters` for other consumers.
427
- * The input array is not mutated. When `casing` is not set, the original array is returned unchanged.
586
+ * @example Named export
587
+ * ```ts
588
+ * createExport({ name: ['Pet'], path: './Pet' })
589
+ * // export { Pet } from './Pet'
590
+ * ```
428
591
  */
429
- const caseParamsMemo = memoize(/* @__PURE__ */ new WeakMap(), (params) => memoize(/* @__PURE__ */ new Map(), (casing) => params.map((param) => {
430
- const transformed = casing === "camelcase" || !require_utils.isValidVarName(param.name) ? require_utils.camelCase(param.name) : param.name;
431
- return {
432
- ...param,
433
- name: transformed
434
- };
435
- })));
436
- function caseParams(params, casing) {
437
- if (!casing) return params;
438
- return caseParamsMemo(params)(casing);
439
- }
592
+ const createExport = exportDef.create;
440
593
  /**
441
- * Creates a single-property object schema used as a discriminator literal.
594
+ * Definition for the {@link SourceNode}.
595
+ */
596
+ const sourceDef = defineNode({ kind: "Source" });
597
+ /**
598
+ * Creates a `SourceNode` representing a fragment of source code within a file.
442
599
  *
443
600
  * @example
444
601
  * ```ts
445
- * createDiscriminantNode({ propertyName: 'type', value: 'dog' })
446
- * // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
602
+ * createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')], isExportable: true })
447
603
  * ```
448
604
  */
449
- function createDiscriminantNode({ propertyName, value }) {
450
- return createSchema({
451
- type: "object",
452
- primitive: "object",
453
- properties: [createProperty({
454
- name: propertyName,
455
- schema: createSchema({
456
- type: "enum",
457
- primitive: "string",
458
- enumValues: [value]
459
- }),
460
- required: true
461
- })]
462
- });
463
- }
464
- function resolveParamsType({ node, param, resolver }) {
465
- if (!resolver) return createParamsType({
466
- variant: "reference",
467
- name: param.schema.primitive ?? "unknown"
468
- });
469
- const individualName = resolver.resolveParamName(node, param);
470
- const groupLocation = param.in === "path" || param.in === "query" || param.in === "header" ? param.in : void 0;
471
- const groupResolvers = {
472
- path: resolver.resolvePathParamsName,
473
- query: resolver.resolveQueryParamsName,
474
- header: resolver.resolveHeaderParamsName
475
- };
476
- const groupName = groupLocation ? groupResolvers[groupLocation].call(resolver, node, param) : void 0;
477
- if (groupName && groupName !== individualName) return createParamsType({
478
- variant: "member",
479
- base: groupName,
480
- key: param.name
481
- });
482
- return createParamsType({
483
- variant: "reference",
484
- name: individualName
485
- });
486
- }
605
+ const createSource = sourceDef.create;
487
606
  /**
488
- * Converts an `OperationNode` into function parameters for code generation.
489
- *
490
- * Centralizes parameter grouping logic for all plugins. Provide a `resolver` for type name resolution
491
- * and `extraParams` for plugin-specific trailing parameters (e.g., `options` objects).
492
- * Supports three grouping modes: `object` (single destructured param), `inline` (separate params),
493
- * and `inlineSpread` (rest parameter). Use `CreateOperationParamsOptions` to fine-tune output.
607
+ * Definition for the {@link FileNode}. The fully resolved builder lives in
608
+ * `createFile`, so this definition only supplies the guard.
494
609
  */
495
- function createOperationParams(node, options) {
496
- const { paramsType, pathParamsType, paramsCasing, resolver, pathParamsDefault, extraParams = [], paramNames, typeWrapper } = options;
497
- const dataName = paramNames?.data ?? "data";
498
- const paramsName = paramNames?.params ?? "params";
499
- const headersName = paramNames?.headers ?? "headers";
500
- const pathName = paramNames?.path ?? "pathParams";
501
- const wrapType = (type) => createParamsType({
502
- variant: "reference",
503
- name: typeWrapper ? typeWrapper(type) : type
504
- });
505
- const wrapTypeNode = (type) => type.kind === "ParamsType" && type.variant === "reference" ? wrapType(type.name) : type;
506
- const casedParams = caseParams(node.parameters, paramsCasing);
507
- const pathParams = casedParams.filter((p) => p.in === "path");
508
- const queryParams = casedParams.filter((p) => p.in === "query");
509
- const headerParams = casedParams.filter((p) => p.in === "header");
510
- const bodyType = node.requestBody?.content?.[0]?.schema ? wrapType(resolver?.resolveDataName(node) ?? "unknown") : void 0;
511
- const bodyRequired = node.requestBody?.required ?? false;
512
- const queryGroupType = resolver ? resolveGroupType({
513
- node,
514
- params: queryParams,
515
- groupMethod: resolver.resolveQueryParamsName,
516
- resolver
517
- }) : void 0;
518
- const headerGroupType = resolver ? resolveGroupType({
519
- node,
520
- params: headerParams,
521
- groupMethod: resolver.resolveHeaderParamsName,
522
- resolver
523
- }) : void 0;
524
- const params = [];
525
- if (paramsType === "object") {
526
- const children = [
527
- ...pathParams.map((p) => {
528
- const type = resolveParamsType({
529
- node,
530
- param: p,
531
- resolver
532
- });
533
- return createFunctionParameter({
534
- name: p.name,
535
- type: wrapTypeNode(type),
536
- optional: !p.required
537
- });
538
- }),
539
- ...bodyType ? [createFunctionParameter({
540
- name: dataName,
541
- type: bodyType,
542
- optional: !bodyRequired
543
- })] : [],
544
- ...buildGroupParam({
545
- name: paramsName,
546
- node,
547
- params: queryParams,
548
- groupType: queryGroupType,
549
- resolver,
550
- wrapType
551
- }),
552
- ...buildGroupParam({
553
- name: headersName,
554
- node,
555
- params: headerParams,
556
- groupType: headerGroupType,
557
- resolver,
558
- wrapType
559
- })
560
- ];
561
- if (children.length) params.push(createParameterGroup({
562
- properties: children,
563
- default: children.every((c) => c.optional) ? "{}" : void 0
564
- }));
565
- } else {
566
- if (pathParams.length) if (pathParamsType === "inlineSpread") {
567
- const spreadType = resolver?.resolvePathParamsName(node, pathParams[0]);
568
- params.push(createFunctionParameter({
569
- name: pathName,
570
- type: spreadType ? wrapType(spreadType) : void 0,
571
- rest: true
572
- }));
573
- } else {
574
- const pathChildren = pathParams.map((p) => {
575
- const type = resolveParamsType({
576
- node,
577
- param: p,
578
- resolver
579
- });
580
- return createFunctionParameter({
581
- name: p.name,
582
- type: wrapTypeNode(type),
583
- optional: !p.required
584
- });
585
- });
586
- params.push(createParameterGroup({
587
- properties: pathChildren,
588
- inline: pathParamsType === "inline",
589
- default: pathParamsDefault ?? (pathChildren.every((c) => c.optional) ? "{}" : void 0)
590
- }));
591
- }
592
- if (bodyType) params.push(createFunctionParameter({
593
- name: dataName,
594
- type: bodyType,
595
- optional: !bodyRequired
596
- }));
597
- params.push(...buildGroupParam({
598
- name: paramsName,
599
- node,
600
- params: queryParams,
601
- groupType: queryGroupType,
602
- resolver,
603
- wrapType
604
- }));
605
- params.push(...buildGroupParam({
606
- name: headersName,
607
- node,
608
- params: headerParams,
609
- groupType: headerGroupType,
610
- resolver,
611
- wrapType
612
- }));
613
- }
614
- params.push(...extraParams);
615
- return createFunctionParameters({ params });
616
- }
610
+ const fileDef = defineNode({ kind: "File" });
611
+ //#endregion
612
+ //#region src/nodes/function.ts
617
613
  /**
618
- * Builds a single {@link FunctionParameterNode} for a query or header group.
619
- * Returns an empty array when there are no params to emit.
614
+ * Definition for the {@link TypeLiteralNode}.
615
+ */
616
+ const typeLiteralDef = defineNode({ kind: "TypeLiteral" });
617
+ /**
618
+ * Creates a {@link TypeLiteralNode} representing an inline anonymous object type.
620
619
  *
621
- * If a pre-resolved `groupType` is provided it emits `name: GroupType`.
622
- * Otherwise, it builds an inline struct from the individual params.
620
+ * @example
621
+ * ```ts
622
+ * createTypeLiteral({ members: [{ name: 'petId', type: 'string', optional: false }] })
623
+ * // { petId: string }
624
+ * ```
623
625
  */
624
- function buildGroupParam({ name, node, params, groupType, resolver, wrapType }) {
625
- if (groupType) return [createFunctionParameter({
626
- name,
627
- type: groupType.type.kind === "ParamsType" && groupType.type.variant === "reference" ? wrapType(groupType.type.name) : groupType.type,
628
- optional: groupType.optional
629
- })];
630
- if (params.length) return [createFunctionParameter({
631
- name,
632
- type: toStructType({
633
- node,
634
- params,
635
- resolver
636
- }),
637
- optional: params.every((p) => !p.required)
638
- })];
639
- return [];
640
- }
626
+ const createTypeLiteral = typeLiteralDef.create;
641
627
  /**
642
- * Derives a {@link ParamGroupType} from the resolver's group method.
643
- * Returns `null` when the group name equals the individual param name (no real group).
628
+ * Definition for the {@link IndexedAccessTypeNode}.
644
629
  */
645
- function resolveGroupType({ node, params, groupMethod, resolver }) {
646
- if (!params.length) return null;
647
- const firstParam = params[0];
648
- const groupName = groupMethod.call(resolver, node, firstParam);
649
- if (groupName === resolver.resolveParamName(node, firstParam)) return null;
650
- const allOptional = params.every((p) => !p.required);
651
- return {
652
- type: createParamsType({
653
- variant: "reference",
654
- name: groupName
655
- }),
656
- optional: allOptional
657
- };
658
- }
630
+ const indexedAccessTypeDef = defineNode({ kind: "IndexedAccessType" });
659
631
  /**
660
- * Builds a {@link TypeNode} with `variant: 'struct'` for an inline anonymous type grouping named fields.
632
+ * Creates an {@link IndexedAccessTypeNode} representing a single field accessed from a named type.
661
633
  *
662
- * Used when query or header parameters have no dedicated group type name.
663
- * Each language printer renders this appropriately (TypeScript: `{ petId: string; name?: string }`).
664
- */
665
- function toStructType({ node, params, resolver }) {
666
- return createParamsType({
667
- variant: "struct",
668
- properties: params.map((p) => ({
669
- name: p.name,
670
- optional: !p.required,
671
- type: resolveParamsType({
672
- node,
673
- param: p,
674
- resolver
675
- })
676
- }))
677
- });
678
- }
679
- function sourceKey(source) {
680
- return `${source.name ?? extractStringsFromNodes(source.nodes)}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}`;
681
- }
682
- function pathTypeKey(path, isTypeOnly) {
683
- return `${path}:${isTypeOnly ?? false}`;
684
- }
685
- function exportKey(path, name, isTypeOnly, asAlias) {
686
- return `${path}:${name ?? ""}:${isTypeOnly ?? false}:${asAlias ?? ""}`;
687
- }
688
- function importKey(path, name, isTypeOnly) {
689
- return `${path}:${name ?? ""}:${isTypeOnly ?? false}`;
690
- }
691
- /**
692
- * Computes a multi-level sort key for exports and imports:
693
- * non-array names first (wildcards/namespace aliases). Type-only before value. Alphabetical path. Unnamed before named.
634
+ * @example
635
+ * ```ts
636
+ * createIndexedAccessType({ objectType: 'DeletePetPathParams', indexType: 'petId' })
637
+ * // DeletePetPathParams['petId']
638
+ * ```
694
639
  */
695
- function sortKey(node) {
696
- const isArray = Array.isArray(node.name) ? "1" : "0";
697
- const typeOnly = node.isTypeOnly ? "0" : "1";
698
- const hasName = node.name != null ? "1" : "0";
699
- const name = Array.isArray(node.name) ? node.name.toSorted().join("\0") : node.name ?? "";
700
- return `${isArray}:${typeOnly}:${node.path}:${hasName}:${name}`;
701
- }
640
+ const createIndexedAccessType = indexedAccessTypeDef.create;
702
641
  /**
703
- * Deduplicates and merges `SourceNode` objects by `name + isExportable + isTypeOnly`.
704
- *
705
- * Unnamed sources are deduplicated by object reference. Returns a deduplicated array in original order.
642
+ * Definition for the {@link ObjectBindingPatternNode}.
706
643
  */
707
- function combineSources(sources) {
708
- const seen = /* @__PURE__ */ new Map();
709
- for (const source of sources) {
710
- const key = sourceKey(source);
711
- if (!seen.has(key)) seen.set(key, source);
712
- }
713
- return [...seen.values()];
714
- }
644
+ const objectBindingPatternDef = defineNode({ kind: "ObjectBindingPattern" });
715
645
  /**
716
- * Merges `incoming` names into `existing`, preserving order and dropping duplicates.
646
+ * Creates an {@link ObjectBindingPatternNode} for a destructured parameter binding.
717
647
  *
718
- * Shared by `combineExports` and `combineImports` for the same-path name-merge case.
648
+ * @example
649
+ * ```ts
650
+ * createObjectBindingPattern({ elements: [{ name: 'id' }, { name: 'name' }] })
651
+ * // { id, name }
652
+ * ```
719
653
  */
720
- function mergeNameArrays(existing, incoming) {
721
- const merged = new Set(existing);
722
- for (const name of incoming) merged.add(name);
723
- return [...merged];
724
- }
654
+ const createObjectBindingPattern = objectBindingPatternDef.create;
725
655
  /**
726
- * Deduplicates and merges `ExportNode` objects by path and type.
727
- *
728
- * Named exports with the same path and `isTypeOnly` flag have their names merged into a single export.
729
- * Non-array exports are deduplicated by exact identity. Returns a sorted, deduplicated array.
656
+ * Definition for the {@link FunctionParameterNode}. `optional` defaults to `false`.
657
+ * Passing `properties` builds a destructured group: an {@link ObjectBindingPatternNode} name
658
+ * paired with a {@link TypeLiteralNode} type.
730
659
  */
731
- function combineExports(exports) {
732
- const result = [];
733
- const namedByPath = /* @__PURE__ */ new Map();
734
- const seen = /* @__PURE__ */ new Set();
735
- const keyed = exports.map((node) => ({
736
- node,
737
- key: sortKey(node)
738
- }));
739
- keyed.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
740
- for (const { node: curr } of keyed) {
741
- const { name, path, isTypeOnly, asAlias } = curr;
742
- if (Array.isArray(name)) {
743
- if (!name.length) continue;
744
- const key = pathTypeKey(path, isTypeOnly);
745
- const existing = namedByPath.get(key);
746
- if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
747
- else {
748
- const newItem = {
749
- ...curr,
750
- name: [...new Set(name)]
751
- };
752
- result.push(newItem);
753
- namedByPath.set(key, newItem);
754
- }
755
- } else {
756
- const key = exportKey(path, name, isTypeOnly, asAlias);
757
- if (!seen.has(key)) {
758
- result.push(curr);
759
- seen.add(key);
760
- }
761
- }
660
+ const functionParameterDef = defineNode({
661
+ kind: "FunctionParameter",
662
+ build: (input) => {
663
+ if ("properties" in input) return {
664
+ name: createObjectBindingPattern({ elements: input.properties.map((p) => ({ name: p.name })) }),
665
+ type: createTypeLiteral({ members: input.properties.map((p) => ({
666
+ name: p.name,
667
+ type: p.type,
668
+ optional: p.optional ?? false
669
+ })) }),
670
+ optional: input.optional ?? false,
671
+ ...input.default !== void 0 ? { default: input.default } : {}
672
+ };
673
+ return {
674
+ optional: false,
675
+ ...input
676
+ };
762
677
  }
763
- return result;
764
- }
678
+ });
765
679
  /**
766
- * Deduplicates and merges `ImportNode` objects, filtering out unused imports.
680
+ * Creates a `FunctionParameterNode`. `optional` defaults to `false`.
767
681
  *
768
- * Retains imports that are referenced in `source` or re-exported. Imports with the same path and
769
- * `isTypeOnly` flag have their names merged. Returns a sorted, deduplicated, filtered array.
682
+ * @example Optional param
683
+ * ```ts
684
+ * createFunctionParameter({ name: 'params', type: 'QueryParams', optional: true })
685
+ * // → params?: QueryParams
686
+ * ```
770
687
  *
771
- * @note Use this when combining imports from multiple files to avoid duplicate declarations.
688
+ * @example Destructured group
689
+ * ```ts
690
+ * createFunctionParameter({ properties: [{ name: 'id', type: 'string' }, { name: 'name', type: 'string', optional: true }], default: '{}' })
691
+ * // → { id, name }: { id: string; name?: string } = {}
692
+ * ```
772
693
  */
773
- function combineImports(imports, exports, source) {
774
- const exportedNames = new Set(exports.flatMap((e) => Array.isArray(e.name) ? e.name : e.name ? [e.name] : []));
775
- const isUsed = (importName) => !source || source.includes(importName) || exportedNames.has(importName);
776
- const importNameMemo = /* @__PURE__ */ new Map();
777
- const canonicalizeName = (n) => {
778
- if (typeof n === "string") return n;
779
- const key = `${n.propertyName}:${n.name ?? ""}`;
780
- if (!importNameMemo.has(key)) importNameMemo.set(key, n);
781
- return importNameMemo.get(key);
782
- };
783
- const pathsWithUsedNamedImport = /* @__PURE__ */ new Set();
784
- for (const node of imports) {
785
- if (!Array.isArray(node.name)) continue;
786
- if (node.name.some((item) => typeof item === "string" ? isUsed(item) : isUsed(item.name ?? item.propertyName))) pathsWithUsedNamedImport.add(node.path);
787
- }
788
- const result = [];
789
- const namedByPath = /* @__PURE__ */ new Map();
790
- const seen = /* @__PURE__ */ new Set();
791
- const keyed = imports.map((node) => ({
792
- node,
793
- key: sortKey(node)
794
- }));
795
- keyed.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
796
- for (const { node: curr } of keyed) {
797
- if (curr.path === curr.root) continue;
798
- const { path, isTypeOnly } = curr;
799
- let { name } = curr;
800
- if (Array.isArray(name)) {
801
- name = [...new Set(name.map(canonicalizeName))].filter((item) => typeof item === "string" ? isUsed(item) : isUsed(item.name ?? item.propertyName));
802
- if (!name.length) continue;
803
- const key = pathTypeKey(path, isTypeOnly);
804
- const existing = namedByPath.get(key);
805
- if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
806
- else {
807
- const newItem = {
808
- ...curr,
809
- name
810
- };
811
- result.push(newItem);
812
- namedByPath.set(key, newItem);
813
- }
814
- } else {
815
- if (name && !isUsed(name) && !pathsWithUsedNamedImport.has(path)) continue;
816
- const key = importKey(path, name, isTypeOnly);
817
- if (!seen.has(key)) {
818
- result.push(curr);
819
- seen.add(key);
820
- }
821
- }
822
- }
823
- return result;
824
- }
694
+ const createFunctionParameter = functionParameterDef.create;
825
695
  /**
826
- * Extracts all string content from a `CodeNode` tree recursively.
827
- *
828
- * Collects text node values, identifier references in string fields (`params`, `generics`, `returnType`, `type`),
829
- * and nested node content. Used internally to build the full source string for import filtering.
696
+ * Definition for the {@link FunctionParametersNode}.
830
697
  */
831
- function extractStringsFromNodes(nodes) {
832
- if (!nodes?.length) return "";
833
- return nodes.map((node) => {
834
- if (typeof node === "string") return node;
835
- if (node.kind === "Text") return node.value;
836
- if (node.kind === "Break") return "";
837
- if (node.kind === "Jsx") return node.value;
838
- const parts = [];
839
- if ("params" in node && node.params) parts.push(node.params);
840
- if ("generics" in node && node.generics) parts.push(Array.isArray(node.generics) ? node.generics.join(", ") : node.generics);
841
- if ("returnType" in node && node.returnType) parts.push(node.returnType);
842
- if ("type" in node && typeof node.type === "string") parts.push(node.type);
843
- const nested = extractStringsFromNodes(node.nodes);
844
- if (nested) parts.push(nested);
845
- return parts.join("\n");
846
- }).filter(Boolean).join("\n");
847
- }
698
+ const functionParametersDef = defineNode({
699
+ kind: "FunctionParameters",
700
+ defaults: { params: [] }
701
+ });
848
702
  /**
849
- * Resolves the schema name of a ref node, falling back through `ref` `name` nested `schema.name`.
850
- *
851
- * Returns `null` for non-ref nodes or when no name can be resolved. Use this to get a schema's
852
- * identifier for type definitions or error messages.
703
+ * Creates a `FunctionParametersNode` from an ordered list of parameters.
853
704
  *
854
705
  * @example
855
706
  * ```ts
856
- * resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Pet' })
857
- * // => 'Pet'
707
+ * const empty = createFunctionParameters()
708
+ * // { kind: 'FunctionParameters', params: [] }
858
709
  * ```
859
710
  */
860
- function resolveRefName(node) {
861
- if (!node || node.type !== "ref") return null;
862
- if (node.ref) return require_utils.extractRefName(node.ref) ?? node.name ?? node.schema?.name ?? null;
863
- return node.name ?? node.schema?.name ?? null;
711
+ function createFunctionParameters(props = {}) {
712
+ return functionParametersDef.create(props);
864
713
  }
714
+ //#endregion
715
+ //#region src/nodes/input.ts
865
716
  /**
866
- * Collects every named schema referenced (transitively) from a node via ref edges.
867
- *
868
- * Refs are followed by name only, the resolved `node.schema` is not traversed inline.
869
- * Use this to determine schema dependencies, build reference graphs, or detect what schemas need to be emitted.
870
- *
871
- * @example Collect refs from a single schema
872
- * ```ts
873
- * const names = collectReferencedSchemaNames(petSchema)
874
- * // → Set { 'Category', 'Tag' }
875
- * ```
876
- *
877
- * @example Accumulate refs from multiple schemas into one set
878
- * ```ts
879
- * const out = new Set<string>()
880
- * for (const schema of schemas) {
881
- * collectReferencedSchemaNames(schema, out)
882
- * }
883
- * ```
717
+ * Definition for the {@link InputNode}.
884
718
  */
885
- const collectSchemaRefs = memoize(/* @__PURE__ */ new WeakMap(), (node) => {
886
- const refs = /* @__PURE__ */ new Set();
887
- collect(node, { schema(child) {
888
- if (child.type === "ref") {
889
- const name = resolveRefName(child);
890
- if (name) refs.add(name);
719
+ const inputDef = defineNode({
720
+ kind: "Input",
721
+ defaults: {
722
+ schemas: [],
723
+ operations: [],
724
+ meta: {
725
+ circularNames: [],
726
+ enumNames: []
891
727
  }
892
- } });
893
- return refs;
728
+ },
729
+ children: ["schemas", "operations"],
730
+ visitorKey: "input"
894
731
  });
895
- function collectReferencedSchemaNames(node, out = /* @__PURE__ */ new Set()) {
896
- if (!node) return out;
897
- for (const name of collectSchemaRefs(node)) out.add(name);
898
- return out;
899
- }
900
732
  /**
901
- * Collects the names of all top-level schemas transitively used by a set of operations.
902
- *
903
- * An operation uses a schema when any of its parameters, request body content, or responses
904
- * reference it, directly or indirectly through other named schemas.
905
- * The walk is iterative and safe against reference cycles.
906
- *
907
- * Use this together with `include` filters to determine which schemas from `components/schemas`
908
- * are reachable from the allowed operations, so that schemas used only by excluded operations
909
- * are not generated.
733
+ * Creates an `InputNode` with stable defaults for `schemas` and `operations`.
910
734
  *
911
- * @example Only generate schemas referenced by included operations
735
+ * @example
912
736
  * ```ts
913
- * const includedOps = operations.filter(op => resolver.resolveOptions(op, { options, include }) !== null)
914
- * const allowed = collectUsedSchemaNames(includedOps, schemas)
915
- *
916
- * for (const schema of schemas) {
917
- * if (schema.name && !allowed.has(schema.name)) continue
918
- * // … generate schema
919
- * }
737
+ * const input = createInput()
738
+ * // { kind: 'Input', schemas: [], operations: [] }
920
739
  * ```
740
+ */
741
+ function createInput(overrides = {}) {
742
+ return inputDef.create(overrides);
743
+ }
744
+ /**
745
+ * Creates a streaming `InputNode<true>` from pre-built `AsyncIterable` sources.
921
746
  *
922
- * @example Check whether a specific schema is needed
747
+ * @example
923
748
  * ```ts
924
- * const allowed = collectUsedSchemaNames(includedOps, schemas)
925
- * allowed.has('OrderStatus') // false when no included operation references OrderStatus
749
+ * const node = createStreamInput(schemasIterable, operationsIterable, { title: 'My API' })
926
750
  * ```
927
751
  */
928
- const collectUsedSchemaNamesMemo = memoize(/* @__PURE__ */ new WeakMap(), (ops) => memoize(/* @__PURE__ */ new WeakMap(), (schemas) => computeUsedSchemaNames(ops, schemas)));
929
- function computeUsedSchemaNames(operations, schemas) {
930
- const schemaMap = /* @__PURE__ */ new Map();
931
- for (const schema of schemas) if (schema.name) schemaMap.set(schema.name, schema);
932
- const result = /* @__PURE__ */ new Set();
933
- function visitSchema(schema) {
934
- const directRefs = collectReferencedSchemaNames(schema);
935
- for (const name of directRefs) if (!result.has(name)) {
936
- result.add(name);
937
- const namedSchema = schemaMap.get(name);
938
- if (namedSchema) visitSchema(namedSchema);
939
- }
940
- }
941
- for (const op of operations) for (const schema of collectLazy(op, {
942
- depth: "shallow",
943
- schema: (node) => node
944
- })) visitSchema(schema);
945
- return result;
946
- }
947
- function collectUsedSchemaNames(operations, schemas) {
948
- return collectUsedSchemaNamesMemo(operations)(schemas);
752
+ function createStreamInput(schemas, operations, meta) {
753
+ return {
754
+ kind: "Input",
755
+ schemas,
756
+ operations,
757
+ meta
758
+ };
949
759
  }
950
- const EMPTY_CIRCULAR_SET = /* @__PURE__ */ new Set();
951
- const findCircularSchemasMemo = memoize(/* @__PURE__ */ new WeakMap(), (schemas) => {
952
- const graph = /* @__PURE__ */ new Map();
953
- for (const schema of schemas) {
954
- if (!schema.name) continue;
955
- graph.set(schema.name, collectReferencedSchemaNames(schema));
956
- }
957
- const circular = /* @__PURE__ */ new Set();
958
- for (const start of graph.keys()) {
959
- const visited = /* @__PURE__ */ new Set();
960
- const stack = [...graph.get(start) ?? []];
961
- while (stack.length > 0) {
962
- const node = stack.pop();
963
- if (node === start) {
964
- circular.add(start);
965
- break;
966
- }
967
- if (visited.has(node)) continue;
968
- visited.add(node);
969
- const next = graph.get(node);
970
- if (next) for (const r of next) stack.push(r);
971
- }
972
- }
973
- return circular;
974
- });
760
+ //#endregion
761
+ //#region src/nodes/requestBody.ts
975
762
  /**
976
- * Identifies all schemas that participate in circular dependency chains, including direct self-loops.
977
- *
978
- * Returns a Set of schema names with circular dependencies. Use this to wrap recursive schema positions
979
- * in deferred constructs (lazy getter, `z.lazy(() => …)`) to prevent infinite recursion when generated code runs.
980
- * Refs are followed by name only, keeping the algorithm linear in the schema graph size.
981
- *
982
- * @note Call this once on the full schema graph, then use `containsCircularRef()` to check individual schemas.
763
+ * Definition for the {@link RequestBodyNode}, normalizing each content entry into a `ContentNode`.
983
764
  */
984
- function findCircularSchemas(schemas) {
985
- if (schemas.length === 0) return EMPTY_CIRCULAR_SET;
986
- return findCircularSchemasMemo(schemas);
987
- }
765
+ const requestBodyDef = defineNode({
766
+ kind: "RequestBody",
767
+ build: (props) => ({
768
+ ...props,
769
+ content: props.content?.map(createContent)
770
+ }),
771
+ children: ["content"]
772
+ });
988
773
  /**
989
- * Type guard returning `true` when a schema or anything nested within it contains a ref to a circular schema.
990
- *
991
- * Use `excludeName` to ignore refs to specific schemas (useful when self-references are handled separately).
992
- * Commonly used with `findCircularSchemas()` to detect where lazy wrappers are needed in code generation.
993
- *
994
- * @note Returns `true` for the first matching circular ref found. Use for fast dependency checks.
774
+ * Creates a `RequestBodyNode`, normalizing each content entry into a `ContentNode`.
995
775
  */
996
- function containsCircularRef(node, { circularSchemas, excludeName }) {
997
- if (!node || circularSchemas.size === 0) return false;
998
- for (const _ of collectLazy(node, { schema(child) {
999
- if (child.type !== "ref") return null;
1000
- const name = resolveRefName(child);
1001
- return name && name !== excludeName && circularSchemas.has(name) ? true : null;
1002
- } })) return true;
1003
- return false;
776
+ const createRequestBody = requestBodyDef.create;
777
+ //#endregion
778
+ //#region src/nodes/operation.ts
779
+ /**
780
+ * Definition for the {@link OperationNode}. HTTP operations (those carrying both
781
+ * `method` and `path`) are tagged with `protocol: 'http'`, and the request body is
782
+ * normalized into a `RequestBodyNode`.
783
+ */
784
+ const operationDef = defineNode({
785
+ kind: "Operation",
786
+ build: (props) => {
787
+ const { requestBody, ...rest } = props;
788
+ const isHttp = rest.method !== void 0 && rest.path !== void 0;
789
+ return {
790
+ tags: [],
791
+ parameters: [],
792
+ responses: [],
793
+ ...rest,
794
+ ...isHttp ? { protocol: "http" } : {},
795
+ requestBody: requestBody ? createRequestBody(requestBody) : void 0
796
+ };
797
+ },
798
+ children: [
799
+ "parameters",
800
+ "requestBody",
801
+ "responses"
802
+ ],
803
+ visitorKey: "operation"
804
+ });
805
+ function createOperation(props) {
806
+ return operationDef.create(props);
1004
807
  }
1005
808
  //#endregion
1006
- //#region src/factory.ts
809
+ //#region src/nodes/output.ts
1007
810
  /**
1008
- * Updates a schema's `optional` and `nullish` flags from a parent's `required`
1009
- * value and the schema's own `nullable`. Mirrors how OpenAPI parameters and
1010
- * object properties combine "required" and "nullable" into a single AST.
1011
- *
1012
- * - Non-required + non-nullable → `optional: true`.
1013
- * - Non-required + nullable → `nullish: true`.
1014
- * - Required → both flags cleared.
811
+ * Definition for the {@link OutputNode}.
1015
812
  */
1016
- function syncOptionality(schema, required) {
1017
- const nullable = schema.nullable ?? false;
1018
- return {
1019
- ...schema,
1020
- optional: !required && !nullable ? true : void 0,
1021
- nullish: !required && nullable ? true : void 0
1022
- };
1023
- }
813
+ const outputDef = defineNode({
814
+ kind: "Output",
815
+ defaults: { files: [] },
816
+ visitorKey: "output"
817
+ });
1024
818
  /**
1025
- * Identity-preserving node update: returns `node` unchanged when every field in
1026
- * `changes` already equals (by reference) the current value, otherwise a new node
1027
- * with the changes applied.
1028
- *
1029
- * Mirrors the TypeScript compiler's `factory.updateX` contract, pair it with the
1030
- * structural sharing in {@link transform} so a no-op rewrite doesn't allocate and
1031
- * downstream passes can detect "nothing changed" by identity. Comparison is
1032
- * shallow: a structurally-equal but newly-allocated array/object counts as a change.
819
+ * Creates an `OutputNode` with a stable default for `files`.
1033
820
  *
1034
821
  * @example
1035
822
  * ```ts
1036
- * update(node, { name: node.name }) // -> same `node` reference
1037
- * update(node, { name: 'renamed' }) // -> new node, `name` replaced
823
+ * const output = createOutput()
824
+ * // { kind: 'Output', files: [] }
1038
825
  * ```
1039
826
  */
1040
- function update(node, changes) {
1041
- for (const key in changes) if (changes[key] !== node[key]) return {
1042
- ...node,
1043
- ...changes
1044
- };
1045
- return node;
827
+ function createOutput(overrides = {}) {
828
+ return outputDef.create(overrides);
1046
829
  }
830
+ //#endregion
831
+ //#region src/nodes/parameter.ts
832
+ /**
833
+ * Definition for the {@link ParameterNode}. `required` defaults to `false` and the
834
+ * schema's `optional`/`nullish` flags are kept in sync with it.
835
+ */
836
+ const parameterDef = defineNode({
837
+ kind: "Parameter",
838
+ build: (props) => {
839
+ const required = props.required ?? false;
840
+ return {
841
+ ...props,
842
+ required,
843
+ schema: syncOptionality(props.schema, required)
844
+ };
845
+ },
846
+ children: ["schema"],
847
+ visitorKey: "parameter",
848
+ rebuild: true
849
+ });
1047
850
  /**
1048
- * Creates an `InputNode` with stable defaults for `schemas` and `operations`.
851
+ * Creates a `ParameterNode`.
1049
852
  *
1050
853
  * @example
1051
854
  * ```ts
1052
- * const input = createInput()
1053
- * // { kind: 'Input', schemas: [], operations: [] }
855
+ * const param = createParameter({
856
+ * name: 'petId',
857
+ * in: 'path',
858
+ * required: true,
859
+ * schema: createSchema({ type: 'string' }),
860
+ * })
1054
861
  * ```
862
+ */
863
+ const createParameter = parameterDef.create;
864
+ //#endregion
865
+ //#region src/nodes/property.ts
866
+ /**
867
+ * Definition for the {@link PropertyNode}. `required` defaults to `false` and the
868
+ * schema's `optional`/`nullish` flags are kept in sync with it.
869
+ */
870
+ const propertyDef = defineNode({
871
+ kind: "Property",
872
+ build: (props) => {
873
+ const required = props.required ?? false;
874
+ return {
875
+ ...props,
876
+ required,
877
+ schema: syncOptionality(props.schema, required)
878
+ };
879
+ },
880
+ children: ["schema"],
881
+ visitorKey: "property",
882
+ rebuild: true
883
+ });
884
+ /**
885
+ * Creates a `PropertyNode`.
1055
886
  *
1056
887
  * @example
1057
888
  * ```ts
1058
- * const input = createInput({ schemas: [petSchema] })
1059
- * // keeps default operations: []
889
+ * const property = createProperty({
890
+ * name: 'status',
891
+ * required: true,
892
+ * schema: createSchema({ type: 'string', nullable: true }),
893
+ * })
894
+ * // required=true, no optional/nullish
1060
895
  * ```
1061
896
  */
1062
- function createInput(overrides = {}) {
1063
- return {
1064
- schemas: [],
1065
- operations: [],
1066
- meta: {
1067
- circularNames: [],
1068
- enumNames: []
1069
- },
1070
- ...overrides,
1071
- kind: "Input"
1072
- };
1073
- }
897
+ const createProperty = propertyDef.create;
898
+ //#endregion
899
+ //#region src/nodes/response.ts
900
+ /**
901
+ * Definition for the {@link ResponseNode}. A single legacy `schema` (with optional
902
+ * `mediaType`/`keysToOmit`) is normalized into one `content` entry.
903
+ */
904
+ const responseDef = defineNode({
905
+ kind: "Response",
906
+ build: (props) => {
907
+ const { schema, mediaType, keysToOmit, content, ...rest } = props;
908
+ const entries = content ?? (schema ? [{
909
+ contentType: mediaType ?? "application/json",
910
+ schema,
911
+ keysToOmit: keysToOmit ?? null
912
+ }] : void 0);
913
+ return {
914
+ ...rest,
915
+ content: entries?.map(createContent)
916
+ };
917
+ },
918
+ children: ["content"],
919
+ visitorKey: "response"
920
+ });
1074
921
  /**
1075
- * Creates a streaming `InputNode<true>` from pre-built `AsyncIterable` sources.
922
+ * Creates a `ResponseNode`.
1076
923
  *
1077
924
  * @example
1078
925
  * ```ts
1079
- * const node = createStreamInput(schemasIterable, operationsIterable, { title: 'My API' })
926
+ * const response = createResponse({
927
+ * statusCode: '200',
928
+ * content: [{ contentType: 'application/json', schema: createSchema({ type: 'object', properties: [] }) }],
929
+ * })
1080
930
  * ```
1081
931
  */
1082
- function createStreamInput(schemas, operations, meta) {
1083
- return {
1084
- kind: "Input",
1085
- schemas,
1086
- operations,
1087
- meta
1088
- };
1089
- }
932
+ const createResponse = responseDef.create;
933
+ //#endregion
934
+ //#region src/registry.ts
935
+ /**
936
+ * Every node definition. Adding a node means adding its `defineNode` to one
937
+ * `nodes/*.ts` file and listing it here. The visitor tables below derive from it.
938
+ */
939
+ const nodeDefs = [
940
+ inputDef,
941
+ outputDef,
942
+ operationDef,
943
+ requestBodyDef,
944
+ contentDef,
945
+ responseDef,
946
+ schemaDef,
947
+ propertyDef,
948
+ parameterDef,
949
+ functionParameterDef,
950
+ functionParametersDef,
951
+ typeLiteralDef,
952
+ indexedAccessTypeDef,
953
+ objectBindingPatternDef,
954
+ constDef,
955
+ typeDef,
956
+ functionDef,
957
+ arrowFunctionDef,
958
+ textDef,
959
+ breakDef,
960
+ jsxDef,
961
+ importDef,
962
+ exportDef,
963
+ sourceDef,
964
+ fileDef
965
+ ];
1090
966
  /**
1091
- * Creates an `OutputNode` with a stable default for `files`.
967
+ * Child node fields per node kind, in traversal order (Babel's `VISITOR_KEYS`).
968
+ * Derived from each definition's `children`.
969
+ */
970
+ const VISITOR_KEYS = Object.fromEntries(nodeDefs.flatMap((def) => def.children ? [[def.kind, def.children]] : []));
971
+ /**
972
+ * Maps a node kind to the matching visitor callback name. Derived from each
973
+ * definition's `visitorKey`.
974
+ */
975
+ const VISITOR_KEY_BY_KIND = Object.fromEntries(nodeDefs.flatMap((def) => def.visitorKey ? [[def.kind, def.visitorKey]] : []));
976
+ /**
977
+ * Per-kind builders rerun after children are rebuilt. Derived from each
978
+ * definition's `rebuild` flag.
979
+ */
980
+ const nodeRebuilders = Object.fromEntries(nodeDefs.flatMap((def) => def.rebuild ? [[def.kind, def.create]] : []));
981
+ //#endregion
982
+ //#region src/visitor.ts
983
+ /**
984
+ * Creates a small async concurrency limiter.
1092
985
  *
1093
- * @example
1094
- * ```ts
1095
- * const output = createOutput()
1096
- * // { kind: 'Output', files: [] }
1097
- * ```
986
+ * At most `concurrency` tasks are in flight at once. Extra tasks are queued.
1098
987
  *
1099
988
  * @example
1100
989
  * ```ts
1101
- * const output = createOutput({ files: [petFile] })
990
+ * const limit = createLimit(2)
991
+ * for (const task of [taskA, taskB, taskC]) {
992
+ * await limit(() => task())
993
+ * }
994
+ * // only 2 tasks run at the same time
1102
995
  * ```
1103
996
  */
1104
- function createOutput(overrides = {}) {
1105
- return {
1106
- files: [],
1107
- ...overrides,
1108
- kind: "Output"
997
+ function createLimit(concurrency) {
998
+ let active = 0;
999
+ const queue = [];
1000
+ function next() {
1001
+ if (active < concurrency && queue.length > 0) {
1002
+ active++;
1003
+ queue.shift()();
1004
+ }
1005
+ }
1006
+ return function limit(fn) {
1007
+ return new Promise((resolve, reject) => {
1008
+ queue.push(() => {
1009
+ Promise.resolve(fn()).then(resolve, reject).finally(() => {
1010
+ active--;
1011
+ next();
1012
+ });
1013
+ });
1014
+ next();
1015
+ });
1109
1016
  };
1110
1017
  }
1018
+ const visitorKeysByKind = VISITOR_KEYS;
1111
1019
  /**
1112
- * Creates a `ContentNode` for a single request-body or response content type.
1020
+ * Returns `true` when `value` is an AST node (an object carrying a `kind`).
1113
1021
  */
1114
- function createContent(props) {
1115
- return {
1116
- ...props,
1117
- kind: "Content"
1118
- };
1022
+ function isNode(value) {
1023
+ return typeof value === "object" && value !== null && "kind" in value;
1119
1024
  }
1120
1025
  /**
1121
- * Creates a `RequestBodyNode`, normalizing each content entry into a `ContentNode`.
1026
+ * Returns the immediate traversable children of `node` based on {@link VISITOR_KEYS}.
1027
+ *
1028
+ * `Schema` children are only included when `recurse` is `true`. Shallow mode skips them.
1029
+ *
1030
+ * @example
1031
+ * ```ts
1032
+ * const children = getChildren(operationNode, true)
1033
+ * // returns parameters, the request body, and responses
1034
+ * ```
1122
1035
  */
1123
- function createRequestBody(props) {
1124
- return {
1125
- ...props,
1126
- kind: "RequestBody",
1127
- content: props.content?.map(createContent)
1128
- };
1129
- }
1130
- function createOperation(props) {
1131
- const { requestBody, ...rest } = props;
1132
- const isHttp = rest.method !== void 0 && rest.path !== void 0;
1133
- return {
1134
- tags: [],
1135
- parameters: [],
1136
- responses: [],
1137
- ...rest,
1138
- ...isHttp ? { protocol: "http" } : {},
1139
- kind: "Operation",
1140
- requestBody: requestBody ? createRequestBody(requestBody) : void 0
1141
- };
1142
- }
1143
- /**
1144
- * Maps schema `type` to its underlying `primitive`.
1145
- * Primitive types map to themselves. Special string formats map to `'string'`.
1146
- * Complex types (`ref`, `enum`, `union`, `intersection`, `tuple`, `blob`) are left unset.
1147
- */
1148
- const TYPE_TO_PRIMITIVE = {
1149
- string: "string",
1150
- number: "number",
1151
- integer: "integer",
1152
- bigint: "bigint",
1153
- boolean: "boolean",
1154
- null: "null",
1155
- any: "any",
1156
- unknown: "unknown",
1157
- void: "void",
1158
- never: "never",
1159
- object: "object",
1160
- array: "array",
1161
- date: "date",
1162
- uuid: "string",
1163
- email: "string",
1164
- url: "string",
1165
- datetime: "string",
1166
- time: "string"
1167
- };
1168
- function createSchema(props) {
1169
- const inferredPrimitive = TYPE_TO_PRIMITIVE[props.type];
1170
- if (props["type"] === "object") return {
1171
- properties: [],
1172
- primitive: "object",
1173
- ...props,
1174
- kind: "Schema"
1175
- };
1176
- return {
1177
- primitive: inferredPrimitive,
1178
- ...props,
1179
- kind: "Schema"
1180
- };
1036
+ function* getChildren(node, recurse) {
1037
+ if (node.kind === "Schema" && !recurse) return;
1038
+ const keys = visitorKeysByKind[node.kind];
1039
+ if (!keys) return;
1040
+ const record = node;
1041
+ for (const key of keys) {
1042
+ const value = record[key];
1043
+ if (Array.isArray(value)) {
1044
+ for (const item of value) if (isNode(item)) yield item;
1045
+ } else if (isNode(value)) yield value;
1046
+ }
1181
1047
  }
1182
1048
  /**
1183
- * Creates a `PropertyNode`.
1184
- *
1185
- * `required` defaults to `false`.
1186
- * `schema.optional` and `schema.nullish` are derived from `required` and `schema.nullable`.
1187
- *
1188
- * @example
1189
- * ```ts
1190
- * const property = createProperty({
1191
- * name: 'status',
1192
- * schema: createSchema({ type: 'string' }),
1193
- * })
1194
- * // required=false, schema.optional=true
1195
- * ```
1049
+ * Invokes the visitor callback that matches `node.kind`, passing the traversal
1050
+ * context. Returns the callback's result (a replacement node, a collected
1051
+ * value, or `undefined` when no callback is registered for the kind).
1196
1052
  *
1197
- * @example
1198
- * ```ts
1199
- * const property = createProperty({
1200
- * name: 'status',
1201
- * required: true,
1202
- * schema: createSchema({ type: 'string', nullable: true }),
1203
- * })
1204
- * // required=true, no optional/nullish
1205
- * ```
1053
+ * Shared by `walk`, `transform`, and `collectLazy` so node-kind dispatch lives
1054
+ * in one place. `TResult` is the caller's expected return: the same node type
1055
+ * for `transform`, the collected value type for `collectLazy`, ignored for `walk`.
1206
1056
  */
1207
- function createProperty(props) {
1208
- const required = props.required ?? false;
1209
- return {
1210
- ...props,
1211
- kind: "Property",
1212
- required,
1213
- schema: syncOptionality(props.schema, required)
1214
- };
1057
+ function applyVisitor(node, visitor, parent) {
1058
+ const key = VISITOR_KEY_BY_KIND[node.kind];
1059
+ if (!key) return void 0;
1060
+ const fn = visitor[key];
1061
+ return fn?.(node, { parent });
1215
1062
  }
1216
1063
  /**
1217
- * Creates a `ParameterNode`.
1064
+ * Async depth-first traversal for side effects. Visitor return values are
1065
+ * ignored. Use `transform` when you want to rewrite nodes.
1218
1066
  *
1219
- * `required` defaults to `false`.
1220
- * Nested schema flags are set from `required` and `schema.nullable`.
1067
+ * Sibling nodes at each depth run concurrently up to `options.concurrency`
1068
+ * (defaults to `WALK_CONCURRENCY`). Higher values overlap I/O-bound visitor
1069
+ * work. Lower values reduce memory pressure.
1221
1070
  *
1222
- * @example
1071
+ * @example Log every operation
1223
1072
  * ```ts
1224
- * const param = createParameter({
1225
- * name: 'petId',
1226
- * in: 'path',
1227
- * required: true,
1228
- * schema: createSchema({ type: 'string' }),
1073
+ * await walk(root, {
1074
+ * operation(node) {
1075
+ * console.log(node.operationId)
1076
+ * },
1229
1077
  * })
1230
1078
  * ```
1231
1079
  *
1232
- * @example
1080
+ * @example Only visit the root node
1233
1081
  * ```ts
1234
- * const param = createParameter({
1235
- * name: 'status',
1236
- * in: 'query',
1237
- * schema: createSchema({ type: 'string', nullable: true }),
1238
- * })
1239
- * // required=false, schema.nullish=true
1082
+ * await walk(root, { depth: 'shallow', input: () => {} })
1240
1083
  * ```
1241
1084
  */
1242
- function createParameter(props) {
1243
- const required = props.required ?? false;
1244
- return {
1245
- ...props,
1246
- kind: "Parameter",
1247
- required,
1248
- schema: syncOptionality(props.schema, required)
1249
- };
1085
+ async function walk(node, options) {
1086
+ return _walk(node, options, (options.depth ?? require_utils.visitorDepths.deep) === require_utils.visitorDepths.deep, createLimit(options.concurrency ?? 30), void 0);
1250
1087
  }
1251
- /**
1252
- * Creates a `ResponseNode`.
1253
- *
1254
- * Response body schemas live inside `content`. For convenience a single legacy `schema`
1255
- * (with optional `mediaType`/`keysToOmit`) is normalized into one `content` entry, so the same
1256
- * schema is never stored both at the node root and inside `content`.
1257
- *
1258
- * @example
1259
- * ```ts
1260
- * const response = createResponse({
1261
- * statusCode: '200',
1262
- * content: [{ contentType: 'application/json', schema: createSchema({ type: 'object', properties: [] }) }],
1263
- * })
1264
- * ```
1265
- */
1266
- function createResponse(props) {
1267
- const { schema, mediaType, keysToOmit, content, ...rest } = props;
1268
- const entries = content ?? (schema ? [{
1269
- contentType: mediaType ?? "application/json",
1270
- schema,
1271
- keysToOmit: keysToOmit ?? null
1272
- }] : void 0);
1273
- return {
1274
- ...rest,
1275
- kind: "Response",
1276
- content: entries?.map(createContent)
1277
- };
1088
+ async function _walk(node, visitor, recurse, limit, parent) {
1089
+ await limit(() => applyVisitor(node, visitor, parent));
1090
+ const children = Array.from(getChildren(node, recurse));
1091
+ if (children.length === 0) return;
1092
+ await Promise.all(children.map((child) => _walk(child, visitor, recurse, limit, node)));
1278
1093
  }
1279
- /**
1280
- * Creates a `FunctionParameterNode`.
1281
- *
1282
- * `optional` defaults to `false`.
1283
- *
1284
- * @example Required typed param
1285
- * ```ts
1286
- * createFunctionParameter({ name: 'petId', type: createParamsType({ variant: 'reference', name: 'string' }) })
1287
- * // → petId: string
1288
- * ```
1289
- *
1290
- * @example Optional param
1291
- * ```ts
1292
- * createFunctionParameter({ name: 'params', type: createParamsType({ variant: 'reference', name: 'QueryParams' }), optional: true })
1293
- * // → params?: QueryParams
1294
- * ```
1295
- *
1296
- * @example Param with default (implicitly optional. Cannot combine with `optional: true`)
1297
- * ```ts
1298
- * createFunctionParameter({ name: 'config', type: createParamsType({ variant: 'reference', name: 'RequestConfig' }), default: '{}' })
1299
- * // → config: RequestConfig = {}
1300
- * ```
1301
- */
1302
- function createFunctionParameter(props) {
1303
- return {
1304
- optional: false,
1305
- ...props,
1306
- kind: "FunctionParameter"
1307
- };
1094
+ function transform(node, options) {
1095
+ const { depth, parent, ...visitor } = options;
1096
+ const recurse = (depth ?? require_utils.visitorDepths.deep) === require_utils.visitorDepths.deep;
1097
+ const rebuilt = transformChildren(applyVisitor(node, visitor, parent) ?? node, options, recurse);
1098
+ if (rebuilt === node) return node;
1099
+ const rebuild = nodeRebuilders[rebuilt.kind];
1100
+ return rebuild ? rebuild(rebuilt) : rebuilt;
1308
1101
  }
1309
1102
  /**
1310
- * Creates a {@link TypeNode} representing a language-agnostic structured type expression.
1311
- *
1312
- * Use `variant: 'struct'` for inline anonymous types and `variant: 'member'` for a single
1313
- * named field accessed from a group type. Each language's printer renders the variant
1314
- * into its own syntax (TypeScript, Python, C#, Kotlin, …).
1315
- *
1316
- * @example Reference type (TypeScript: `QueryParams`)
1317
- * ```ts
1318
- * createParamsType({ variant: 'reference', name: 'QueryParams' })
1319
- * ```
1320
- *
1321
- * @example Struct type (TypeScript: `{ petId: string }`)
1322
- * ```ts
1323
- * createParamsType({ variant: 'struct', properties: [{ name: 'petId', optional: false, type: createParamsType({ variant: 'reference', name: 'string' }) }] })
1324
- * ```
1325
- *
1326
- * @example Member type (TypeScript: `DeletePetPathParams['petId']`)
1327
- * ```ts
1328
- * createParamsType({ variant: 'member', base: 'DeletePetPathParams', key: 'petId' })
1329
- * ```
1103
+ * Immutably rebuilds a node's children using {@link VISITOR_KEYS}, transforming
1104
+ * each child node and leaving non-node values (e.g. `additionalProperties: true`) intact.
1105
+ * `Schema` children are skipped in shallow mode.
1330
1106
  */
1331
- function createParamsType(props) {
1332
- return {
1333
- ...props,
1334
- kind: "ParamsType"
1107
+ function transformChildren(node, options, recurse) {
1108
+ if (node.kind === "Schema" && !recurse) return node;
1109
+ const keys = visitorKeysByKind[node.kind];
1110
+ if (!keys) return node;
1111
+ const record = node;
1112
+ const childOptions = {
1113
+ ...options,
1114
+ parent: node
1335
1115
  };
1116
+ let updates;
1117
+ for (const key of keys) {
1118
+ if (!(key in record)) continue;
1119
+ const value = record[key];
1120
+ if (Array.isArray(value)) {
1121
+ let changed = false;
1122
+ const mapped = value.map((item) => {
1123
+ if (!isNode(item)) return item;
1124
+ const next = transform(item, childOptions);
1125
+ if (next !== item) changed = true;
1126
+ return next;
1127
+ });
1128
+ if (changed) (updates ??= {})[key] = mapped;
1129
+ } else if (isNode(value)) {
1130
+ const next = transform(value, childOptions);
1131
+ if (next !== value) (updates ??= {})[key] = next;
1132
+ }
1133
+ }
1134
+ return updates ? {
1135
+ ...node,
1136
+ ...updates
1137
+ } : node;
1336
1138
  }
1337
1139
  /**
1338
- * Creates a `ParameterGroupNode` representing a group of related parameters treated as a unit.
1339
- *
1340
- * @example Grouped param (TypeScript declaration)
1341
- * ```ts
1342
- * createParameterGroup({
1343
- * properties: [
1344
- * createFunctionParameter({ name: 'id', type: createParamsType({ variant: 'reference', name: 'string' }), optional: false }),
1345
- * createFunctionParameter({ name: 'name', type: createParamsType({ variant: 'reference', name: 'string' }), optional: true }),
1346
- * ],
1347
- * default: '{}',
1348
- * })
1349
- * // declaration → { id, name? }: { id: string; name?: string } = {}
1350
- * // call → { id, name }
1351
- * ```
1140
+ * Lazy depth-first collection pass. Yields every non-null value returned by
1141
+ * the visitor callbacks. Use `collect` for the eager array form.
1352
1142
  *
1353
- * @example Inline (spread), children emitted as individual top-level parameters
1143
+ * @example Collect every operationId
1354
1144
  * ```ts
1355
- * createParameterGroup({
1356
- * properties: [createFunctionParameter({ name: 'petId', type: createParamsType({ variant: 'reference', name: 'string' }), optional: false })],
1357
- * inline: true,
1358
- * })
1359
- * // declaration → petId: string
1360
- * // call → petId
1145
+ * const ids: string[] = []
1146
+ * for (const id of collectLazy<string>(root, {
1147
+ * operation(node) {
1148
+ * return node.operationId
1149
+ * },
1150
+ * })) {
1151
+ * ids.push(id)
1152
+ * }
1361
1153
  * ```
1362
1154
  */
1363
- function createParameterGroup(props) {
1364
- return {
1365
- ...props,
1366
- kind: "ParameterGroup"
1367
- };
1155
+ function* collectLazy(node, options) {
1156
+ const { depth, parent, ...visitor } = options;
1157
+ const recurse = (depth ?? require_utils.visitorDepths.deep) === require_utils.visitorDepths.deep;
1158
+ const v = applyVisitor(node, visitor, parent);
1159
+ if (v != null) yield v;
1160
+ for (const child of getChildren(node, recurse)) yield* collectLazy(child, {
1161
+ ...options,
1162
+ parent: node
1163
+ });
1368
1164
  }
1369
1165
  /**
1370
- * Creates a `FunctionParametersNode` from an ordered list of parameters.
1166
+ * Eager depth-first collection pass. Returns an array of every non-null value
1167
+ * the visitor callbacks return.
1371
1168
  *
1372
- * @example
1169
+ * @example Collect every operationId
1373
1170
  * ```ts
1374
- * createFunctionParameters({
1375
- * params: [
1376
- * createFunctionParameter({ name: 'petId', type: createParamsType({ variant: 'reference', name: 'string' }), optional: false }),
1377
- * createFunctionParameter({ name: 'config', type: createParamsType({ variant: 'reference', name: 'RequestConfig' }), optional: false, default: '{}' }),
1378
- * ],
1171
+ * const ids = collect<string>(root, {
1172
+ * operation(node) {
1173
+ * return node.operationId
1174
+ * },
1379
1175
  * })
1380
1176
  * ```
1381
- *
1382
- * @example
1383
- * ```ts
1384
- * const empty = createFunctionParameters()
1385
- * // { kind: 'FunctionParameters', params: [] }
1386
- * ```
1387
1177
  */
1388
- function createFunctionParameters(props = {}) {
1389
- return {
1390
- params: [],
1391
- ...props,
1392
- kind: "FunctionParameters"
1393
- };
1178
+ function collect(node, options) {
1179
+ return Array.from(collectLazy(node, options));
1394
1180
  }
1181
+ //#endregion
1182
+ //#region src/dedupe.ts
1395
1183
  /**
1396
- * Creates an `ImportNode` representing a language-agnostic import/dependency declaration.
1397
- *
1398
- * @example Named import
1399
- * ```ts
1400
- * createImport({ name: ['useState'], path: 'react' })
1401
- * // import { useState } from 'react'
1402
- * ```
1403
- *
1404
- * @example Type-only import
1405
- * ```ts
1406
- * createImport({ name: ['FC'], path: 'react', isTypeOnly: true })
1407
- * // import type { FC } from 'react'
1408
- * ```
1184
+ * Builds the shared `ref` replacement for a duplicate occurrence, carrying the
1185
+ * usage-slot and documentation fields that are not part of the canonical type.
1409
1186
  */
1410
- function createImport(props) {
1411
- return {
1412
- ...props,
1413
- kind: "Import"
1414
- };
1415
- }
1187
+ function createRefNode(node, canonical) {
1188
+ return createSchema({
1189
+ type: "ref",
1190
+ name: canonical.name,
1191
+ ref: canonical.ref,
1192
+ optional: node.optional,
1193
+ nullish: node.nullish,
1194
+ readOnly: node.readOnly,
1195
+ writeOnly: node.writeOnly,
1196
+ deprecated: node.deprecated,
1197
+ description: node.description,
1198
+ default: node.default,
1199
+ example: node.example
1200
+ });
1201
+ }
1202
+ function applyDedupe(node, plan, skipRootMatch = false) {
1203
+ const { canonicalBySignature, aliasNames } = plan;
1204
+ if (canonicalBySignature.size === 0 && aliasNames.size === 0) return node;
1205
+ const root = node;
1206
+ return transform(node, { schema(schemaNode) {
1207
+ if (schemaNode.type === "ref") {
1208
+ const target = schemaNode.ref ? require_utils.extractRefName(schemaNode.ref) : schemaNode.name;
1209
+ const canonical = target ? aliasNames.get(target) : void 0;
1210
+ return canonical ? {
1211
+ ...schemaNode,
1212
+ name: canonical.name,
1213
+ ref: canonical.ref
1214
+ } : void 0;
1215
+ }
1216
+ const signature = signatureOf(schemaNode);
1217
+ if (skipRootMatch && schemaNode === root) return void 0;
1218
+ const canonical = canonicalBySignature.get(signature);
1219
+ if (!canonical) return void 0;
1220
+ return createRefNode(schemaNode, canonical);
1221
+ } });
1222
+ }
1416
1223
  /**
1417
- * Creates an `ExportNode` representing a language-agnostic export/public API declaration.
1418
- *
1419
- * @example Named export
1420
- * ```ts
1421
- * createExport({ name: ['Pet'], path: './Pet' })
1422
- * // export { Pet } from './Pet'
1423
- * ```
1424
- *
1425
- * @example Wildcard export
1426
- * ```ts
1427
- * createExport({ path: './utils' })
1428
- * // export * from './utils'
1429
- * ```
1224
+ * Strips usage-slot flags from a hoisted definition and applies its canonical name.
1225
+ * A standalone definition is never optional, so `optional`/`nullish` are cleared.
1430
1226
  */
1431
- function createExport(props) {
1227
+ function cleanDefinition(node, name) {
1432
1228
  return {
1433
- ...props,
1434
- kind: "Export"
1229
+ ...node,
1230
+ name,
1231
+ optional: void 0,
1232
+ nullish: void 0
1435
1233
  };
1436
1234
  }
1437
1235
  /**
1438
- * Creates a `SourceNode` representing a fragment of source code within a file.
1236
+ * Scans a forest of schema and operation nodes and produces a {@link DedupePlan}.
1237
+ *
1238
+ * A shape that occurs at least `minOccurrences` times is deduplicated: if any occurrence
1239
+ * is a named top-level schema, the first one becomes the canonical (so other top-level
1240
+ * duplicates and inline copies turn into references to it). Every other top-level name with
1241
+ * the same content is recorded in `aliasNames`, so refs to it can be repointed at the
1242
+ * canonical. Otherwise a new definition is hoisted using `nameFor`. The plan is then applied
1243
+ * per node with {@link applyDedupe}.
1439
1244
  *
1440
1245
  * @example
1441
1246
  * ```ts
1442
- * createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')], isExportable: true })
1247
+ * const plan = buildDedupePlan([...schemaNodes, ...operationNodes], {
1248
+ * isCandidate: (node) => node.type === 'enum' || node.type === 'object',
1249
+ * nameFor: (node) => node.name ?? null,
1250
+ * refFor: (name) => `#/components/schemas/${name}`,
1251
+ * })
1443
1252
  * ```
1444
1253
  */
1445
- function createSource(props) {
1254
+ function buildDedupePlan(roots, options) {
1255
+ const { isCandidate, nameFor, refFor, minOccurrences = 2 } = options;
1256
+ const topLevelNodes = /* @__PURE__ */ new Set();
1257
+ const groups = /* @__PURE__ */ new Map();
1258
+ function record(schemaNode) {
1259
+ const signature = signatureOf(schemaNode);
1260
+ if (!isCandidate(schemaNode)) return;
1261
+ const isTopLevel = topLevelNodes.has(schemaNode) && !!schemaNode.name;
1262
+ const group = groups.get(signature);
1263
+ if (group) {
1264
+ group.count++;
1265
+ if (isTopLevel) group.topLevelNames.push(schemaNode.name);
1266
+ } else groups.set(signature, {
1267
+ count: 1,
1268
+ representative: schemaNode,
1269
+ topLevelNames: isTopLevel ? [schemaNode.name] : []
1270
+ });
1271
+ }
1272
+ for (const root of roots) {
1273
+ if (root.kind === "Schema") topLevelNodes.add(root);
1274
+ for (const schemaNode of collectLazy(root, { schema: (node) => node })) record(schemaNode);
1275
+ }
1276
+ const canonicalBySignature = /* @__PURE__ */ new Map();
1277
+ const aliasNames = /* @__PURE__ */ new Map();
1278
+ const pendingHoists = [];
1279
+ for (const [signature, group] of groups) {
1280
+ if (group.count < minOccurrences) continue;
1281
+ const [firstName, ...duplicateNames] = group.topLevelNames;
1282
+ if (firstName) {
1283
+ const canonical = {
1284
+ name: firstName,
1285
+ ref: refFor(firstName)
1286
+ };
1287
+ canonicalBySignature.set(signature, canonical);
1288
+ for (const duplicate of duplicateNames) aliasNames.set(duplicate, canonical);
1289
+ continue;
1290
+ }
1291
+ const name = nameFor(group.representative, signature);
1292
+ if (!name) continue;
1293
+ canonicalBySignature.set(signature, {
1294
+ name,
1295
+ ref: refFor(name)
1296
+ });
1297
+ pendingHoists.push({
1298
+ name,
1299
+ representative: group.representative
1300
+ });
1301
+ }
1446
1302
  return {
1447
- ...props,
1448
- kind: "Source"
1303
+ canonicalBySignature,
1304
+ aliasNames,
1305
+ hoisted: pendingHoists.map(({ name, representative }) => cleanDefinition(applyDedupe(representative, {
1306
+ canonicalBySignature,
1307
+ aliasNames
1308
+ }, true), name))
1449
1309
  };
1450
1310
  }
1311
+ //#endregion
1312
+ //#region src/dialect.ts
1451
1313
  /**
1452
- * Creates a fully resolved `FileNode` from a file input descriptor.
1453
- *
1454
- * Computes:
1455
- * - `id` SHA256 hash of the file path
1456
- * - `name` `baseName` without extension
1457
- * - `extname` extension extracted from `baseName`
1458
- *
1459
- * Deduplicates:
1460
- * - `sources` via `combineSources`
1461
- * - `exports` via `combineExports`
1462
- * - `imports` via `combineImports` (also filters unused imports)
1463
- *
1464
- * @throws {Error} when `baseName` has no extension.
1314
+ * Types a {@link SchemaDialect} for an adapter. Adds no runtime behavior and only pins the
1315
+ * dialect's type for inference.
1465
1316
  *
1466
1317
  * @example
1467
1318
  * ```ts
1468
- * const file = createFile({
1469
- * baseName: 'petStore.ts',
1470
- * path: 'src/models/petStore.ts',
1471
- * sources: [createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')] })],
1472
- * imports: [createImport({ name: ['z'], path: 'zod' })],
1473
- * exports: [createExport({ name: ['Pet'], path: './petStore' })],
1319
+ * export const oasDialect = defineSchemaDialect({
1320
+ * name: 'oas',
1321
+ * isNullable,
1322
+ * isReference,
1323
+ * isDiscriminator,
1324
+ * isBinary: (schema) => schema.type === 'string' && schema.contentMediaType === 'application/octet-stream',
1325
+ * resolveRef,
1474
1326
  * })
1475
- * // file.id = SHA256 hash of 'src/models/petStore.ts'
1476
- * // file.name = 'petStore'
1477
- * // file.extname = '.ts'
1478
1327
  * ```
1479
1328
  */
1480
- function createFile(input) {
1481
- const extname = node_path.default.extname(input.baseName) || (input.baseName.startsWith(".") ? input.baseName : "");
1482
- if (!extname) throw new Error(`No extname found for ${input.baseName}`);
1483
- const source = (input.sources ?? []).flatMap((item) => item.nodes ?? []).map((node) => extractStringsFromNodes([node])).filter(Boolean).join("\n\n");
1484
- const resolvedExports = input.exports?.length ? combineExports(input.exports) : [];
1485
- const combinedImports = input.imports?.length ? combineImports(input.imports, resolvedExports, source || void 0) : [];
1486
- const localNames = new Set((input.sources ?? []).map((item) => item.name).filter((name) => Boolean(name)));
1487
- const nameOf = (item) => typeof item === "string" ? item : item.name ?? item.propertyName;
1488
- const resolvedImports = combinedImports.filter((imp) => imp.path !== input.path).flatMap((imp) => {
1489
- if (!Array.isArray(imp.name)) return typeof imp.name === "string" && localNames.has(imp.name) ? [] : [imp];
1490
- const kept = imp.name.filter((item) => !localNames.has(nameOf(item)));
1491
- if (!kept.length) return [];
1492
- return [kept.length === imp.name.length ? imp : {
1493
- ...imp,
1494
- name: kept
1495
- }];
1496
- });
1497
- const resolvedSources = input.sources?.length ? combineSources(input.sources) : [];
1498
- return {
1499
- kind: "File",
1500
- ...input,
1501
- id: (0, node_crypto.hash)("sha256", input.path, "hex"),
1502
- name: trimExtName(input.baseName),
1503
- extname,
1504
- imports: resolvedImports,
1505
- exports: resolvedExports,
1506
- sources: resolvedSources,
1507
- meta: input.meta ?? {}
1508
- };
1329
+ function defineSchemaDialect(dialect) {
1330
+ return dialect;
1509
1331
  }
1332
+ //#endregion
1333
+ //#region src/guards.ts
1510
1334
  /**
1511
- * Creates a `ConstNode` representing a TypeScript `const` declaration.
1512
- *
1513
- * Mirrors the `Const` component from `@kubb/renderer-jsx`.
1514
- * The component's `children` are represented as `nodes`.
1515
- *
1516
- * @example Simple constant
1517
- * ```ts
1518
- * createConst({ name: 'pet' })
1519
- * // const pet = ...
1520
- * ```
1521
- *
1522
- * @example Exported constant with type and `as const`
1523
- * ```ts
1524
- * createConst({ name: 'pets', export: true, type: 'Pet[]', asConst: true })
1525
- * // export const pets: Pet[] = ... as const
1526
- * ```
1335
+ * Narrows a `SchemaNode` to the variant that matches `type`.
1527
1336
  *
1528
- * @example With JSDoc and child nodes
1337
+ * @example
1529
1338
  * ```ts
1530
- * createConst({
1531
- * name: 'config',
1532
- * export: true,
1533
- * JSDoc: { comments: ['@description App configuration'] },
1534
- * nodes: [],
1535
- * })
1339
+ * const schema = createSchema({ type: 'string' })
1340
+ * const stringNode = narrowSchema(schema, 'string') // StringSchemaNode | null
1536
1341
  * ```
1537
1342
  */
1538
- function createConst(props) {
1539
- return {
1540
- ...props,
1541
- kind: "Const"
1542
- };
1343
+ function narrowSchema(node, type) {
1344
+ return node?.type === type ? node : null;
1543
1345
  }
1544
1346
  /**
1545
- * Creates a `TypeNode` representing a TypeScript `type` alias declaration.
1546
- *
1547
- * Mirrors the `Type` component from `@kubb/renderer-jsx`.
1548
- * The component's `children` are represented as `nodes`.
1549
- *
1550
- * @example Simple type alias
1551
- * ```ts
1552
- * createType({ name: 'Pet' })
1553
- * // type Pet = ...
1554
- * ```
1347
+ * Narrows an `OperationNode` to an `HttpOperationNode`, guaranteeing `method` and `path`.
1555
1348
  *
1556
- * @example Exported type with JSDoc
1349
+ * @example
1557
1350
  * ```ts
1558
- * createType({
1559
- * name: 'PetStatus',
1560
- * export: true,
1561
- * JSDoc: { comments: ['@description Status of a pet'] },
1562
- * })
1563
- * // export type PetStatus = ...
1351
+ * if (isHttpOperationNode(node)) {
1352
+ * console.log(node.method, node.path)
1353
+ * }
1564
1354
  * ```
1565
1355
  */
1566
- function createType(props) {
1567
- return {
1568
- ...props,
1569
- kind: "Type"
1570
- };
1356
+ function isHttpOperationNode(node) {
1357
+ return node.protocol === "http" || node.method !== void 0 && node.path !== void 0;
1571
1358
  }
1359
+ //#endregion
1360
+ //#region src/utils/ast.ts
1361
+ const plainStringTypes = new Set([
1362
+ "string",
1363
+ "uuid",
1364
+ "email",
1365
+ "url",
1366
+ "datetime"
1367
+ ]);
1572
1368
  /**
1573
- * Creates a `FunctionNode` representing a TypeScript `function` declaration.
1574
- *
1575
- * Mirrors the `Function` component from `@kubb/renderer-jsx`.
1576
- * The component's `children` are represented as `nodes`.
1577
- *
1578
- * @example Simple function
1579
- * ```ts
1580
- * createFunction({ name: 'getPet' })
1581
- * // function getPet() { ... }
1582
- * ```
1369
+ * Merges a ref node with its resolved schema, giving usage-site fields precedence.
1583
1370
  *
1584
- * @example Exported async function with return type
1585
- * ```ts
1586
- * createFunction({ name: 'fetchPet', export: true, async: true, returnType: 'Pet' })
1587
- * // export async function fetchPet(): Promise<Pet> { ... }
1588
- * ```
1371
+ * Usage-site fields (`description`, `readOnly`, `nullable`, `deprecated`) on the ref node
1372
+ * override the same fields in the resolved `node.schema`. Non-ref nodes are returned unchanged.
1589
1373
  *
1590
- * @example Function with generics and params
1374
+ * @example
1591
1375
  * ```ts
1592
- * createFunction({
1593
- * name: 'identity',
1594
- * export: true,
1595
- * generics: ['T'],
1596
- * params: 'value: T',
1597
- * returnType: 'T',
1598
- * })
1599
- * // export function identity<T>(value: T): T { ... }
1376
+ * // Ref with description override
1377
+ * const ref = createSchema({ type: 'ref', ref: '#/components/schemas/Pet', description: 'A cute pet' })
1378
+ * const merged = syncSchemaRef(ref) // merges with resolved Pet schema
1600
1379
  * ```
1601
1380
  */
1602
- function createFunction(props) {
1603
- return {
1604
- ...props,
1605
- kind: "Function"
1606
- };
1381
+ function syncSchemaRef(node) {
1382
+ const ref = narrowSchema(node, "ref");
1383
+ if (!ref) return node;
1384
+ if (!ref.schema) return node;
1385
+ const { kind: _kind, type: _type, name: _name, ref: _ref, schema: _schema, ...overrides } = ref;
1386
+ const definedOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== void 0));
1387
+ return createSchema({
1388
+ ...ref.schema,
1389
+ ...definedOverrides
1390
+ });
1607
1391
  }
1608
1392
  /**
1609
- * Creates an `ArrowFunctionNode` representing a TypeScript arrow function.
1610
- *
1611
- * Mirrors the `Function.Arrow` component from `@kubb/renderer-jsx`.
1612
- * The component's `children` are represented as `nodes`.
1613
- *
1614
- * @example Simple arrow function
1615
- * ```ts
1616
- * createArrowFunction({ name: 'getPet' })
1617
- * // const getPet = () => { ... }
1618
- * ```
1393
+ * Type guard that returns `true` when a schema emits as a plain `string` type.
1619
1394
  *
1620
- * @example Single-line exported arrow function
1621
- * ```ts
1622
- * createArrowFunction({ name: 'double', export: true, params: 'n: number', singleLine: true })
1623
- * // export const double = (n: number) => ...
1624
- * ```
1395
+ * Covers `string`, `uuid`, `email`, `url`, and `datetime` types. For `date` and `time`
1396
+ * types, returns `true` only when `representation` is `'string'` rather than `'date'`.
1397
+ */
1398
+ function isStringType(node) {
1399
+ if (plainStringTypes.has(node.type)) return true;
1400
+ const temporal = narrowSchema(node, "date") ?? narrowSchema(node, "time");
1401
+ if (temporal) return temporal.representation !== "date";
1402
+ return false;
1403
+ }
1404
+ /**
1405
+ * Applies casing rules to parameter names and returns a new parameter array.
1625
1406
  *
1626
- * @example Async arrow function with generics
1627
- * ```ts
1628
- * createArrowFunction({
1629
- * name: 'fetchPet',
1630
- * export: true,
1631
- * async: true,
1632
- * generics: ['T'],
1633
- * params: 'id: string',
1634
- * returnType: 'T',
1635
- * })
1636
- * // export const fetchPet = async <T>(id: string): Promise<T> => { ... }
1637
- * ```
1407
+ * Use this before passing parameters to schema builders so output property keys match
1408
+ * the desired casing while preserving `OperationNode.parameters` for other consumers.
1409
+ * The input array is not mutated. When `casing` is not set, the original array is returned unchanged.
1638
1410
  */
1639
- function createArrowFunction(props) {
1411
+ const caseParamsMemo = memoize(/* @__PURE__ */ new WeakMap(), (params) => memoize(/* @__PURE__ */ new Map(), (casing) => params.map((param) => {
1412
+ const transformed = casing === "camelcase" || !require_utils.isValidVarName(param.name) ? require_utils.camelCase(param.name) : param.name;
1640
1413
  return {
1641
- ...props,
1642
- kind: "ArrowFunction"
1414
+ ...param,
1415
+ name: transformed
1643
1416
  };
1417
+ })));
1418
+ function caseParams(params, casing) {
1419
+ if (!casing) return params;
1420
+ return caseParamsMemo(params)(casing);
1644
1421
  }
1645
1422
  /**
1646
- * Creates a {@link TextNode} representing a raw string fragment in the source output.
1647
- *
1648
- * Use this instead of bare strings when building `nodes` arrays so that every
1649
- * entry in the array is a typed {@link CodeNode}.
1423
+ * Creates a single-property object schema used as a discriminator literal.
1650
1424
  *
1651
1425
  * @example
1652
1426
  * ```ts
1653
- * createText('return fetch(id)')
1654
- * // { kind: 'Text', value: 'return fetch(id)' }
1427
+ * createDiscriminantNode({ propertyName: 'type', value: 'dog' })
1428
+ * // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
1655
1429
  * ```
1656
1430
  */
1657
- function createText(value) {
1658
- return {
1659
- value,
1660
- kind: "Text"
1431
+ function createDiscriminantNode({ propertyName, value }) {
1432
+ return createSchema({
1433
+ type: "object",
1434
+ primitive: "object",
1435
+ properties: [createProperty({
1436
+ name: propertyName,
1437
+ schema: createSchema({
1438
+ type: "enum",
1439
+ primitive: "string",
1440
+ enumValues: [value]
1441
+ }),
1442
+ required: true
1443
+ })]
1444
+ });
1445
+ }
1446
+ /**
1447
+ * Resolves the {@link TypeExpression} for an individual parameter.
1448
+ *
1449
+ * Without a resolver, falls back to the schema primitive (a plain type-name string).
1450
+ * When the parameter belongs to a named group, emits an {@link IndexedAccessTypeNode}
1451
+ * (`GroupParams['petId']`); otherwise the resolved individual name as a plain string.
1452
+ */
1453
+ function resolveParamType({ node, param, resolver }) {
1454
+ if (!resolver) return param.schema.primitive ?? "unknown";
1455
+ const individualName = resolver.resolveParamName(node, param);
1456
+ const groupLocation = param.in === "path" || param.in === "query" || param.in === "header" ? param.in : void 0;
1457
+ const groupResolvers = {
1458
+ path: resolver.resolvePathParamsName,
1459
+ query: resolver.resolveQueryParamsName,
1460
+ header: resolver.resolveHeaderParamsName
1661
1461
  };
1462
+ const groupName = groupLocation ? groupResolvers[groupLocation].call(resolver, node, param) : void 0;
1463
+ if (groupName && groupName !== individualName) return createIndexedAccessType({
1464
+ objectType: groupName,
1465
+ indexType: param.name
1466
+ });
1467
+ return individualName;
1662
1468
  }
1663
1469
  /**
1664
- * Creates a {@link BreakNode} representing a line break in the source output.
1470
+ * Converts an `OperationNode` into function parameters for code generation.
1665
1471
  *
1666
- * Corresponds to `<br/>` in JSX components. Prints as an empty string which,
1667
- * when joined with `\n` by `printNodes`, produces a blank line.
1472
+ * Centralizes parameter grouping logic for all plugins. `paramsType` chooses between one
1473
+ * destructured object parameter (`object`) and separate top-level parameters (`inline`), while
1474
+ * `pathParamsType` controls how path params render in inline mode. Provide a `resolver` for type
1475
+ * name resolution and `extraParams` for plugin-specific trailing parameters such as an `options` object.
1476
+ */
1477
+ function createOperationParams(node, options) {
1478
+ const { paramsType, pathParamsType, paramsCasing, resolver, pathParamsDefault, extraParams = [], paramNames, typeWrapper } = options;
1479
+ const dataName = paramNames?.data ?? "data";
1480
+ const paramsName = paramNames?.params ?? "params";
1481
+ const headersName = paramNames?.headers ?? "headers";
1482
+ const pathName = paramNames?.path ?? "pathParams";
1483
+ const wrapType = (type) => typeWrapper ? typeWrapper(type) : type;
1484
+ const wrapTypeExpression = (type) => typeof type === "string" ? wrapType(type) : type;
1485
+ const casedParams = caseParams(node.parameters, paramsCasing);
1486
+ const pathParams = casedParams.filter((p) => p.in === "path");
1487
+ const queryParams = casedParams.filter((p) => p.in === "query");
1488
+ const headerParams = casedParams.filter((p) => p.in === "header");
1489
+ const toProperty = (param) => ({
1490
+ name: param.name,
1491
+ type: wrapTypeExpression(resolveParamType({
1492
+ node,
1493
+ param,
1494
+ resolver
1495
+ })),
1496
+ optional: !param.required
1497
+ });
1498
+ const emptyObjectDefault = (props) => props.every((p) => p.optional) ? "{}" : void 0;
1499
+ const bodyType = node.requestBody?.content?.[0]?.schema ? wrapType(resolver?.resolveDataName(node) ?? "unknown") : void 0;
1500
+ const bodyProperty = bodyType ? [{
1501
+ name: dataName,
1502
+ type: bodyType,
1503
+ optional: !(node.requestBody?.required ?? false)
1504
+ }] : [];
1505
+ const trailingGroups = [{
1506
+ name: paramsName,
1507
+ node,
1508
+ params: queryParams,
1509
+ groupType: resolveGroupType({
1510
+ node,
1511
+ params: queryParams,
1512
+ group: "query",
1513
+ resolver
1514
+ }),
1515
+ resolver,
1516
+ wrapType
1517
+ }, {
1518
+ name: headersName,
1519
+ node,
1520
+ params: headerParams,
1521
+ groupType: resolveGroupType({
1522
+ node,
1523
+ params: headerParams,
1524
+ group: "header",
1525
+ resolver
1526
+ }),
1527
+ resolver,
1528
+ wrapType
1529
+ }];
1530
+ const params = [];
1531
+ if (paramsType === "object") {
1532
+ const children = [
1533
+ ...pathParams.map(toProperty),
1534
+ ...bodyProperty,
1535
+ ...trailingGroups.flatMap(buildGroupProperty)
1536
+ ];
1537
+ if (children.length) params.push(createFunctionParameter({
1538
+ properties: children,
1539
+ default: emptyObjectDefault(children)
1540
+ }));
1541
+ } else {
1542
+ if (pathParamsType === "inlineSpread" && pathParams.length) {
1543
+ const spreadType = resolver?.resolvePathParamsName(node, pathParams[0]);
1544
+ params.push(createFunctionParameter({
1545
+ name: pathName,
1546
+ type: spreadType ? wrapType(spreadType) : void 0,
1547
+ rest: true
1548
+ }));
1549
+ } else if (pathParamsType === "inline") params.push(...pathParams.map((p) => createFunctionParameter(toProperty(p))));
1550
+ else if (pathParams.length) {
1551
+ const pathChildren = pathParams.map(toProperty);
1552
+ params.push(createFunctionParameter({
1553
+ properties: pathChildren,
1554
+ default: pathParamsDefault ?? emptyObjectDefault(pathChildren)
1555
+ }));
1556
+ }
1557
+ params.push(...bodyProperty.map((p) => createFunctionParameter(p)));
1558
+ params.push(...trailingGroups.flatMap(buildGroupParam));
1559
+ }
1560
+ params.push(...extraParams);
1561
+ return createFunctionParameters({ params });
1562
+ }
1563
+ /**
1564
+ * Builds the property descriptor for a query or header group.
1565
+ * Returns an empty array when there are no params to emit.
1668
1566
  *
1669
- * @example
1670
- * ```ts
1671
- * createBreak()
1672
- * // { kind: 'Break' }
1673
- * ```
1567
+ * A pre-resolved `groupType` emits `name: GroupType`. Otherwise it builds an inline
1568
+ * {@link TypeLiteralNode} from the individual params.
1674
1569
  */
1675
- function createBreak() {
1676
- return { kind: "Break" };
1570
+ function buildGroupProperty({ name, node, params, groupType, resolver, wrapType }) {
1571
+ if (groupType) return [{
1572
+ name,
1573
+ type: typeof groupType.type === "string" ? wrapType(groupType.type) : groupType.type,
1574
+ optional: groupType.optional
1575
+ }];
1576
+ if (params.length) return [{
1577
+ name,
1578
+ type: buildTypeLiteral({
1579
+ node,
1580
+ params,
1581
+ resolver
1582
+ }),
1583
+ optional: params.every((p) => !p.required)
1584
+ }];
1585
+ return [];
1677
1586
  }
1678
1587
  /**
1679
- * Creates a {@link JsxNode} representing a raw JSX fragment in the source output.
1588
+ * Builds a single {@link FunctionParameterNode} for a query or header group.
1589
+ * Returns an empty array when there are no params to emit.
1680
1590
  *
1681
- * Use this to embed JSX markup (including fragments `<>…</>`) directly in generated code.
1591
+ * A pre-resolved `groupType` emits `name: GroupType`. Otherwise it builds an inline
1592
+ * {@link TypeLiteralNode} from the individual params.
1593
+ */
1594
+ function buildGroupParam(args) {
1595
+ return buildGroupProperty(args).map((p) => createFunctionParameter(p));
1596
+ }
1597
+ /**
1598
+ * Derives a {@link ParamGroupType} for a query or header group from the resolver.
1682
1599
  *
1683
- * @example
1684
- * ```ts
1685
- * createJsx('<>\n <a href={href}>Open</a>\n</>')
1686
- * // { kind: 'Jsx', value: '<>\n <a href={href}>Open</a>\n</>' }
1687
- * ```
1600
+ * Returns `null` when there is no resolver, no params, or the group name equals the
1601
+ * individual param name (so there is no real group to emit).
1688
1602
  */
1689
- function createJsx(value) {
1603
+ function resolveGroupType({ node, params, group, resolver }) {
1604
+ if (!resolver || !params.length) return null;
1605
+ const firstParam = params[0];
1606
+ const groupName = (group === "query" ? resolver.resolveQueryParamsName : resolver.resolveHeaderParamsName).call(resolver, node, firstParam);
1607
+ if (groupName === resolver.resolveParamName(node, firstParam)) return null;
1690
1608
  return {
1691
- value,
1692
- kind: "Jsx"
1609
+ type: groupName,
1610
+ optional: params.every((p) => !p.required)
1693
1611
  };
1694
1612
  }
1695
- //#endregion
1696
- //#region src/signature.ts
1697
1613
  /**
1698
- * The flags shared by every node kind that affect its type: `primitive`, `format`, `nullable`.
1614
+ * Builds a {@link TypeLiteralNode} for an inline anonymous type grouping named fields.
1615
+ *
1616
+ * Used when query or header parameters have no dedicated group type name.
1617
+ * Each language printer renders this appropriately (TypeScript: `{ petId: string; name?: string }`).
1699
1618
  */
1700
- function flagsDescriptor(node) {
1701
- return `${node.primitive ?? ""};${node.format ?? ""};${node.nullable ? 1 : 0}`;
1619
+ function buildTypeLiteral({ node, params, resolver }) {
1620
+ return createTypeLiteral({ members: params.map((p) => ({
1621
+ name: p.name,
1622
+ type: resolveParamType({
1623
+ node,
1624
+ param: p,
1625
+ resolver
1626
+ }),
1627
+ optional: !p.required
1628
+ })) });
1702
1629
  }
1703
- function refTargetName(node) {
1704
- if (node.ref) return require_utils.extractRefName(node.ref);
1705
- return node.name ?? "";
1630
+ function sourceKey(source) {
1631
+ return `${source.name ?? extractStringsFromNodes(source.nodes)}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}`;
1632
+ }
1633
+ function pathTypeKey(path, isTypeOnly) {
1634
+ return `${path}:${isTypeOnly ?? false}`;
1635
+ }
1636
+ function exportKey(path, name, isTypeOnly, asAlias) {
1637
+ return `${path}:${name ?? ""}:${isTypeOnly ?? false}:${asAlias ?? ""}`;
1638
+ }
1639
+ function importKey(path, name, isTypeOnly) {
1640
+ return `${path}:${name ?? ""}:${isTypeOnly ?? false}`;
1706
1641
  }
1707
- const arrayTupleFields = [
1708
- {
1709
- kind: "children",
1710
- key: "items",
1711
- prefix: "i"
1712
- },
1713
- {
1714
- kind: "child",
1715
- key: "rest",
1716
- prefix: "r"
1717
- },
1718
- {
1719
- kind: "scalar",
1720
- key: "min",
1721
- prefix: "mn"
1722
- },
1723
- {
1724
- kind: "scalar",
1725
- key: "max",
1726
- prefix: "mx"
1727
- },
1728
- {
1729
- kind: "bool",
1730
- key: "unique",
1731
- prefix: "u"
1732
- }
1733
- ];
1734
- const numericFields = [
1735
- {
1736
- kind: "scalar",
1737
- key: "min",
1738
- prefix: "mn"
1739
- },
1740
- {
1741
- kind: "scalar",
1742
- key: "max",
1743
- prefix: "mx"
1744
- },
1745
- {
1746
- kind: "scalar",
1747
- key: "exclusiveMinimum",
1748
- prefix: "emn"
1749
- },
1750
- {
1751
- kind: "scalar",
1752
- key: "exclusiveMaximum",
1753
- prefix: "emx"
1754
- },
1755
- {
1756
- kind: "scalar",
1757
- key: "multipleOf",
1758
- prefix: "mo"
1759
- }
1760
- ];
1761
- const rangeFields = [{
1762
- kind: "scalar",
1763
- key: "min",
1764
- prefix: "mn"
1765
- }, {
1766
- kind: "scalar",
1767
- key: "max",
1768
- prefix: "mx"
1769
- }];
1770
1642
  /**
1771
- * Maps each node `type` to its ordered shape-contributing fields. Types absent from the map
1772
- * (boolean, null, any, and other scalars) fall back to `${type}|${flags}`.
1643
+ * Computes a multi-level sort key for exports and imports:
1644
+ * non-array names first (wildcards/namespace aliases). Type-only before value. Alphabetical path. Unnamed before named.
1773
1645
  */
1774
- const SHAPE_KEYS = {
1775
- object: [
1776
- { kind: "objectProps" },
1777
- { kind: "additionalProps" },
1778
- { kind: "patternProps" },
1779
- {
1780
- kind: "scalar",
1781
- key: "minProperties",
1782
- prefix: "mn"
1783
- },
1784
- {
1785
- kind: "scalar",
1786
- key: "maxProperties",
1787
- prefix: "mx"
1788
- }
1789
- ],
1790
- array: arrayTupleFields,
1791
- tuple: arrayTupleFields,
1792
- union: [
1793
- {
1794
- kind: "scalar",
1795
- key: "strategy",
1796
- prefix: "s"
1797
- },
1798
- {
1799
- kind: "scalar",
1800
- key: "discriminatorPropertyName",
1801
- prefix: "d"
1802
- },
1803
- {
1804
- kind: "children",
1805
- key: "members",
1806
- prefix: "m"
1807
- }
1808
- ],
1809
- intersection: [{
1810
- kind: "children",
1811
- key: "members",
1812
- prefix: "m"
1813
- }],
1814
- enum: [{ kind: "enumValues" }],
1815
- ref: [{ kind: "refTarget" }],
1816
- string: [
1817
- {
1818
- kind: "scalar",
1819
- key: "min",
1820
- prefix: "mn"
1821
- },
1822
- {
1823
- kind: "scalar",
1824
- key: "max",
1825
- prefix: "mx"
1826
- },
1827
- {
1828
- kind: "scalar",
1829
- key: "pattern",
1830
- prefix: "pt"
1831
- }
1832
- ],
1833
- number: numericFields,
1834
- integer: numericFields,
1835
- bigint: numericFields,
1836
- url: [
1837
- {
1838
- kind: "scalar",
1839
- key: "path",
1840
- prefix: "path"
1841
- },
1842
- {
1843
- kind: "scalar",
1844
- key: "min",
1845
- prefix: "mn"
1846
- },
1847
- {
1848
- kind: "scalar",
1849
- key: "max",
1850
- prefix: "mx"
1851
- }
1852
- ],
1853
- uuid: rangeFields,
1854
- email: rangeFields,
1855
- datetime: [{
1856
- kind: "bool",
1857
- key: "offset",
1858
- prefix: "o"
1859
- }, {
1860
- kind: "bool",
1861
- key: "local",
1862
- prefix: "l"
1863
- }],
1864
- date: [{
1865
- kind: "scalar",
1866
- key: "representation",
1867
- prefix: "rep"
1868
- }],
1869
- time: [{
1870
- kind: "scalar",
1871
- key: "representation",
1872
- prefix: "rep"
1873
- }]
1874
- };
1875
- function serializeShapeField(field, node, record) {
1876
- switch (field.kind) {
1877
- case "scalar": return `${field.prefix}:${record[field.key] ?? ""}`;
1878
- case "bool": return `${field.prefix}:${record[field.key] ? 1 : 0}`;
1879
- case "child": {
1880
- const child = record[field.key];
1881
- return `${field.prefix}:${child ? signatureOf(child) : ""}`;
1882
- }
1883
- case "children": {
1884
- const children = record[field.key] ?? [];
1885
- return `${field.prefix}[${children.map((c) => signatureOf(c)).join(",")}]`;
1646
+ function sortKey(node) {
1647
+ const isArray = Array.isArray(node.name) ? "1" : "0";
1648
+ const typeOnly = node.isTypeOnly ? "0" : "1";
1649
+ const hasName = node.name != null ? "1" : "0";
1650
+ const name = Array.isArray(node.name) ? node.name.toSorted().join("\0") : node.name ?? "";
1651
+ return `${isArray}:${typeOnly}:${node.path}:${hasName}:${name}`;
1652
+ }
1653
+ /**
1654
+ * Deduplicates and merges `SourceNode` objects by `name + isExportable + isTypeOnly`.
1655
+ *
1656
+ * Unnamed sources are deduplicated by object reference. Returns a deduplicated array in original order.
1657
+ */
1658
+ function combineSources(sources) {
1659
+ const seen = /* @__PURE__ */ new Map();
1660
+ for (const source of sources) {
1661
+ const key = sourceKey(source);
1662
+ if (!seen.has(key)) seen.set(key, source);
1663
+ }
1664
+ return [...seen.values()];
1665
+ }
1666
+ /**
1667
+ * Merges `incoming` names into `existing`, preserving order and dropping duplicates.
1668
+ *
1669
+ * Shared by `combineExports` and `combineImports` for the same-path name-merge case.
1670
+ */
1671
+ function mergeNameArrays(existing, incoming) {
1672
+ const merged = new Set(existing);
1673
+ for (const name of incoming) merged.add(name);
1674
+ return [...merged];
1675
+ }
1676
+ /**
1677
+ * Deduplicates and merges `ExportNode` objects by path and type.
1678
+ *
1679
+ * Named exports with the same path and `isTypeOnly` flag have their names merged into a single export.
1680
+ * Non-array exports are deduplicated by exact identity. Returns a sorted, deduplicated array.
1681
+ */
1682
+ function combineExports(exports) {
1683
+ const result = [];
1684
+ const namedByPath = /* @__PURE__ */ new Map();
1685
+ const seen = /* @__PURE__ */ new Set();
1686
+ const keyed = exports.map((node) => ({
1687
+ node,
1688
+ key: sortKey(node)
1689
+ }));
1690
+ keyed.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
1691
+ for (const { node: curr } of keyed) {
1692
+ const { name, path, isTypeOnly, asAlias } = curr;
1693
+ if (Array.isArray(name)) {
1694
+ if (!name.length) continue;
1695
+ const key = pathTypeKey(path, isTypeOnly);
1696
+ const existing = namedByPath.get(key);
1697
+ if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
1698
+ else {
1699
+ const newItem = {
1700
+ ...curr,
1701
+ name: [...new Set(name)]
1702
+ };
1703
+ result.push(newItem);
1704
+ namedByPath.set(key, newItem);
1705
+ }
1706
+ } else {
1707
+ const key = exportKey(path, name, isTypeOnly, asAlias);
1708
+ if (!seen.has(key)) {
1709
+ result.push(curr);
1710
+ seen.add(key);
1711
+ }
1886
1712
  }
1887
- case "objectProps": return `p[${(node.properties ?? []).map((prop) => `${prop.name}${prop.required ? "!" : "?"}${signatureOf(prop.schema)}`).join(",")}]`;
1888
- case "additionalProps": {
1889
- const obj = node;
1890
- if (typeof obj.additionalProperties === "boolean") return `ab:${obj.additionalProperties}`;
1891
- if (obj.additionalProperties) return `as:${signatureOf(obj.additionalProperties)}`;
1892
- return "";
1713
+ }
1714
+ return result;
1715
+ }
1716
+ /**
1717
+ * Deduplicates and merges `ImportNode` objects, filtering out unused imports.
1718
+ *
1719
+ * Retains imports that are referenced in `source` or re-exported. Imports with the same path and
1720
+ * `isTypeOnly` flag have their names merged. Returns a sorted, deduplicated, filtered array.
1721
+ *
1722
+ * @note Use this when combining imports from multiple files to avoid duplicate declarations.
1723
+ */
1724
+ function combineImports(imports, exports, source) {
1725
+ const exportedNames = new Set(exports.flatMap((e) => Array.isArray(e.name) ? e.name : e.name ? [e.name] : []));
1726
+ const isUsed = (importName) => !source || source.includes(importName) || exportedNames.has(importName);
1727
+ const importNameMemo = /* @__PURE__ */ new Map();
1728
+ const canonicalizeName = (n) => {
1729
+ if (typeof n === "string") return n;
1730
+ const key = `${n.propertyName}:${n.name ?? ""}`;
1731
+ if (!importNameMemo.has(key)) importNameMemo.set(key, n);
1732
+ return importNameMemo.get(key);
1733
+ };
1734
+ const pathsWithUsedNamedImport = /* @__PURE__ */ new Set();
1735
+ for (const node of imports) {
1736
+ if (!Array.isArray(node.name)) continue;
1737
+ if (node.name.some((item) => typeof item === "string" ? isUsed(item) : isUsed(item.name ?? item.propertyName))) pathsWithUsedNamedImport.add(node.path);
1738
+ }
1739
+ const result = [];
1740
+ const namedByPath = /* @__PURE__ */ new Map();
1741
+ const seen = /* @__PURE__ */ new Set();
1742
+ const keyed = imports.map((node) => ({
1743
+ node,
1744
+ key: sortKey(node)
1745
+ }));
1746
+ keyed.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
1747
+ for (const { node: curr } of keyed) {
1748
+ if (curr.path === curr.root) continue;
1749
+ const { path, isTypeOnly } = curr;
1750
+ let { name } = curr;
1751
+ if (Array.isArray(name)) {
1752
+ name = [...new Set(name.map(canonicalizeName))].filter((item) => typeof item === "string" ? isUsed(item) : isUsed(item.name ?? item.propertyName));
1753
+ if (!name.length) continue;
1754
+ const key = pathTypeKey(path, isTypeOnly);
1755
+ const existing = namedByPath.get(key);
1756
+ if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
1757
+ else {
1758
+ const newItem = {
1759
+ ...curr,
1760
+ name
1761
+ };
1762
+ result.push(newItem);
1763
+ namedByPath.set(key, newItem);
1764
+ }
1765
+ } else {
1766
+ if (name && !isUsed(name) && !pathsWithUsedNamedImport.has(path)) continue;
1767
+ const key = importKey(path, name, isTypeOnly);
1768
+ if (!seen.has(key)) {
1769
+ result.push(curr);
1770
+ seen.add(key);
1771
+ }
1893
1772
  }
1894
- case "patternProps": {
1895
- const obj = node;
1896
- return `pp[${obj.patternProperties ? Object.keys(obj.patternProperties).sort().map((key) => `${key}=${signatureOf(obj.patternProperties[key])}`).join(",") : ""}]`;
1773
+ }
1774
+ return result;
1775
+ }
1776
+ /**
1777
+ * Extracts all string content from a `CodeNode` tree recursively.
1778
+ *
1779
+ * Collects text node values, identifier references in string fields (`params`, `generics`, `returnType`, `type`),
1780
+ * and nested node content. Used internally to build the full source string for import filtering.
1781
+ */
1782
+ function extractStringsFromNodes(nodes) {
1783
+ if (!nodes?.length) return "";
1784
+ return nodes.map((node) => {
1785
+ if (typeof node === "string") return node;
1786
+ if (node.kind === "Text") return node.value;
1787
+ if (node.kind === "Break") return "";
1788
+ if (node.kind === "Jsx") return node.value;
1789
+ const parts = [];
1790
+ if ("params" in node && node.params) parts.push(node.params);
1791
+ if ("generics" in node && node.generics) parts.push(Array.isArray(node.generics) ? node.generics.join(", ") : node.generics);
1792
+ if ("returnType" in node && node.returnType) parts.push(node.returnType);
1793
+ if ("type" in node && typeof node.type === "string") parts.push(node.type);
1794
+ const nested = extractStringsFromNodes(node.nodes);
1795
+ if (nested) parts.push(nested);
1796
+ return parts.join("\n");
1797
+ }).filter(Boolean).join("\n");
1798
+ }
1799
+ /**
1800
+ * Resolves the schema name of a ref node, falling back through `ref` → `name` → nested `schema.name`.
1801
+ *
1802
+ * Returns `null` for non-ref nodes or when no name can be resolved. Use this to get a schema's
1803
+ * identifier for type definitions or error messages.
1804
+ *
1805
+ * @example
1806
+ * ```ts
1807
+ * resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Pet' })
1808
+ * // => 'Pet'
1809
+ * ```
1810
+ */
1811
+ function resolveRefName(node) {
1812
+ if (!node || node.type !== "ref") return null;
1813
+ if (node.ref) return require_utils.extractRefName(node.ref) ?? node.name ?? node.schema?.name ?? null;
1814
+ return node.name ?? node.schema?.name ?? null;
1815
+ }
1816
+ /**
1817
+ * Collects every named schema referenced (transitively) from a node via ref edges.
1818
+ *
1819
+ * Refs are followed by name only, the resolved `node.schema` is not traversed inline.
1820
+ * Use this to determine schema dependencies, build reference graphs, or detect what schemas need to be emitted.
1821
+ *
1822
+ * @example Collect refs from a single schema
1823
+ * ```ts
1824
+ * const names = collectReferencedSchemaNames(petSchema)
1825
+ * // → Set { 'Category', 'Tag' }
1826
+ * ```
1827
+ *
1828
+ * @example Accumulate refs from multiple schemas into one set
1829
+ * ```ts
1830
+ * const out = new Set<string>()
1831
+ * for (const schema of schemas) {
1832
+ * collectReferencedSchemaNames(schema, out)
1833
+ * }
1834
+ * ```
1835
+ */
1836
+ const collectSchemaRefs = memoize(/* @__PURE__ */ new WeakMap(), (node) => {
1837
+ const refs = /* @__PURE__ */ new Set();
1838
+ collect(node, { schema(child) {
1839
+ if (child.type === "ref") {
1840
+ const name = resolveRefName(child);
1841
+ if (name) refs.add(name);
1897
1842
  }
1898
- case "enumValues": {
1899
- const en = node;
1900
- let values = "";
1901
- if (en.namedEnumValues?.length) values = en.namedEnumValues.map((entry) => `${entry.name}=${entry.primitive}:${String(entry.value)}`).join(",");
1902
- else if (en.enumValues?.length) values = en.enumValues.map((value) => `${value === null ? "null" : typeof value}:${String(value)}`).join(",");
1903
- return `v[${values}]`;
1843
+ } });
1844
+ return refs;
1845
+ });
1846
+ function collectReferencedSchemaNames(node, out = /* @__PURE__ */ new Set()) {
1847
+ if (!node) return out;
1848
+ for (const name of collectSchemaRefs(node)) out.add(name);
1849
+ return out;
1850
+ }
1851
+ /**
1852
+ * Collects the names of all top-level schemas transitively used by a set of operations.
1853
+ *
1854
+ * An operation uses a schema when any of its parameters, request body content, or responses
1855
+ * reference it, directly or indirectly through other named schemas.
1856
+ * The walk is iterative and safe against reference cycles.
1857
+ *
1858
+ * Use this together with `include` filters to determine which schemas from `components/schemas`
1859
+ * are reachable from the allowed operations, so that schemas used only by excluded operations
1860
+ * are not generated.
1861
+ *
1862
+ * @example Only generate schemas referenced by included operations
1863
+ * ```ts
1864
+ * const includedOps = operations.filter(op => resolver.resolveOptions(op, { options, include }) !== null)
1865
+ * const allowed = collectUsedSchemaNames(includedOps, schemas)
1866
+ *
1867
+ * for (const schema of schemas) {
1868
+ * if (schema.name && !allowed.has(schema.name)) continue
1869
+ * // … generate schema
1870
+ * }
1871
+ * ```
1872
+ *
1873
+ * @example Check whether a specific schema is needed
1874
+ * ```ts
1875
+ * const allowed = collectUsedSchemaNames(includedOps, schemas)
1876
+ * allowed.has('OrderStatus') // false when no included operation references OrderStatus
1877
+ * ```
1878
+ */
1879
+ const collectUsedSchemaNamesMemo = memoize(/* @__PURE__ */ new WeakMap(), (ops) => memoize(/* @__PURE__ */ new WeakMap(), (schemas) => computeUsedSchemaNames(ops, schemas)));
1880
+ function computeUsedSchemaNames(operations, schemas) {
1881
+ const schemaMap = /* @__PURE__ */ new Map();
1882
+ for (const schema of schemas) if (schema.name) schemaMap.set(schema.name, schema);
1883
+ const result = /* @__PURE__ */ new Set();
1884
+ function visitSchema(schema) {
1885
+ const directRefs = collectReferencedSchemaNames(schema);
1886
+ for (const name of directRefs) if (!result.has(name)) {
1887
+ result.add(name);
1888
+ const namedSchema = schemaMap.get(name);
1889
+ if (namedSchema) visitSchema(namedSchema);
1904
1890
  }
1905
- case "refTarget": return `->${refTargetName(node)}`;
1906
1891
  }
1892
+ for (const op of operations) for (const schema of collectLazy(op, {
1893
+ depth: "shallow",
1894
+ schema: (node) => node
1895
+ })) visitSchema(schema);
1896
+ return result;
1907
1897
  }
1898
+ function collectUsedSchemaNames(operations, schemas) {
1899
+ return collectUsedSchemaNamesMemo(operations)(schemas);
1900
+ }
1901
+ const EMPTY_CIRCULAR_SET = /* @__PURE__ */ new Set();
1902
+ const findCircularSchemasMemo = memoize(/* @__PURE__ */ new WeakMap(), (schemas) => {
1903
+ const graph = /* @__PURE__ */ new Map();
1904
+ for (const schema of schemas) {
1905
+ if (!schema.name) continue;
1906
+ graph.set(schema.name, collectReferencedSchemaNames(schema));
1907
+ }
1908
+ const circular = /* @__PURE__ */ new Set();
1909
+ for (const start of graph.keys()) {
1910
+ const visited = /* @__PURE__ */ new Set();
1911
+ const stack = [...graph.get(start) ?? []];
1912
+ while (stack.length > 0) {
1913
+ const node = stack.pop();
1914
+ if (node === start) {
1915
+ circular.add(start);
1916
+ break;
1917
+ }
1918
+ if (visited.has(node)) continue;
1919
+ visited.add(node);
1920
+ const next = graph.get(node);
1921
+ if (next) for (const r of next) stack.push(r);
1922
+ }
1923
+ }
1924
+ return circular;
1925
+ });
1908
1926
  /**
1909
- * Builds the local shape descriptor that {@link signatureOf} hashes: the node's kind, flags,
1910
- * constraints, and its children's signatures.
1927
+ * Identifies all schemas that participate in circular dependency chains, including direct self-loops.
1928
+ *
1929
+ * Returns a Set of schema names with circular dependencies. Use this to wrap recursive schema positions
1930
+ * in deferred constructs (lazy getter, `z.lazy(() => …)`) to prevent infinite recursion when generated code runs.
1931
+ * Refs are followed by name only, keeping the algorithm linear in the schema graph size.
1932
+ *
1933
+ * @note Call this once on the full schema graph, then use `containsCircularRef()` to check individual schemas.
1911
1934
  */
1912
- function describeShape(node) {
1913
- const flags = flagsDescriptor(node);
1914
- const fields = SHAPE_KEYS[node.type];
1915
- if (!fields) return `${node.type}|${flags}`;
1916
- const record = node;
1917
- const parts = [`${node.type}|${flags}`];
1918
- for (const field of fields) parts.push(serializeShapeField(field, node, record));
1919
- return parts.join("|");
1935
+ function findCircularSchemas(schemas) {
1936
+ if (schemas.length === 0) return EMPTY_CIRCULAR_SET;
1937
+ return findCircularSchemasMemo(schemas);
1920
1938
  }
1921
1939
  /**
1922
- * Node digest cache, keyed by identity. A `WeakMap` so entries die with the node, and so a tree
1923
- * hashed during dedupe planning is not walked again when it is rewritten during streaming. Reuse
1924
- * is safe because a signature depends only on content, and nodes are immutable once created.
1925
- */
1926
- const signatureCache = /* @__PURE__ */ new WeakMap();
1927
- /**
1928
- * Computes a deterministic, shape-only signature (a content hash) for a schema node. Two schemas
1929
- * share a signature when they are structurally identical, ignoring documentation (`name`, `title`,
1930
- * `description`, `example`, `default`, `deprecated`) and usage-slot flags (`optional`, `nullish`,
1931
- * `readOnly`, `writeOnly`). `nullable` is kept because it changes the produced type, and `ref`
1932
- * nodes compare by target name, which also terminates on circular shapes.
1940
+ * Type guard returning `true` when a schema or anything nested within it contains a ref to a circular schema.
1933
1941
  *
1934
- * @example Two enums with different descriptions share a signature
1935
- * ```ts
1936
- * signatureOf(createSchema({ type: 'enum', primitive: 'string', enumValues: ['a', 'b'], description: 'x' })) ===
1937
- * signatureOf(createSchema({ type: 'enum', primitive: 'string', enumValues: ['a', 'b'] }))
1938
- * ```
1942
+ * Use `excludeName` to ignore refs to specific schemas (useful when self-references are handled separately).
1943
+ * Commonly used with `findCircularSchemas()` to detect where lazy wrappers are needed in code generation.
1944
+ *
1945
+ * @note Returns `true` for the first matching circular ref found. Use for fast dependency checks.
1939
1946
  */
1940
- function signatureOf(node) {
1941
- const cached = signatureCache.get(node);
1942
- if (cached !== void 0) return cached;
1943
- const signature = (0, node_crypto.hash)("sha256", describeShape(node), "hex");
1944
- signatureCache.set(node, signature);
1945
- return signature;
1947
+ function containsCircularRef(node, { circularSchemas, excludeName }) {
1948
+ if (!node || circularSchemas.size === 0) return false;
1949
+ for (const _ of collectLazy(node, { schema(child) {
1950
+ if (child.type !== "ref") return null;
1951
+ const name = resolveRefName(child);
1952
+ return name && name !== excludeName && circularSchemas.has(name) ? true : null;
1953
+ } })) return true;
1954
+ return false;
1946
1955
  }
1947
1956
  //#endregion
1948
- //#region src/dedupe.ts
1949
- /**
1950
- * Builds the shared `ref` replacement for a duplicate occurrence, carrying the
1951
- * usage-slot and documentation fields that are not part of the canonical type.
1952
- */
1953
- function createRefNode(node, canonical) {
1954
- return createSchema({
1955
- type: "ref",
1956
- name: canonical.name,
1957
- ref: canonical.ref,
1958
- optional: node.optional,
1959
- nullish: node.nullish,
1960
- readOnly: node.readOnly,
1961
- writeOnly: node.writeOnly,
1962
- deprecated: node.deprecated,
1963
- description: node.description,
1964
- default: node.default,
1965
- example: node.example
1966
- });
1967
- }
1968
- function applyDedupe(node, plan, skipRootMatch = false) {
1969
- const { canonicalBySignature, aliasNames } = plan;
1970
- if (canonicalBySignature.size === 0 && aliasNames.size === 0) return node;
1971
- const root = node;
1972
- return transform(node, { schema(schemaNode) {
1973
- if (schemaNode.type === "ref") {
1974
- const target = schemaNode.ref ? require_utils.extractRefName(schemaNode.ref) : schemaNode.name;
1975
- const canonical = target ? aliasNames.get(target) : void 0;
1976
- return canonical ? {
1977
- ...schemaNode,
1978
- name: canonical.name,
1979
- ref: canonical.ref
1980
- } : void 0;
1981
- }
1982
- const signature = signatureOf(schemaNode);
1983
- if (skipRootMatch && schemaNode === root) return void 0;
1984
- const canonical = canonicalBySignature.get(signature);
1985
- if (!canonical) return void 0;
1986
- return createRefNode(schemaNode, canonical);
1987
- } });
1988
- }
1989
- /**
1990
- * Strips usage-slot flags from a hoisted definition and applies its canonical name.
1991
- * A standalone definition is never optional, so `optional`/`nullish` are cleared.
1992
- */
1993
- function cleanDefinition(node, name) {
1994
- return {
1995
- ...node,
1996
- name,
1997
- optional: void 0,
1998
- nullish: void 0
1999
- };
2000
- }
1957
+ //#region src/factory.ts
2001
1958
  /**
2002
- * Scans a forest of schema and operation nodes and produces a {@link DedupePlan}.
1959
+ * Identity-preserving node update: returns `node` unchanged when every field in
1960
+ * `changes` already equals (by reference) the current value, otherwise a new node
1961
+ * with the changes applied.
2003
1962
  *
2004
- * A shape that occurs at least `minOccurrences` times is deduplicated: if any occurrence
2005
- * is a named top-level schema, the first one becomes the canonical (so other top-level
2006
- * duplicates and inline copies turn into references to it). Every other top-level name with
2007
- * the same content is recorded in `aliasNames`, so refs to it can be repointed at the
2008
- * canonical. Otherwise a new definition is hoisted using `nameFor`. The plan is then applied
2009
- * per node with {@link applyDedupe}.
1963
+ * Mirrors the TypeScript compiler's `factory.updateX` contract, pair it with the
1964
+ * structural sharing in {@link transform} so a no-op rewrite doesn't allocate and
1965
+ * downstream passes can detect "nothing changed" by identity. Comparison is
1966
+ * shallow: a structurally-equal but newly-allocated array/object counts as a change.
2010
1967
  *
2011
1968
  * @example
2012
1969
  * ```ts
2013
- * const plan = buildDedupePlan([...schemaNodes, ...operationNodes], {
2014
- * isCandidate: (node) => node.type === 'enum' || node.type === 'object',
2015
- * nameFor: (node) => node.name ?? null,
2016
- * refFor: (name) => `#/components/schemas/${name}`,
2017
- * })
1970
+ * update(node, { name: node.name }) // -> same `node` reference
1971
+ * update(node, { name: 'renamed' }) // -> new node, `name` replaced
2018
1972
  * ```
2019
1973
  */
2020
- function buildDedupePlan(roots, options) {
2021
- const { isCandidate, nameFor, refFor, minOccurrences = 2 } = options;
2022
- const topLevelNodes = /* @__PURE__ */ new Set();
2023
- const groups = /* @__PURE__ */ new Map();
2024
- function record(schemaNode) {
2025
- const signature = signatureOf(schemaNode);
2026
- if (!isCandidate(schemaNode)) return;
2027
- const isTopLevel = topLevelNodes.has(schemaNode) && !!schemaNode.name;
2028
- const group = groups.get(signature);
2029
- if (group) {
2030
- group.count++;
2031
- if (isTopLevel) group.topLevelNames.push(schemaNode.name);
2032
- } else groups.set(signature, {
2033
- count: 1,
2034
- representative: schemaNode,
2035
- topLevelNames: isTopLevel ? [schemaNode.name] : []
2036
- });
2037
- }
2038
- for (const root of roots) {
2039
- if (root.kind === "Schema") topLevelNodes.add(root);
2040
- for (const schemaNode of collectLazy(root, { schema: (node) => node })) record(schemaNode);
2041
- }
2042
- const canonicalBySignature = /* @__PURE__ */ new Map();
2043
- const aliasNames = /* @__PURE__ */ new Map();
2044
- const pendingHoists = [];
2045
- for (const [signature, group] of groups) {
2046
- if (group.count < minOccurrences) continue;
2047
- const [firstName, ...duplicateNames] = group.topLevelNames;
2048
- if (firstName) {
2049
- const canonical = {
2050
- name: firstName,
2051
- ref: refFor(firstName)
2052
- };
2053
- canonicalBySignature.set(signature, canonical);
2054
- for (const duplicate of duplicateNames) aliasNames.set(duplicate, canonical);
2055
- continue;
2056
- }
2057
- const name = nameFor(group.representative, signature);
2058
- if (!name) continue;
2059
- canonicalBySignature.set(signature, {
2060
- name,
2061
- ref: refFor(name)
2062
- });
2063
- pendingHoists.push({
2064
- name,
2065
- representative: group.representative
2066
- });
2067
- }
2068
- return {
2069
- canonicalBySignature,
2070
- aliasNames,
2071
- hoisted: pendingHoists.map(({ name, representative }) => cleanDefinition(applyDedupe(representative, {
2072
- canonicalBySignature,
2073
- aliasNames
2074
- }, true), name))
1974
+ function update(node, changes) {
1975
+ for (const key in changes) if (changes[key] !== node[key]) return {
1976
+ ...node,
1977
+ ...changes
2075
1978
  };
1979
+ return node;
2076
1980
  }
2077
- //#endregion
2078
- //#region src/dialect.ts
2079
1981
  /**
2080
- * Types a {@link SchemaDialect} for an adapter. Adds no runtime behavior and only pins the
2081
- * dialect's type for inference.
1982
+ * Creates a fully resolved `FileNode` from a file input descriptor.
1983
+ *
1984
+ * Computes:
1985
+ * - `id` SHA256 hash of the file path
1986
+ * - `name` `baseName` without extension
1987
+ * - `extname` extension extracted from `baseName`
1988
+ *
1989
+ * Deduplicates:
1990
+ * - `sources` via `combineSources`
1991
+ * - `exports` via `combineExports`
1992
+ * - `imports` via `combineImports` (also filters unused imports)
1993
+ *
1994
+ * @throws {Error} when `baseName` has no extension.
2082
1995
  *
2083
1996
  * @example
2084
1997
  * ```ts
2085
- * export const oasDialect = defineSchemaDialect({
2086
- * name: 'oas',
2087
- * isNullable,
2088
- * isReference,
2089
- * isDiscriminator,
2090
- * isBinary: (schema) => schema.type === 'string' && schema.contentMediaType === 'application/octet-stream',
2091
- * resolveRef,
1998
+ * const file = createFile({
1999
+ * baseName: 'petStore.ts',
2000
+ * path: 'src/models/petStore.ts',
2001
+ * sources: [createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')] })],
2002
+ * imports: [createImport({ name: ['z'], path: 'zod' })],
2003
+ * exports: [createExport({ name: ['Pet'], path: './petStore' })],
2092
2004
  * })
2005
+ * // file.id = SHA256 hash of 'src/models/petStore.ts'
2006
+ * // file.name = 'petStore'
2007
+ * // file.extname = '.ts'
2093
2008
  * ```
2094
2009
  */
2095
- function defineSchemaDialect(dialect) {
2096
- return dialect;
2010
+ function createFile(input) {
2011
+ const extname = node_path.default.extname(input.baseName) || (input.baseName.startsWith(".") ? input.baseName : "");
2012
+ if (!extname) throw new Error(`No extname found for ${input.baseName}`);
2013
+ const source = (input.sources ?? []).flatMap((item) => item.nodes ?? []).map((node) => extractStringsFromNodes([node])).filter(Boolean).join("\n\n");
2014
+ const resolvedExports = input.exports?.length ? combineExports(input.exports) : [];
2015
+ const combinedImports = input.imports?.length ? combineImports(input.imports, resolvedExports, source || void 0) : [];
2016
+ const localNames = new Set((input.sources ?? []).map((item) => item.name).filter((name) => Boolean(name)));
2017
+ const nameOf = (item) => typeof item === "string" ? item : item.name ?? item.propertyName;
2018
+ const resolvedImports = combinedImports.filter((imp) => imp.path !== input.path).flatMap((imp) => {
2019
+ if (!Array.isArray(imp.name)) return typeof imp.name === "string" && localNames.has(imp.name) ? [] : [imp];
2020
+ const kept = imp.name.filter((item) => !localNames.has(nameOf(item)));
2021
+ if (!kept.length) return [];
2022
+ return [kept.length === imp.name.length ? imp : {
2023
+ ...imp,
2024
+ name: kept
2025
+ }];
2026
+ });
2027
+ const resolvedSources = input.sources?.length ? combineSources(input.sources) : [];
2028
+ return {
2029
+ kind: "File",
2030
+ ...input,
2031
+ id: (0, node_crypto.hash)("sha256", input.path, "hex"),
2032
+ name: trimExtName(input.baseName),
2033
+ extname,
2034
+ imports: resolvedImports,
2035
+ exports: resolvedExports,
2036
+ sources: resolvedSources,
2037
+ meta: input.meta ?? {}
2038
+ };
2097
2039
  }
2098
2040
  //#endregion
2099
2041
  //#region src/printer.ts
@@ -2281,11 +2223,15 @@ function setEnumName(propNode, parentName, propName, enumSuffix) {
2281
2223
  }
2282
2224
  //#endregion
2283
2225
  exports.applyDedupe = applyDedupe;
2226
+ exports.arrowFunctionDef = arrowFunctionDef;
2227
+ exports.breakDef = breakDef;
2284
2228
  exports.buildDedupePlan = buildDedupePlan;
2285
2229
  exports.caseParams = caseParams;
2286
2230
  exports.collect = collect;
2287
2231
  exports.collectUsedSchemaNames = collectUsedSchemaNames;
2232
+ exports.constDef = constDef;
2288
2233
  exports.containsCircularRef = containsCircularRef;
2234
+ exports.contentDef = contentDef;
2289
2235
  exports.createArrowFunction = createArrowFunction;
2290
2236
  exports.createBreak = createBreak;
2291
2237
  exports.createConst = createConst;
@@ -2296,14 +2242,14 @@ exports.createFunction = createFunction;
2296
2242
  exports.createFunctionParameter = createFunctionParameter;
2297
2243
  exports.createFunctionParameters = createFunctionParameters;
2298
2244
  exports.createImport = createImport;
2245
+ exports.createIndexedAccessType = createIndexedAccessType;
2299
2246
  exports.createInput = createInput;
2300
2247
  exports.createJsx = createJsx;
2248
+ exports.createObjectBindingPattern = createObjectBindingPattern;
2301
2249
  exports.createOperation = createOperation;
2302
2250
  exports.createOperationParams = createOperationParams;
2303
2251
  exports.createOutput = createOutput;
2304
2252
  exports.createParameter = createParameter;
2305
- exports.createParameterGroup = createParameterGroup;
2306
- exports.createParamsType = createParamsType;
2307
2253
  exports.createPrinterFactory = createPrinterFactory;
2308
2254
  exports.createProperty = createProperty;
2309
2255
  exports.createResponse = createResponse;
@@ -2312,27 +2258,46 @@ exports.createSource = createSource;
2312
2258
  exports.createStreamInput = createStreamInput;
2313
2259
  exports.createText = createText;
2314
2260
  exports.createType = createType;
2261
+ exports.createTypeLiteral = createTypeLiteral;
2262
+ exports.defineNode = defineNode;
2315
2263
  exports.definePrinter = definePrinter;
2316
2264
  exports.defineSchemaDialect = defineSchemaDialect;
2265
+ exports.exportDef = exportDef;
2317
2266
  exports.extractStringsFromNodes = extractStringsFromNodes;
2267
+ exports.fileDef = fileDef;
2318
2268
  exports.findCircularSchemas = findCircularSchemas;
2269
+ exports.functionDef = functionDef;
2270
+ exports.functionParameterDef = functionParameterDef;
2271
+ exports.functionParametersDef = functionParametersDef;
2319
2272
  exports.httpMethods = require_utils.httpMethods;
2273
+ exports.importDef = importDef;
2274
+ exports.indexedAccessTypeDef = indexedAccessTypeDef;
2275
+ exports.inputDef = inputDef;
2320
2276
  exports.isHttpOperationNode = isHttpOperationNode;
2321
- exports.isInputNode = isInputNode;
2322
- exports.isOperationNode = isOperationNode;
2323
- exports.isOutputNode = isOutputNode;
2324
- exports.isSchemaNode = isSchemaNode;
2325
2277
  exports.isStringType = isStringType;
2278
+ exports.jsxDef = jsxDef;
2326
2279
  exports.mergeAdjacentObjectsLazy = mergeAdjacentObjectsLazy;
2327
2280
  exports.narrowSchema = narrowSchema;
2281
+ exports.objectBindingPatternDef = objectBindingPatternDef;
2282
+ exports.operationDef = operationDef;
2283
+ exports.outputDef = outputDef;
2284
+ exports.parameterDef = parameterDef;
2285
+ exports.propertyDef = propertyDef;
2286
+ exports.requestBodyDef = requestBodyDef;
2287
+ exports.responseDef = responseDef;
2288
+ exports.schemaDef = schemaDef;
2328
2289
  exports.schemaTypes = require_utils.schemaTypes;
2329
2290
  exports.setDiscriminatorEnum = setDiscriminatorEnum;
2330
2291
  exports.setEnumName = setEnumName;
2331
2292
  exports.signatureOf = signatureOf;
2332
2293
  exports.simplifyUnion = simplifyUnion;
2294
+ exports.sourceDef = sourceDef;
2333
2295
  exports.syncOptionality = syncOptionality;
2334
2296
  exports.syncSchemaRef = syncSchemaRef;
2297
+ exports.textDef = textDef;
2335
2298
  exports.transform = transform;
2299
+ exports.typeDef = typeDef;
2300
+ exports.typeLiteralDef = typeLiteralDef;
2336
2301
  exports.update = update;
2337
2302
  exports.walk = walk;
2338
2303