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