@kubb/ast 5.8.4 → 5.9.1

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
@@ -1,6 +1,6 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-CNktS9qV.js";
2
- import { hash } from "node:crypto";
3
2
  import path from "node:path";
3
+ import { hash } from "node:crypto";
4
4
  //#region src/constants.ts
5
5
  const visitorDepths = {
6
6
  shallow: "shallow",
@@ -145,11 +145,32 @@ function isHttpOperationNode(node) {
145
145
  return node.protocol === "http" || node.method !== void 0 && node.path !== void 0;
146
146
  }
147
147
  //#endregion
148
- //#region src/defineNode.ts
148
+ //#region src/visitor.ts
149
149
  /**
150
- * Visitor callback names, one per traversable node kind, in traversal order.
151
- * Kept in sync with the keys of `Visitor` in `visitor.ts`.
150
+ * Child node fields per node kind, in traversal order (Babel's `VISITOR_KEYS`).
152
151
  */
152
+ const VISITOR_KEYS = {
153
+ Input: ["schemas", "operations"],
154
+ Operation: [
155
+ "parameters",
156
+ "requestBody",
157
+ "responses"
158
+ ],
159
+ RequestBody: ["content"],
160
+ Content: ["schema"],
161
+ Response: ["content"],
162
+ Schema: [
163
+ "properties",
164
+ "items",
165
+ "members",
166
+ "additionalProperties",
167
+ "patternProperties",
168
+ "rest"
169
+ ],
170
+ Property: ["schema"],
171
+ Parameter: ["schema"]
172
+ };
173
+ /** Visitor callback names in traversal order, the keys of {@link Visitor}. */
153
174
  const visitorKeys = [
154
175
  "input",
155
176
  "output",
@@ -160,176 +181,395 @@ const visitorKeys = [
160
181
  "response"
161
182
  ];
162
183
  /**
163
- * Builds a type guard that matches nodes of the given `kind`.
184
+ * Maps a node kind to the matching visitor callback name.
164
185
  */
165
- function isKind(kind) {
166
- return (node) => node?.kind === kind;
167
- }
186
+ const VISITOR_KEY_BY_KIND = {
187
+ Input: "input",
188
+ Output: "output",
189
+ Operation: "operation",
190
+ Schema: "schema",
191
+ Property: "property",
192
+ Parameter: "parameter",
193
+ Response: "response"
194
+ };
195
+ const visitorKeysByKind = VISITOR_KEYS;
168
196
  /**
169
- * Defines a node once and derives its `create` builder, `is` guard, and traversal
170
- * metadata. `create` merges `defaults`, the `build` hook (or the raw input), and the
171
- * `kind`, so node construction lives in one place without scattered `as` casts.
172
- *
173
- * @example Simple node
174
- * ```ts
175
- * const importDef = defineNode<ImportNode>({ kind: 'Import' })
176
- * const createImport = importDef.create
177
- * ```
178
- *
179
- * @example Node with a build hook
180
- * ```ts
181
- * const propertyDef = defineNode<PropertyNode, UserPropertyNode>({
182
- * kind: 'Property',
183
- * build: (props) => ({ ...props, required: props.required ?? false }),
184
- * children: ['schema'],
185
- * visitorKey: 'property',
186
- * })
187
- * ```
197
+ * Returns `true` when `value` is an AST node (an object carrying a `kind`).
188
198
  */
189
- function defineNode(config) {
190
- const { kind, defaults, build, children, visitorKey } = config;
191
- function create(input) {
192
- const base = build ? build(input) : input;
193
- const node = {
194
- kind,
195
- ...defaults,
196
- ...base
197
- };
198
- node.kind = kind;
199
- return node;
200
- }
201
- return {
202
- kind,
203
- create,
204
- is: isKind(kind),
205
- children,
206
- visitorKey
207
- };
199
+ function isNode(value) {
200
+ return typeof value === "object" && value !== null && typeof value.kind === "string";
208
201
  }
209
- //#endregion
210
- //#region src/nodes/code.ts
211
- /**
212
- * Definition for the {@link ConstNode}.
213
- */
214
- const constDef = defineNode({ kind: "Const" });
215
- /**
216
- * Definition for the {@link TypeNode}.
217
- */
218
- const typeDef = defineNode({ kind: "Type" });
219
- /**
220
- * Definition for the {@link FunctionNode}.
221
- */
222
- const functionDef = defineNode({ kind: "Function" });
223
- /**
224
- * Definition for the {@link ArrowFunctionNode}.
225
- */
226
- const arrowFunctionDef = defineNode({ kind: "ArrowFunction" });
227
- /**
228
- * Definition for the {@link TextNode}.
229
- */
230
- const textDef = defineNode({
231
- kind: "Text",
232
- build: (value) => ({ value })
233
- });
234
202
  /**
235
- * Definition for the {@link BreakNode}.
203
+ * Returns `true` for a plain object that maps names to nodes, such as `patternProperties`.
236
204
  */
237
- const breakDef = defineNode({
238
- kind: "Break",
239
- build: () => ({})
240
- });
205
+ function isNodeRecord(value) {
206
+ return typeof value === "object" && value !== null && !Array.isArray(value);
207
+ }
241
208
  /**
242
- * Definition for the {@link JsxNode}.
209
+ * Returns the child fields of `node` to walk, from {@link VISITOR_KEYS}, or `undefined` when there
210
+ * is nothing to walk. `Schema` children are only walked when `recurse` is `true`.
243
211
  */
244
- const jsxDef = defineNode({
245
- kind: "Jsx",
246
- build: (value) => ({ value })
247
- });
212
+ function childKeys(node, recurse) {
213
+ if (node.kind === "Schema" && !recurse) return void 0;
214
+ return visitorKeysByKind[node.kind];
215
+ }
248
216
  /**
249
- * Creates a `ConstNode` representing a TypeScript `const` declaration.
217
+ * Returns the immediate traversable children of `node`.
250
218
  *
251
- * @example Exported constant with type and `as const`
219
+ * @example
252
220
  * ```ts
253
- * createConst({ name: 'pets', export: true, type: 'Pet[]', asConst: true })
254
- * // export const pets: Pet[] = ... as const
221
+ * const children = getChildren(operationNode, true)
222
+ * // returns parameters, the request body, and responses
255
223
  * ```
256
224
  */
257
- const createConst = constDef.create;
225
+ function* getChildren(node, recurse) {
226
+ const keys = childKeys(node, recurse);
227
+ if (!keys) return;
228
+ const record = node;
229
+ for (const key of keys) {
230
+ const value = record[key];
231
+ if (Array.isArray(value)) {
232
+ for (const item of value) if (isNode(item)) yield item;
233
+ continue;
234
+ }
235
+ if (isNode(value)) {
236
+ yield value;
237
+ continue;
238
+ }
239
+ if (isNodeRecord(value)) {
240
+ for (const item of Object.values(value)) if (isNode(item)) yield item;
241
+ }
242
+ }
243
+ }
258
244
  /**
259
- * Creates a `TypeNode` representing a TypeScript `type` alias declaration.
245
+ * Runs the visitor callback that matches `node.kind` with the traversal
246
+ * context. The result is a replacement node, a collected value, or `undefined`
247
+ * when no callback is registered for the kind.
260
248
  *
261
- * @example
262
- * ```ts
263
- * createType({ name: 'Pet', export: true })
264
- * // export type Pet = ...
265
- * ```
249
+ * Shared by `transform` and `collect` so node-kind dispatch lives in one place.
250
+ * `TResult` is the caller's expected return: the same node type for `transform`,
251
+ * the collected value type for `collect`.
266
252
  */
267
- const createType = typeDef.create;
253
+ function applyVisitor(node, visitor, parent) {
254
+ const key = VISITOR_KEY_BY_KIND[node.kind];
255
+ if (!key) return void 0;
256
+ const fn = visitor[key];
257
+ return fn?.(node, { parent });
258
+ }
268
259
  /**
269
- * Creates a `FunctionNode` representing a TypeScript `function` declaration.
260
+ * Synchronous depth-first transform. Each visitor callback can return a
261
+ * replacement node. Returning `undefined` keeps the original.
270
262
  *
271
- * @example
263
+ * The original tree is never mutated, a new tree is returned. Pass
264
+ * `depth: 'shallow'` to skip recursion into children.
265
+ *
266
+ * @example Prefix every operationId
272
267
  * ```ts
273
- * createFunction({ name: 'fetchPet', export: true, async: true, returnType: 'Pet' })
274
- * // export async function fetchPet(): Promise<Pet> { ... }
268
+ * const next = transform(root, {
269
+ * operation(node) {
270
+ * return { ...node, operationId: `prefixed_${node.operationId}` }
271
+ * },
272
+ * })
275
273
  * ```
276
- */
277
- const createFunction = functionDef.create;
278
- /**
279
- * Creates an `ArrowFunctionNode` representing a TypeScript arrow function.
280
274
  *
281
- * @example
275
+ * @example Replace only the root node
282
276
  * ```ts
283
- * createArrowFunction({ name: 'double', export: true, params: 'n: number', singleLine: true })
284
- * // export const double = (n: number) => ...
277
+ * const next = transform(root, {
278
+ * depth: 'shallow',
279
+ * input: (node) => ({ ...node, meta: { ...node.meta, title: 'Rewritten' } }),
280
+ * })
285
281
  * ```
286
282
  */
287
- const createArrowFunction = arrowFunctionDef.create;
283
+ function transform(node, options) {
284
+ const { depth, parent, ...visitor } = options;
285
+ return transformNode(node, visitor, (depth ?? visitorDepths.deep) === visitorDepths.deep, parent);
286
+ }
288
287
  /**
289
- * Creates a {@link TextNode} representing a raw string fragment in the source output.
288
+ * Visits a single node, then immutably rebuilds its children. Returns the original
289
+ * reference when neither the visitor nor the child rebuild changed anything, so callers
290
+ * can detect "nothing changed" by identity and ancestors avoid reallocating.
291
+ */
292
+ function transformNode(node, visitor, recurse, parent) {
293
+ return transformChildren(applyVisitor(node, visitor, parent) ?? node, visitor, recurse);
294
+ }
295
+ /**
296
+ * Immutably rebuilds a node's children, transforming each child node and leaving non-node values
297
+ * (e.g. `additionalProperties: true`) intact.
298
+ */
299
+ function transformChildren(node, visitor, recurse) {
300
+ const keys = childKeys(node, recurse);
301
+ if (!keys) return node;
302
+ const record = node;
303
+ let updates;
304
+ for (const key of keys) {
305
+ if (!(key in record)) continue;
306
+ const value = record[key];
307
+ if (Array.isArray(value)) {
308
+ let mapped;
309
+ for (const [i, item] of value.entries()) {
310
+ const next = isNode(item) ? transformNode(item, visitor, recurse, node) : item;
311
+ if (mapped) {
312
+ mapped.push(next);
313
+ continue;
314
+ }
315
+ if (next !== item) mapped = [...value.slice(0, i), next];
316
+ }
317
+ if (mapped) (updates ??= {})[key] = mapped;
318
+ continue;
319
+ }
320
+ if (isNode(value)) {
321
+ const next = transformNode(value, visitor, recurse, node);
322
+ if (next !== value) (updates ??= {})[key] = next;
323
+ continue;
324
+ }
325
+ if (isNodeRecord(value)) {
326
+ let mapped;
327
+ for (const [name, item] of Object.entries(value)) {
328
+ const next = isNode(item) ? transformNode(item, visitor, recurse, node) : item;
329
+ if (next !== item) (mapped ??= { ...value })[name] = next;
330
+ }
331
+ if (mapped) (updates ??= {})[key] = mapped;
332
+ }
333
+ }
334
+ if (!updates) return node;
335
+ return {
336
+ ...node,
337
+ ...updates
338
+ };
339
+ }
340
+ /**
341
+ * Lazy depth-first collection pass. Yields every non-null value returned by
342
+ * the visitor callbacks. Use `collectSync` for the eager array form.
290
343
  *
291
- * @example
344
+ * @example Collect every operationId
292
345
  * ```ts
293
- * createText('return fetch(id)')
294
- * // { kind: 'Text', value: 'return fetch(id)' }
346
+ * const ids: string[] = []
347
+ * for (const id of collect<string>(root, {
348
+ * operation(node) {
349
+ * return node.operationId
350
+ * },
351
+ * })) {
352
+ * ids.push(id)
353
+ * }
295
354
  * ```
296
355
  */
297
- const createText = textDef.create;
356
+ function* collect(node, options) {
357
+ const { depth, parent, ...visitor } = options;
358
+ yield* collectNode(node, visitor, (depth ?? visitorDepths.deep) === visitorDepths.deep, parent);
359
+ }
360
+ function* collectNode(node, visitor, recurse, parent) {
361
+ const v = applyVisitor(node, visitor, parent);
362
+ if (v != null) yield v;
363
+ for (const child of getChildren(node, recurse)) yield* collectNode(child, visitor, recurse, node);
364
+ }
298
365
  /**
299
- * Creates a {@link BreakNode} representing a line break in the source output.
366
+ * Eager depth-first collection pass. Gathers every non-null value the visitor
367
+ * callbacks return into an array.
300
368
  *
301
- * @example
369
+ * @example Collect every operationId
302
370
  * ```ts
303
- * createBreak()
304
- * // { kind: 'Break' }
371
+ * const ids = collectSync<string>(root, {
372
+ * operation(node) {
373
+ * return node.operationId
374
+ * },
375
+ * })
305
376
  * ```
306
377
  */
307
- function createBreak() {
308
- return breakDef.create();
378
+ function collectSync(node, options) {
379
+ return Array.from(collect(node, options));
309
380
  }
381
+ //#endregion
382
+ //#region src/defineMacro.ts
310
383
  /**
311
- * Creates a {@link JsxNode} representing a raw JSX fragment in the source output.
384
+ * Sort weight for an `enforce` hint. `pre` sorts before unmarked items and `post` after, so a plain
385
+ * list keeps its authored order.
386
+ */
387
+ function enforceWeight(enforce) {
388
+ if (enforce === "pre") return 0;
389
+ if (enforce === "post") return 2;
390
+ return 1;
391
+ }
392
+ /**
393
+ * Types a macro for inference and a single construction site, mirroring `defineExtension`.
394
+ * Adds no runtime behavior.
312
395
  *
313
396
  * @example
314
397
  * ```ts
315
- * createJsx('<>\n <a href={href}>Open</a>\n</>')
316
- * // { kind: 'Jsx', value: '<>\n <a href={href}>Open</a>\n</>' }
398
+ * const macroUntagged = defineMacro({
399
+ * name: 'untagged',
400
+ * operation(node) {
401
+ * return node.tags?.length ? undefined : { ...node, tags: ['untagged'] }
402
+ * },
403
+ * })
317
404
  * ```
318
405
  */
319
- const createJsx = jsxDef.create;
320
- //#endregion
321
- //#region src/nodes/content.ts
406
+ function defineMacro(macro) {
407
+ return macro;
408
+ }
322
409
  /**
323
- * Definition for the {@link ContentNode}.
410
+ * Runs every macro's callback for one node kind in order, chaining the result so each macro sees
411
+ * the previous macro's output. Returns `undefined` when nothing changed, so `transform` keeps the
412
+ * original reference (structural sharing).
324
413
  */
325
- const contentDef = defineNode({
326
- kind: "Content",
327
- children: ["schema"]
328
- });
414
+ function chain({ macros, key, node, context }) {
415
+ let current = node;
416
+ for (const macro of macros) {
417
+ const callback = macro[key];
418
+ if (!callback) continue;
419
+ if (macro.match && !macro.match(current)) continue;
420
+ const next = callback(current, context);
421
+ if (next != null) current = next;
422
+ }
423
+ return current === node ? void 0 : current;
424
+ }
329
425
  /**
330
- * Creates a `ContentNode` for a single request-body or response content type.
426
+ * Folds an ordered list of macros into a single {@link Visitor} that `transform` (and the per-plugin
427
+ * transform layer in `@kubb/core`) can run. Macros are stable-sorted by `enforce`, then applied
428
+ * sequentially per node so later macros see earlier output. This differs from a plain visitor, which
429
+ * has no names, ordering, or composition.
430
+ *
431
+ * @example
432
+ * ```ts
433
+ * const visitor = composeMacros([macroSimplifyUnion, macroDiscriminatorEnum])
434
+ * const next = transform(root, visitor)
435
+ * ```
436
+ */
437
+ function composeMacros(macros) {
438
+ const ordered = [...macros].sort((a, b) => enforceWeight(a.enforce) - enforceWeight(b.enforce));
439
+ const visitor = {};
440
+ for (const key of visitorKeys) {
441
+ if (!ordered.some((macro) => typeof macro[key] === "function")) continue;
442
+ const callback = (node, context) => chain({
443
+ macros: ordered,
444
+ key,
445
+ node,
446
+ context
447
+ });
448
+ visitor[key] = callback;
449
+ }
450
+ return visitor;
451
+ }
452
+ /**
453
+ * Runs a list of macros over a node tree and returns the rewritten tree. Keeps `transform`'s
454
+ * structural sharing, so an empty or no-op macro list returns the same reference. Pass
455
+ * `depth: 'shallow'` to rewrite the root node only.
456
+ *
457
+ * @example
458
+ * ```ts
459
+ * const next = applyMacros(root, [macroIntegerToString])
460
+ * ```
461
+ *
462
+ * @example Apply to the root node only
463
+ * ```ts
464
+ * const named = applyMacros(node, [macroEnumName({ parentName, propName, enumSuffix })], { depth: 'shallow' })
465
+ * ```
466
+ */
467
+ function applyMacros(root, macros, options) {
468
+ if (macros.length === 0) return root;
469
+ return transform(root, {
470
+ ...composeMacros(macros),
471
+ ...options
472
+ });
473
+ }
474
+ //#endregion
475
+ //#region src/optionality.ts
476
+ /**
477
+ * Generic JSON Schema optionality: a non-required field is optional, and a
478
+ * non-required nullable field is nullish.
479
+ */
480
+ function optionality(schema, required) {
481
+ const nullable = schema.nullable ?? false;
482
+ return {
483
+ ...schema,
484
+ optional: !required && !nullable ? true : void 0,
485
+ nullish: !required && nullable ? true : void 0
486
+ };
487
+ }
488
+ /**
489
+ * Defaults `required` to `false` and derives the schema's `optional`/`nullish` flags from it.
490
+ * Shared build step of `createProperty` and `createParameter`.
491
+ */
492
+ function withRequired(props) {
493
+ const required = props.required ?? false;
494
+ return {
495
+ ...props,
496
+ required,
497
+ schema: optionality(props.schema, required)
498
+ };
499
+ }
500
+ //#endregion
501
+ //#region src/createPrinter.ts
502
+ /**
503
+ * Creates a schema printer: a function that takes a `SchemaNode` and emits
504
+ * code in your target language. Each plugin that produces code from schemas
505
+ * (TypeScript types, Zod schemas, Faker factories) ships a printer built
506
+ * with this helper.
507
+ *
508
+ * The builder receives resolved options and returns:
509
+ *
510
+ * - `name` unique identifier for the printer.
511
+ * - `options` stored on the returned printer instance.
512
+ * - `nodes` map of `SchemaType` → handler. Handlers return the rendered
513
+ * output (a string, a TypeScript AST node, ...) for that schema type.
514
+ * - `overrides` (optional), user-supplied handlers that win over `nodes`.
515
+ * An override can call `this.base(node)` to reuse the handler it replaced.
516
+ * - `print` (optional), top-level override exposed as `printer.print`.
517
+ * Use `this.transform(node)` inside it to dispatch to `nodes` recursively.
518
+ *
519
+ * Without a `print` override, `printer.print` falls back to `printer.transform`
520
+ * (the node-level dispatcher).
521
+ *
522
+ * @example Tiny Zod printer
523
+ * ```ts
524
+ * import { createPrinter, type PrinterFactoryOptions } from '@kubb/ast'
525
+ *
526
+ * type PrinterZod = PrinterFactoryOptions<'zod', { strict?: boolean }, string>
527
+ *
528
+ * export const zodPrinter = createPrinter<PrinterZod>((options) => ({
529
+ * name: 'zod',
530
+ * options: { strict: options.strict ?? true },
531
+ * nodes: {
532
+ * string: () => 'z.string()',
533
+ * object(node) {
534
+ * const props = node.properties
535
+ * .map((p) => `${p.name}: ${this.transform(p.schema)}`)
536
+ * .join(', ')
537
+ * return `z.object({ ${props} })`
538
+ * },
539
+ * },
540
+ * }))
541
+ * ```
331
542
  */
332
- const createContent = contentDef.create;
543
+ function createPrinter(build) {
544
+ return (options) => {
545
+ const { name, options: resolvedOptions, nodes, overrides, print: printOverride } = build(options ?? {});
546
+ const merged = overrides ? {
547
+ ...nodes,
548
+ ...overrides
549
+ } : nodes;
550
+ const collectedImports = [];
551
+ const dispatch = (table) => (node) => {
552
+ const handler = table[node.type];
553
+ if (!handler) return null;
554
+ return handler.call(context, node);
555
+ };
556
+ const context = {
557
+ options: resolvedOptions,
558
+ import: (node) => {
559
+ collectedImports.push(node);
560
+ },
561
+ transform: dispatch(merged),
562
+ base: dispatch(nodes)
563
+ };
564
+ return {
565
+ name,
566
+ options: resolvedOptions,
567
+ transform: context.transform,
568
+ print: printOverride ? printOverride.bind(context) : context.transform,
569
+ drainImports: () => collectedImports.splice(0)
570
+ };
571
+ };
572
+ }
333
573
  //#endregion
334
574
  //#region src/utils/extractStringsFromNodes.ts
335
575
  /**
@@ -363,1278 +603,909 @@ function extractStringsFromNodes(nodes) {
363
603
  return collected.join("\n");
364
604
  }
365
605
  //#endregion
366
- //#region src/utils/combineFileMembers.ts
367
- const IDENTIFIER_RUN = /[\w$]+/g;
368
- /**
369
- * How many imports a file needs before indexing the source beats scanning it once per name.
370
- */
371
- const INDEX_ABOVE_IMPORTS = 128;
372
- /**
373
- * Returns the local binding an import name item introduces: the alias when there is one, else the
374
- * imported name.
375
- */
376
- function importLocalName(item) {
377
- return typeof item === "string" ? item : item.name ?? item.propertyName;
378
- }
606
+ //#region src/utils/refs.ts
379
607
  /**
380
- * Computes a multi-level sort key for exports and imports:
381
- * non-array names first (wildcards/namespace aliases). Type-only before value. Alphabetical path. Unnamed before named.
608
+ * Resolves the emitted name of the schema a ref node points at. Prefers `targetName` (set when
609
+ * the referenced schema was renamed, e.g. to break a collision), then the last segment of `ref`,
610
+ * then `name`, then the nested `schema.name`.
611
+ *
612
+ * Returns `null` for non-ref nodes or when no name resolves.
613
+ *
614
+ * @example
615
+ * `resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Pet' }) // 'Pet'`
616
+ *
617
+ * @example Collision-renamed target
618
+ * `resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Order', targetName: 'OrderSchema' }) // 'OrderSchema'`
382
619
  */
383
- function sortKey(node) {
384
- const isArray = Array.isArray(node.name) ? "1" : "0";
385
- const typeOnly = node.isTypeOnly ? "0" : "1";
386
- const hasName = node.name != null ? "1" : "0";
387
- const name = Array.isArray(node.name) ? node.name.toSorted().join("\0") : node.name ?? "";
388
- return `${isArray}:${typeOnly}:${node.path}:${hasName}:${name}`;
620
+ function resolveRefName(node) {
621
+ if (!node || node.type !== "ref") return null;
622
+ if (node.targetName) return node.targetName;
623
+ if (node.ref) return node.ref.split("/").at(-1) ?? node.ref;
624
+ return node.name ?? node.schema?.name ?? null;
389
625
  }
390
626
  /**
391
- * Sorts members by {@link sortKey}. Keys are computed once per node, not per comparison.
627
+ * Merges a ref node with its resolved schema, giving the fields set at the usage site precedence.
628
+ *
629
+ * Every field on the ref except `kind`, `type`, `name`, `ref`, and `schema` overrides the same field
630
+ * of the resolved `schema` (for example `description`, `nullable`, `readOnly`, `deprecated`). Fields
631
+ * left `undefined` on the ref do not shadow the resolved schema. A ref without a resolved `schema`
632
+ * is returned as is.
633
+ *
634
+ * @example
635
+ * ```ts
636
+ * const ref = createSchema({ type: 'ref', ref: '#/components/schemas/Pet', description: 'A pet', schema: petSchema })
637
+ * mergeRefWithSchema(ref) // petSchema's fields with description 'A pet'
638
+ * ```
392
639
  */
393
- function sortMembers(nodes) {
394
- return nodes.map((node) => ({
395
- node,
396
- key: sortKey(node)
397
- })).sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0).map(({ node }) => node);
640
+ function mergeRefWithSchema(node) {
641
+ if (!node.schema) return node;
642
+ const { kind: _kind, type: _type, name: _name, ref: _ref, schema: _schema, ...overrides } = node;
643
+ const definedOverrides = Object.fromEntries(Object.entries(overrides).filter(([, value]) => value !== void 0));
644
+ return {
645
+ ...node.schema,
646
+ ...definedOverrides
647
+ };
398
648
  }
399
649
  /**
400
- * Returns a collector for array-named members. A member whose path and `isTypeOnly` were already
401
- * seen folds its names into that entry, otherwise it joins `result` as a new entry.
650
+ * Returns `true` when a schema is a bare ref without modifiers, metadata, or omit keys.
651
+ *
652
+ * Modifiers inherited from the resolved schema are included in the check because a ref emits the
653
+ * referenced schema variable with those modifiers already applied.
402
654
  */
403
- function mergeByPath(result) {
404
- const byPath = /* @__PURE__ */ new Map();
405
- return ({ node, names }) => {
406
- const key = `${node.path}:${node.isTypeOnly ?? false}`;
407
- const existing = byPath.get(key);
408
- if (existing && Array.isArray(existing.name)) {
409
- existing.name = [.../* @__PURE__ */ new Set([...existing.name, ...names])];
410
- return;
411
- }
412
- const item = {
413
- ...node,
414
- name: names
415
- };
416
- result.push(item);
417
- byPath.set(key, item);
418
- };
655
+ function isBareRef(node, keysToOmit) {
656
+ if (!node || node.type !== "ref") return false;
657
+ const meta = mergeRefWithSchema(node);
658
+ return !meta.nullable && !meta.optional && !meta.nullish && meta.default === void 0 && !meta.description && !meta.examples?.length && !keysToOmit?.length;
419
659
  }
660
+ //#endregion
661
+ //#region ../../internals/utils/src/promise.ts
420
662
  /**
421
- * Deduplicates `SourceNode` objects by `name + isExportable + isTypeOnly`, keeping the first of each
422
- * key. Unnamed sources fall back to their extracted node strings as the name part of the key. Returns
423
- * the deduplicated array in original order.
424
- */
425
- function combineSources(sources) {
426
- const seen = /* @__PURE__ */ new Map();
427
- for (const source of sources) {
428
- const key = `${source.name ?? extractStringsFromNodes(source.nodes)}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}`;
429
- if (!seen.has(key)) seen.set(key, source);
430
- }
431
- return [...seen.values()];
432
- }
433
- /**
434
- * Deduplicates and merges `ExportNode` objects by path and type.
435
- *
436
- * Named exports with the same path and `isTypeOnly` flag have their names merged into a single export.
437
- * Non-array exports are deduplicated by exact identity. Returns a sorted, deduplicated array.
438
- */
439
- function combineExports(exports) {
440
- const result = [];
441
- const merge = mergeByPath(result);
442
- const seen = /* @__PURE__ */ new Set();
443
- for (const curr of sortMembers(exports)) {
444
- const { name, path, isTypeOnly, asAlias } = curr;
445
- if (Array.isArray(name)) {
446
- if (name.length) merge({
447
- node: curr,
448
- names: [...new Set(name)]
449
- });
450
- continue;
451
- }
452
- const key = `${path}:${name ?? ""}:${isTypeOnly ?? false}:${asAlias ?? ""}`;
453
- if (!seen.has(key)) {
454
- result.push(curr);
455
- seen.add(key);
456
- }
457
- }
458
- return result;
459
- }
460
- /**
461
- * Deduplicates and merges `ImportNode` objects, filtering out unused imports.
462
- *
463
- * Retains imports that are referenced in `source` or re-exported. Imports with the same path and
464
- * `isTypeOnly` flag have their names merged. Returns a sorted, deduplicated, filtered array.
465
- */
466
- function combineImports(imports, exports, source) {
467
- const exportedNames = new Set(exports.flatMap((e) => Array.isArray(e.name) ? e.name : e.name ? [e.name] : []));
468
- const identifiers = source && imports.length > INDEX_ABOVE_IMPORTS ? new Set(source.match(IDENTIFIER_RUN)) : null;
469
- const isUsed = (importName) => !source || identifiers?.has(importName) || source.includes(importName) || exportedNames.has(importName);
470
- const importNameMemo = /* @__PURE__ */ new Map();
471
- const canonicalizeName = (n) => {
472
- if (typeof n === "string") return n;
473
- const key = `${n.propertyName}:${n.name ?? ""}`;
474
- if (!importNameMemo.has(key)) importNameMemo.set(key, n);
475
- return importNameMemo.get(key);
476
- };
477
- const pathsWithUsedNamedImport = /* @__PURE__ */ new Set();
478
- for (const node of imports) if (Array.isArray(node.name) && node.name.some((item) => isUsed(importLocalName(item)))) pathsWithUsedNamedImport.add(node.path);
479
- const result = [];
480
- const merge = mergeByPath(result);
481
- const seen = /* @__PURE__ */ new Set();
482
- for (const curr of sortMembers(imports)) {
483
- if (curr.path === curr.root) continue;
484
- const { name, path, isTypeOnly } = curr;
485
- if (Array.isArray(name)) {
486
- const names = [...new Set(name.map(canonicalizeName))].filter((item) => isUsed(importLocalName(item)));
487
- if (names.length) merge({
488
- node: curr,
489
- names
490
- });
491
- continue;
492
- }
493
- if (!isUsed(name) && !pathsWithUsedNamedImport.has(path)) continue;
494
- const key = `${path}:${name}:${isTypeOnly ?? false}`;
495
- if (!seen.has(key)) {
496
- result.push(curr);
497
- seen.add(key);
498
- }
499
- }
500
- return result;
501
- }
502
- //#endregion
503
- //#region src/nodes/file.ts
504
- /**
505
- * Definition for the {@link ImportNode}.
506
- */
507
- const importDef = defineNode({ kind: "Import" });
508
- /**
509
- * Definition for the {@link ExportNode}.
510
- */
511
- const exportDef = defineNode({ kind: "Export" });
512
- /**
513
- * Definition for the {@link SourceNode}.
514
- */
515
- const sourceDef = defineNode({ kind: "Source" });
516
- /**
517
- * Definition for the {@link FileNode}. The fully resolved builder lives in
518
- * `createFile`, so this definition only supplies the guard.
519
- */
520
- const fileDef = defineNode({ kind: "File" });
521
- /**
522
- * Creates an `ImportNode` representing a language-agnostic import/dependency declaration.
663
+ * Wraps `factory` with a keyed cache backed by the provided store.
523
664
  *
524
- * @example Named import
525
- * ```ts
526
- * createImport({ name: ['useState'], path: 'react' })
527
- * // import { useState } from 'react'
528
- * ```
529
- */
530
- const createImport = importDef.create;
531
- /**
532
- * Creates an `ExportNode` representing a language-agnostic export/public API declaration.
665
+ * Pass a `WeakMap` for object keys (results are GC-eligible when the key is
666
+ * collected) or a `Map` for primitive keys. For multi-argument functions,
667
+ * nest two `memoize` calls — the outer keyed by the first argument, the
668
+ * inner (created once per outer miss) keyed by the second.
533
669
  *
534
- * @example Named export
535
- * ```ts
536
- * createExport({ name: ['Pet'], path: './Pet' })
537
- * // export { Pet } from './Pet'
538
- * ```
539
- */
540
- const createExport = exportDef.create;
541
- /**
542
- * Creates a `SourceNode` representing a fragment of source code within a file.
670
+ * Because the cache is owned by the caller, it can be shared, inspected, or
671
+ * cleared independently of the memoized function.
543
672
  *
544
- * @example
673
+ * @example Single WeakMap key
545
674
  * ```ts
546
- * createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')], isExportable: true })
675
+ * const cache = new WeakMap<SchemaNode, Set<string>>()
676
+ * const getRefs = memoize(cache, (node) => collectRefs(node))
547
677
  * ```
548
- */
549
- const createSource = sourceDef.create;
550
- /**
551
- * Creates a fully resolved `FileNode` from a file input descriptor.
552
678
  *
553
- * Computes:
554
- * - `id` SHA256 hash of the file path
555
- * - `name` `baseName` without extension
556
- * - `extname` extension extracted from `baseName`
557
- *
558
- * Deduplicates:
559
- * - `sources` via `combineSources`
560
- * - `exports` via `combineExports`
561
- * - `imports` via `combineImports` (also filters unused imports)
562
- *
563
- * @throws {Error} when `baseName` has no extension.
564
- *
565
- * @example
679
+ * @example Single Map key (primitive)
566
680
  * ```ts
567
- * const file = createFile({
568
- * baseName: 'petStore.ts',
569
- * path: 'src/models/petStore.ts',
570
- * sources: [createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')] })],
571
- * imports: [createImport({ name: ['z'], path: 'zod' })],
572
- * exports: [createExport({ name: ['Pet'], path: './petStore' })],
573
- * })
574
- * // file.id = SHA256 hash of 'src/models/petStore.ts'
575
- * // file.name = 'petStore'
576
- * // file.extname = '.ts'
681
+ * const cache = new Map<string, Resolver>()
682
+ * const getResolver = memoize(cache, (name) => buildResolver(name))
577
683
  * ```
578
684
  *
579
- * @example Copy a real file into the output
685
+ * @example Two-level (object + primitive)
580
686
  * ```ts
581
- * const file = createFile({
582
- * baseName: 'client.ts',
583
- * path: 'src/gen/client.ts',
584
- * copy: '/abs/path/to/templates/client.ts',
585
- * })
687
+ * const outer = new WeakMap<Params[], Map<string, Params[]>>()
688
+ * const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
689
+ * fn(params)('camelcase')
586
690
  * ```
587
691
  */
588
- function createFile(input) {
589
- const extname = path.extname(input.baseName);
590
- if (!extname) throw new Error(`No extname found for ${input.baseName}`);
591
- const resolvedExports = input.exports?.length ? combineExports(input.exports) : [];
592
- const resolvedImports = (() => {
593
- if (!input.imports?.length) return [];
594
- const sourceParts = [];
595
- const localNames = /* @__PURE__ */ new Set();
596
- for (const item of input.sources ?? []) {
597
- const extracted = item.nodes && extractStringsFromNodes(item.nodes);
598
- if (extracted) sourceParts.push(extracted);
599
- if (item.name) localNames.add(item.name);
600
- }
601
- const source = sourceParts.join("\n") || void 0;
602
- return combineImports(input.imports, resolvedExports, source).flatMap((imp) => {
603
- if (imp.path === input.path) return [];
604
- if (!Array.isArray(imp.name)) return typeof imp.name === "string" && localNames.has(imp.name) ? [] : [imp];
605
- const kept = imp.name.filter((item) => !localNames.has(importLocalName(item)));
606
- if (!kept.length) return [];
607
- return [kept.length === imp.name.length ? imp : {
608
- ...imp,
609
- name: kept
610
- }];
611
- });
612
- })();
613
- const resolvedSources = input.sources?.length ? combineSources(input.sources) : [];
614
- return {
615
- kind: "File",
616
- ...input,
617
- id: hash("sha256", input.path, "hex"),
618
- name: path.basename(input.baseName, extname),
619
- extname,
620
- imports: resolvedImports,
621
- exports: resolvedExports,
622
- sources: resolvedSources,
623
- meta: input.meta ?? {}
692
+ function memoize(store, factory) {
693
+ return (key) => {
694
+ if (store.has(key)) return store.get(key);
695
+ const value = factory(key);
696
+ store.set(key, value);
697
+ return value;
624
698
  };
625
699
  }
626
700
  //#endregion
627
- //#region src/nodes/input.ts
628
- /**
629
- * Definition for the {@link InputNode}.
630
- */
631
- const inputDef = defineNode({
632
- kind: "Input",
633
- build: (props) => ({
634
- schemas: [],
635
- operations: [],
636
- meta: {
637
- circularNames: [],
638
- enumNames: []
639
- },
640
- ...props
641
- }),
642
- children: ["schemas", "operations"],
643
- visitorKey: "input"
644
- });
701
+ //#region src/utils/schemaGraph.ts
645
702
  /**
646
- * Creates an `InputNode`, defaulting `schemas`/`operations` to empty arrays and `meta` per
647
- * {@link inputDef}.
703
+ * Collects the names of every schema a node's subtree references, resolved with `resolveRefName`.
704
+ * Each ref contributes its name only, so the schema it points to is never traversed. The walk is
705
+ * memoized by node identity, which lets `findCircularSchemas` and `containsCircularRef` share one
706
+ * scan per schema.
648
707
  *
649
708
  * @example
650
709
  * ```ts
651
- * const input = createInput()
652
- * // { kind: 'Input', schemas: [], operations: [] }
710
+ * collectSchemaRefs(petSchema)
711
+ * // Set { 'Category', 'Tag' }
653
712
  * ```
654
713
  */
655
- function createInput(overrides = {}) {
656
- return inputDef.create(overrides);
657
- }
658
- //#endregion
659
- //#region src/nodes/requestBody.ts
660
- /**
661
- * Definition for the {@link RequestBodyNode}. Content entries are built upfront with
662
- * {@link createContent}, mirroring how `parameters` and `responses` take prebuilt nodes.
663
- */
664
- const requestBodyDef = defineNode({
665
- kind: "RequestBody",
666
- children: ["content"]
667
- });
668
- /**
669
- * Creates a `RequestBodyNode`.
670
- */
671
- const createRequestBody = requestBodyDef.create;
672
- //#endregion
673
- //#region src/nodes/operation.ts
674
- /**
675
- * Definition for the {@link OperationNode}. HTTP operations (those carrying both
676
- * `method` and `path`) are tagged with `protocol: 'http'`, and the request body is
677
- * normalized into a `RequestBodyNode`.
678
- */
679
- const operationDef = defineNode({
680
- kind: "Operation",
681
- build: (props) => {
682
- const { requestBody, ...rest } = props;
683
- const isHttp = rest.method !== void 0 && rest.path !== void 0;
684
- return {
685
- tags: [],
686
- parameters: [],
687
- responses: [],
688
- ...rest,
689
- ...isHttp ? { protocol: "http" } : {},
690
- requestBody: requestBody ? createRequestBody(requestBody) : void 0
691
- };
692
- },
693
- children: [
694
- "parameters",
695
- "requestBody",
696
- "responses"
697
- ],
698
- visitorKey: "operation"
699
- });
700
- function createOperation(props) {
701
- return operationDef.create(props);
702
- }
703
- //#endregion
704
- //#region src/nodes/output.ts
705
- /**
706
- * Definition for the {@link OutputNode}.
707
- */
708
- const outputDef = defineNode({
709
- kind: "Output",
710
- defaults: { files: [] },
711
- visitorKey: "output"
714
+ const collectSchemaRefs = memoize(/* @__PURE__ */ new WeakMap(), (node) => {
715
+ const refs = /* @__PURE__ */ new Set();
716
+ collectSync(node, { schema(child) {
717
+ if (child.type === "ref") {
718
+ const name = resolveRefName(child);
719
+ if (name) refs.add(name);
720
+ }
721
+ } });
722
+ return refs;
712
723
  });
713
724
  /**
714
- * Creates an `OutputNode` with a stable default for `files`.
725
+ * Collects the de-duplicated target names of every pointer-carrying ref in a node's subtree, in
726
+ * first-occurrence order. The walk is memoized by node identity, so the subtree is scanned once and
727
+ * `resolver.imports` reads the same result across the ts, zod, and faker plugins instead of
728
+ * re-scanning the same schema per plugin.
715
729
  *
716
- * @example
717
- * ```ts
718
- * const output = createOutput()
719
- * // { kind: 'Output', files: [] }
720
- * ```
721
- */
722
- function createOutput(overrides = {}) {
723
- return outputDef.create(overrides);
724
- }
725
- //#endregion
726
- //#region src/optionality.ts
727
- /**
728
- * Generic JSON Schema optionality: a non-required field is optional, and a
729
- * non-required nullable field is nullish.
730
- */
731
- function optionality(schema, required) {
732
- const nullable = schema.nullable ?? false;
733
- return {
734
- ...schema,
735
- optional: !required && !nullable ? true : void 0,
736
- nullish: !required && nullable ? true : void 0
737
- };
738
- }
739
- /**
740
- * Defaults `required` to `false` and derives the schema's `optional`/`nullish` flags from it.
741
- * Shared build step of `createProperty` and `createParameter`.
742
- */
743
- function withRequired(props) {
744
- const required = props.required ?? false;
745
- return {
746
- ...props,
747
- required,
748
- schema: optionality(props.schema, required)
749
- };
750
- }
751
- //#endregion
752
- //#region src/nodes/parameter.ts
753
- /**
754
- * Definition for the {@link ParameterNode}. `required` defaults to `false`, and the schema's
755
- * `optional`/`nullish` flags are derived from it through {@link withRequired}.
756
- */
757
- const parameterDef = defineNode({
758
- kind: "Parameter",
759
- build: withRequired,
760
- children: ["schema"],
761
- visitorKey: "parameter"
762
- });
763
- /**
764
- * Creates a `ParameterNode`.
730
+ * Only refs that carry a `$ref` pointer count, so a synthesized ref pointing at a sibling in the
731
+ * same file (a union member created by name) is left out. That leaves exactly the set
732
+ * `resolver.imports` emits.
765
733
  *
766
734
  * @example
767
735
  * ```ts
768
- * const param = createParameter({
769
- * name: 'petId',
770
- * in: 'path',
771
- * required: true,
772
- * schema: createSchema({ type: 'string' }),
773
- * })
736
+ * collectImportedRefNames(petSchema)
737
+ * // ['Category', 'Tag']
774
738
  * ```
775
739
  */
776
- const createParameter = parameterDef.create;
777
- //#endregion
778
- //#region src/nodes/property.ts
779
- /**
780
- * Definition for the {@link PropertyNode}. `required` defaults to `false`, and the schema's
781
- * `optional`/`nullish` flags are derived from it through {@link withRequired}.
782
- */
783
- const propertyDef = defineNode({
784
- kind: "Property",
785
- build: withRequired,
786
- children: ["schema"],
787
- visitorKey: "property"
740
+ const collectImportedRefNames = memoize(/* @__PURE__ */ new WeakMap(), (node) => {
741
+ const seen = /* @__PURE__ */ new Set();
742
+ const names = [];
743
+ collectSync(node, { schema(child) {
744
+ if (child.type !== "ref" || !child.ref) return;
745
+ const name = resolveRefName(child);
746
+ if (name && !seen.has(name)) {
747
+ seen.add(name);
748
+ names.push(name);
749
+ }
750
+ } });
751
+ return names;
788
752
  });
789
753
  /**
790
- * Creates a `PropertyNode`.
754
+ * Collects the names of all top-level schemas transitively used by a set of operations.
791
755
  *
792
- * @example
793
- * ```ts
794
- * const property = createProperty({
795
- * name: 'status',
796
- * required: true,
797
- * schema: createSchema({ type: 'string', nullable: true }),
798
- * })
799
- * // required=true, no optional/nullish
800
- * ```
801
- */
802
- const createProperty = propertyDef.create;
803
- //#endregion
804
- //#region src/nodes/response.ts
805
- /**
806
- * Definition for the {@link ResponseNode}. A single legacy `schema` (with optional
807
- * `mediaType`/`keysToOmit`) is normalized into one `content` entry.
808
- */
809
- const responseDef = defineNode({
810
- kind: "Response",
811
- build: (props) => {
812
- const { schema, mediaType, keysToOmit, content, ...rest } = props;
813
- const entries = content ?? (schema ? [createContent({
814
- contentType: mediaType ?? "application/json",
815
- schema,
816
- keysToOmit: keysToOmit ?? null
817
- })] : void 0);
818
- return {
819
- ...rest,
820
- content: entries
821
- };
822
- },
823
- children: ["content"],
824
- visitorKey: "response"
825
- });
826
- /**
827
- * Creates a `ResponseNode`.
756
+ * An operation uses a schema when its parameters, request body, or responses reference it, directly
757
+ * or through other named schemas. Once a name is added to the result it is not revisited, so
758
+ * reference cycles terminate.
828
759
  *
829
- * @example
760
+ * Pair it with `include` filters so schemas reachable only from excluded operations stay ungenerated.
761
+ *
762
+ * @example Only generate schemas referenced by included operations
830
763
  * ```ts
831
- * const response = createResponse({
832
- * statusCode: '200',
833
- * content: [createContent({ contentType: 'application/json', schema: createSchema({ type: 'object', properties: [] }) })],
834
- * })
764
+ * const includedOps = operations.filter((op) => resolver.default.options(op, { options, include }) !== null)
765
+ * const allowed = collectUsedSchemaNames(includedOps, schemas)
766
+ *
767
+ * for (const schema of schemas) {
768
+ * if (schema.name && !allowed.has(schema.name)) continue
769
+ * // generate schema
770
+ * }
835
771
  * ```
836
772
  */
837
- const createResponse = responseDef.create;
838
- //#endregion
839
- //#region src/nodes/schema.ts
840
- /**
841
- * Maps schema `type` to its underlying `primitive`.
842
- * Primitive types map to themselves and special string formats map to `'string'`.
843
- * Any type not listed here (such as `ref`, `enum`, `union`, `intersection`, `tuple`, `ipv4`, `ipv6`, `blob`) has no `primitive`.
844
- */
845
- const TYPE_TO_PRIMITIVE = {
846
- string: "string",
847
- number: "number",
848
- integer: "integer",
849
- bigint: "bigint",
850
- boolean: "boolean",
851
- null: "null",
852
- any: "any",
853
- unknown: "unknown",
854
- void: "void",
855
- never: "never",
856
- object: "object",
857
- array: "array",
858
- date: "date",
859
- uuid: "string",
860
- email: "string",
861
- url: "string",
862
- datetime: "string",
863
- time: "string"
864
- };
865
- /**
866
- * Definition for the {@link SchemaNode}. Object schemas default `properties` to an
867
- * empty array, and `primitive` is inferred from `type` when not explicitly provided.
868
- */
869
- const schemaDef = defineNode({
870
- kind: "Schema",
871
- build: (props) => {
872
- if (props.type === "object") return {
873
- properties: [],
874
- primitive: "object",
875
- ...props
876
- };
877
- return {
878
- primitive: TYPE_TO_PRIMITIVE[props.type],
879
- ...props
880
- };
881
- },
882
- children: [
883
- "properties",
884
- "items",
885
- "members",
886
- "additionalProperties",
887
- "patternProperties",
888
- "rest"
889
- ],
890
- visitorKey: "schema"
891
- });
892
- function createSchema(props) {
893
- return schemaDef.create(props);
894
- }
895
- //#endregion
896
- //#region src/registry.ts
897
- /**
898
- * Every node definition. Adding a node means adding its `defineNode` to one
899
- * `nodes/*.ts` file and listing it here. The visitor tables in `visitor.ts` derive from it.
900
- */
901
- const nodeDefs = [
902
- inputDef,
903
- outputDef,
904
- operationDef,
905
- requestBodyDef,
906
- contentDef,
907
- responseDef,
908
- schemaDef,
909
- propertyDef,
910
- parameterDef,
911
- constDef,
912
- typeDef,
913
- functionDef,
914
- arrowFunctionDef,
915
- textDef,
916
- breakDef,
917
- jsxDef,
918
- importDef,
919
- exportDef,
920
- sourceDef,
921
- fileDef
922
- ];
923
- //#endregion
924
- //#region src/visitor.ts
925
- /**
926
- * Child node fields per node kind, in traversal order (Babel's `VISITOR_KEYS`).
927
- * Derived from each definition's `children`.
928
- */
929
- const VISITOR_KEYS = Object.fromEntries(nodeDefs.flatMap((def) => def.children ? [[def.kind, def.children]] : []));
930
- /**
931
- * Maps a node kind to the matching visitor callback name. Derived from each
932
- * definition's `visitorKey`.
933
- */
934
- const VISITOR_KEY_BY_KIND = Object.fromEntries(nodeDefs.flatMap((def) => def.visitorKey ? [[def.kind, def.visitorKey]] : []));
935
- const visitorKeysByKind = VISITOR_KEYS;
936
- /**
937
- * Returns `true` when `value` is an AST node (an object carrying a `kind`).
938
- */
939
- function isNode(value) {
940
- return typeof value === "object" && value !== null && typeof value.kind === "string";
941
- }
942
- /**
943
- * Returns `true` for a plain object that maps names to nodes, such as `patternProperties`.
944
- */
945
- function isNodeRecord(value) {
946
- return typeof value === "object" && value !== null && !Array.isArray(value);
947
- }
948
- /**
949
- * Returns the child fields of `node` to walk, from {@link VISITOR_KEYS}, or `undefined` when there
950
- * is nothing to walk. `Schema` children are only walked when `recurse` is `true`.
951
- */
952
- function childKeys(node, recurse) {
953
- if (node.kind === "Schema" && !recurse) return void 0;
954
- return visitorKeysByKind[node.kind];
773
+ function collectUsedSchemaNames(operations, schemas) {
774
+ const schemaMap = /* @__PURE__ */ new Map();
775
+ for (const schema of schemas) if (schema.name) schemaMap.set(schema.name, schema);
776
+ const result = /* @__PURE__ */ new Set();
777
+ function visitSchema(schema) {
778
+ for (const name of collectSchemaRefs(schema)) if (!result.has(name)) {
779
+ result.add(name);
780
+ const namedSchema = schemaMap.get(name);
781
+ if (namedSchema) visitSchema(namedSchema);
782
+ }
783
+ }
784
+ for (const op of operations) for (const schema of collect(op, {
785
+ depth: "shallow",
786
+ schema: (node) => node
787
+ })) visitSchema(schema);
788
+ return result;
955
789
  }
956
790
  /**
957
- * Returns the immediate traversable children of `node`.
791
+ * Finds every schema that takes part in a circular dependency chain in a schema dependency graph
792
+ * that maps each schema name to the names it references directly.
958
793
  *
959
- * @example
960
- * ```ts
961
- * const children = getChildren(operationNode, true)
962
- * // returns parameters, the request body, and responses
963
- * ```
964
- */
965
- function* getChildren(node, recurse) {
966
- const keys = childKeys(node, recurse);
967
- if (!keys) return;
968
- const record = node;
969
- for (const key of keys) {
970
- const value = record[key];
971
- if (Array.isArray(value)) {
972
- for (const item of value) if (isNode(item)) yield item;
973
- continue;
974
- }
975
- if (isNode(value)) {
976
- yield value;
977
- continue;
978
- }
979
- if (isNodeRecord(value)) {
980
- for (const item of Object.values(value)) if (isNode(item)) yield item;
794
+ * Use this when the graph was already collected during another pass (e.g. the adapter's convert
795
+ * walk), so the schema nodes are not swept a second time. `findCircularSchemas` builds the graph
796
+ * from schema nodes and delegates here.
797
+ *
798
+ * @example
799
+ * ```ts
800
+ * const graph = new Map([
801
+ * ['Pet', new Set(['Category'])],
802
+ * ['Category', new Set(['Pet'])],
803
+ * ])
804
+ * findCircularSchemasFromGraph(graph) // Set { 'Pet', 'Category' }
805
+ * ```
806
+ */
807
+ function findCircularSchemasFromGraph(graph) {
808
+ const circular = /* @__PURE__ */ new Set();
809
+ for (const start of graph.keys()) {
810
+ const visited = /* @__PURE__ */ new Set();
811
+ const stack = [...graph.get(start) ?? []];
812
+ while (stack.length > 0) {
813
+ const node = stack.pop();
814
+ if (node === start) {
815
+ circular.add(start);
816
+ break;
817
+ }
818
+ if (visited.has(node)) continue;
819
+ visited.add(node);
820
+ const next = graph.get(node);
821
+ if (next) for (const r of next) stack.push(r);
981
822
  }
982
823
  }
824
+ return circular;
983
825
  }
984
826
  /**
985
- * Runs the visitor callback that matches `node.kind` with the traversal
986
- * context. The result is a replacement node, a collected value, or `undefined`
987
- * when no callback is registered for the kind.
827
+ * Finds every schema that takes part in a circular dependency chain, including direct self-loops.
988
828
  *
989
- * Shared by `transform` and `collect` so node-kind dispatch lives in one place.
990
- * `TResult` is the caller's expected return: the same node type for `transform`,
991
- * the collected value type for `collect`.
829
+ * Wrap the returned schema positions in a deferred construct (a lazy getter or `z.lazy(() => …)`) so
830
+ * the generated code does not recurse forever. Refs are followed by name only, so the walk stays
831
+ * linear in the size of the schema graph.
832
+ *
833
+ * @note Call this once on the full graph, then check individual schemas with `containsCircularRef()`.
992
834
  */
993
- function applyVisitor(node, visitor, parent) {
994
- const key = VISITOR_KEY_BY_KIND[node.kind];
995
- if (!key) return void 0;
996
- const fn = visitor[key];
997
- return fn?.(node, { parent });
835
+ function findCircularSchemas(schemas) {
836
+ const graph = /* @__PURE__ */ new Map();
837
+ for (const schema of schemas) if (schema.name) graph.set(schema.name, collectSchemaRefs(schema));
838
+ return findCircularSchemasFromGraph(graph);
998
839
  }
840
+ //#endregion
841
+ //#region src/utils/schemaProperties.ts
999
842
  /**
1000
- * Synchronous depth-first transform. Each visitor callback can return a
1001
- * replacement node. Returning `undefined` keeps the original.
843
+ * Finds every matching property exposed by an object, resolved reference, or intersection.
1002
844
  *
1003
- * The original tree is never mutated, a new tree is returned. Pass
1004
- * `depth: 'shallow'` to skip recursion into children.
845
+ * References are followed through their parsed `schema`. Circular references terminate without
846
+ * revisiting a node. Unions are not traversed because a property on one branch does not describe
847
+ * every branch.
1005
848
  *
1006
- * @example Prefix every operationId
849
+ * @example Faker discriminator narrowing
1007
850
  * ```ts
1008
- * const next = transform(root, {
1009
- * operation(node) {
1010
- * return { ...node, operationId: `prefixed_${node.operationId}` }
1011
- * },
1012
- * })
851
+ * const values = resolveSchemaProperties({ node: member, propertyName: union.discriminatorPropertyName }).flatMap(({ schema }) =>
852
+ * getSchemaLiteralValues(schema),
853
+ * )
854
+ * const typeName = `Extract<Pet, { type: ${values.map(JSON.stringify).join(' | ')} }>`
1013
855
  * ```
1014
856
  *
1015
- * @example Replace only the root node
857
+ * @example Zod discriminated unions
1016
858
  * ```ts
1017
- * const next = transform(root, {
1018
- * depth: 'shallow',
1019
- * input: (node) => ({ ...node, meta: { ...node.meta, title: 'Rewritten' } }),
1020
- * })
859
+ * const canDiscriminate = union.members?.every(
860
+ * (member) => resolveSchemaProperties({ node: member, propertyName: 'type' }).length > 0,
861
+ * )
862
+ * const expression = canDiscriminate ? `z.discriminatedUnion('type', members)` : `z.union(members)`
863
+ * ```
864
+ *
865
+ * @example TypeScript property lookup
866
+ * ```ts
867
+ * const properties = resolveSchemaProperties({ node: schema, propertyName: 'status' })
868
+ * const statusSchemas = properties.map((property) => print(property.schema))
1021
869
  * ```
1022
870
  */
1023
- function transform(node, options) {
1024
- const { depth, parent, ...visitor } = options;
1025
- return transformNode(node, visitor, (depth ?? visitorDepths.deep) === visitorDepths.deep, parent);
1026
- }
1027
- /**
1028
- * Visits a single node, then immutably rebuilds its children. Returns the original
1029
- * reference when neither the visitor nor the child rebuild changed anything, so callers
1030
- * can detect "nothing changed" by identity and ancestors avoid reallocating.
1031
- */
1032
- function transformNode(node, visitor, recurse, parent) {
1033
- return transformChildren(applyVisitor(node, visitor, parent) ?? node, visitor, recurse);
1034
- }
1035
- /**
1036
- * Immutably rebuilds a node's children, transforming each child node and leaving non-node values
1037
- * (e.g. `additionalProperties: true`) intact.
1038
- */
1039
- function transformChildren(node, visitor, recurse) {
1040
- const keys = childKeys(node, recurse);
1041
- if (!keys) return node;
1042
- const record = node;
1043
- let updates;
1044
- for (const key of keys) {
1045
- if (!(key in record)) continue;
1046
- const value = record[key];
1047
- if (Array.isArray(value)) {
1048
- let mapped;
1049
- for (const [i, item] of value.entries()) {
1050
- const next = isNode(item) ? transformNode(item, visitor, recurse, node) : item;
1051
- if (mapped) {
1052
- mapped.push(next);
1053
- continue;
1054
- }
1055
- if (next !== item) mapped = [...value.slice(0, i), next];
1056
- }
1057
- if (mapped) (updates ??= {})[key] = mapped;
1058
- continue;
1059
- }
1060
- if (isNode(value)) {
1061
- const next = transformNode(value, visitor, recurse, node);
1062
- if (next !== value) (updates ??= {})[key] = next;
1063
- continue;
871
+ function resolveSchemaProperties({ node, propertyName }) {
872
+ const properties = [];
873
+ const visited = /* @__PURE__ */ new WeakSet();
874
+ function visit(current) {
875
+ if (!current || visited.has(current)) return;
876
+ visited.add(current);
877
+ if (current.type === "object") {
878
+ const property = current.properties.find((candidate) => candidate.name === propertyName);
879
+ if (property) properties.push(property);
880
+ return;
1064
881
  }
1065
- if (isNodeRecord(value)) {
1066
- let mapped;
1067
- for (const [name, item] of Object.entries(value)) {
1068
- const next = isNode(item) ? transformNode(item, visitor, recurse, node) : item;
1069
- if (next !== item) (mapped ??= { ...value })[name] = next;
1070
- }
1071
- if (mapped) (updates ??= {})[key] = mapped;
882
+ if (current.type === "ref") {
883
+ visit(current.schema);
884
+ return;
1072
885
  }
886
+ if (current.type === "intersection") for (const member of current.members ?? []) visit(member);
1073
887
  }
1074
- if (!updates) return node;
1075
- return {
1076
- ...node,
1077
- ...updates
1078
- };
888
+ visit(node);
889
+ return properties;
1079
890
  }
1080
891
  /**
1081
- * Lazy depth-first collection pass. Yields every non-null value returned by
1082
- * the visitor callbacks. Use `collectSync` for the eager array form.
892
+ * Reads the unique literal values represented by an enum, resolved reference, or union.
1083
893
  *
1084
- * @example Collect every operationId
894
+ * Named enum values take precedence over simple enum values, matching how renderers consume
895
+ * `EnumSchemaNode`.
896
+ *
897
+ * @example
1085
898
  * ```ts
1086
- * const ids: string[] = []
1087
- * for (const id of collect<string>(root, {
1088
- * operation(node) {
1089
- * return node.operationId
1090
- * },
1091
- * })) {
1092
- * ids.push(id)
1093
- * }
899
+ * const values = getSchemaLiteralValues(enumNode)
900
+ * // ['cat', 'dog']
1094
901
  * ```
1095
902
  */
1096
- function* collect(node, options) {
1097
- const { depth, parent, ...visitor } = options;
1098
- yield* collectNode(node, visitor, (depth ?? visitorDepths.deep) === visitorDepths.deep, parent);
903
+ function getSchemaLiteralValues(node) {
904
+ const values = /* @__PURE__ */ new Set();
905
+ const visited = /* @__PURE__ */ new WeakSet();
906
+ function visit(current) {
907
+ if (!current || visited.has(current)) return;
908
+ visited.add(current);
909
+ if (current.type === "enum") {
910
+ const enumValues = current.namedEnumValues?.map(({ value }) => value) ?? current.enumValues ?? [];
911
+ for (const value of enumValues) values.add(value);
912
+ return;
913
+ }
914
+ if (current.type === "ref") {
915
+ visit(current.schema);
916
+ return;
917
+ }
918
+ if (current.type === "union") for (const member of current.members ?? []) visit(member);
919
+ }
920
+ visit(node);
921
+ return [...values];
1099
922
  }
1100
- function* collectNode(node, visitor, recurse, parent) {
1101
- const v = applyVisitor(node, visitor, parent);
1102
- if (v != null) yield v;
1103
- for (const child of getChildren(node, recurse)) yield* collectNode(child, visitor, recurse, node);
923
+ function collectPropertyNames(node, visited = /* @__PURE__ */ new WeakSet()) {
924
+ if (visited.has(node)) return /* @__PURE__ */ new Set();
925
+ visited.add(node);
926
+ if (node.type === "object") return new Set(node.properties.map((property) => property.name));
927
+ if (node.type === "ref") return node.schema ? collectPropertyNames(node.schema, visited) : /* @__PURE__ */ new Set();
928
+ if (node.type === "intersection") {
929
+ const names = /* @__PURE__ */ new Set();
930
+ for (const member of node.members ?? []) for (const name of collectPropertyNames(member, visited)) names.add(name);
931
+ return names;
932
+ }
933
+ return /* @__PURE__ */ new Set();
1104
934
  }
1105
935
  /**
1106
- * Eager depth-first collection pass. Gathers every non-null value the visitor
1107
- * callbacks return into an array.
1108
- *
1109
- * @example Collect every operationId
1110
- * ```ts
1111
- * const ids = collectSync<string>(root, {
1112
- * operation(node) {
1113
- * return node.operationId
1114
- * },
1115
- * })
1116
- * ```
936
+ * Infers an implicit discriminator: a property every member exposes with a distinct single
937
+ * literal value, the same shape a declared OpenAPI `discriminator` already narrows on.
1117
938
  */
1118
- function collectSync(node, options) {
1119
- return Array.from(collect(node, options));
939
+ function inferDiscriminatorPropertyName(members) {
940
+ if (members.length < 2) return void 0;
941
+ let candidates;
942
+ for (const member of members) {
943
+ const names = collectPropertyNames(member);
944
+ candidates = candidates ? new Set([...candidates].filter((name) => names.has(name))) : names;
945
+ }
946
+ for (const name of candidates ?? []) {
947
+ const values = members.map((member) => {
948
+ const literals = new Set(resolveSchemaProperties({
949
+ node: member,
950
+ propertyName: name
951
+ }).flatMap((property) => getSchemaLiteralValues(property.schema)));
952
+ return literals.size === 1 ? [...literals][0] : void 0;
953
+ });
954
+ if (values.every((value) => value !== void 0) && new Set(values).size === members.length) return name;
955
+ }
1120
956
  }
1121
957
  //#endregion
1122
- //#region src/defineMacro.ts
958
+ //#region src/nodes/code.ts
1123
959
  /**
1124
- * Sort weight for an `enforce` hint. `pre` sorts before unmarked items and `post` after, so a plain
1125
- * list keeps its authored order.
1126
- */
1127
- function enforceWeight(enforce) {
1128
- if (enforce === "pre") return 0;
1129
- if (enforce === "post") return 2;
1130
- return 1;
960
+ * Creates a `ConstNode` representing a TypeScript `const` declaration.
961
+ *
962
+ * @example Exported constant with type and `as const`
963
+ * ```ts
964
+ * createConst({ name: 'pets', export: true, type: 'Pet[]', asConst: true })
965
+ * // export const pets: Pet[] = ... as const
966
+ * ```
967
+ */
968
+ function createConst(input) {
969
+ return {
970
+ ...input,
971
+ kind: "Const"
972
+ };
1131
973
  }
1132
974
  /**
1133
- * Types a macro for inference and a single construction site, mirroring `defineExtension`.
1134
- * Adds no runtime behavior.
975
+ * Creates a `TypeNode` representing a TypeScript `type` alias declaration.
1135
976
  *
1136
977
  * @example
1137
978
  * ```ts
1138
- * const macroUntagged = defineMacro({
1139
- * name: 'untagged',
1140
- * operation(node) {
1141
- * return node.tags?.length ? undefined : { ...node, tags: ['untagged'] }
1142
- * },
1143
- * })
979
+ * createType({ name: 'Pet', export: true })
980
+ * // export type Pet = ...
1144
981
  * ```
1145
982
  */
1146
- function defineMacro(macro) {
1147
- return macro;
983
+ function createType(input) {
984
+ return {
985
+ ...input,
986
+ kind: "Type"
987
+ };
1148
988
  }
1149
989
  /**
1150
- * Runs every macro's callback for one node kind in order, chaining the result so each macro sees
1151
- * the previous macro's output. Returns `undefined` when nothing changed, so `transform` keeps the
1152
- * original reference (structural sharing).
990
+ * Creates a `FunctionNode` representing a TypeScript `function` declaration.
991
+ *
992
+ * @example
993
+ * ```ts
994
+ * createFunction({ name: 'fetchPet', export: true, async: true, returnType: 'Pet' })
995
+ * // export async function fetchPet(): Promise<Pet> { ... }
996
+ * ```
1153
997
  */
1154
- function chain({ macros, key, node, context }) {
1155
- let current = node;
1156
- for (const macro of macros) {
1157
- const callback = macro[key];
1158
- if (!callback) continue;
1159
- if (macro.match && !macro.match(current)) continue;
1160
- const next = callback(current, context);
1161
- if (next != null) current = next;
1162
- }
1163
- return current === node ? void 0 : current;
998
+ function createFunction(input) {
999
+ return {
1000
+ ...input,
1001
+ kind: "Function"
1002
+ };
1164
1003
  }
1165
1004
  /**
1166
- * Folds an ordered list of macros into a single {@link Visitor} that `transform` (and the per-plugin
1167
- * transform layer in `@kubb/core`) can run. Macros are stable-sorted by `enforce`, then applied
1168
- * sequentially per node so later macros see earlier output. This differs from a plain visitor, which
1169
- * has no names, ordering, or composition.
1005
+ * Creates an `ArrowFunctionNode` representing a TypeScript arrow function.
1170
1006
  *
1171
1007
  * @example
1172
1008
  * ```ts
1173
- * const visitor = composeMacros([macroSimplifyUnion, macroDiscriminatorEnum])
1174
- * const next = transform(root, visitor)
1009
+ * createArrowFunction({ name: 'double', export: true, params: 'n: number', singleLine: true })
1010
+ * // export const double = (n: number) => ...
1175
1011
  * ```
1176
1012
  */
1177
- function composeMacros(macros) {
1178
- const ordered = [...macros].sort((a, b) => enforceWeight(a.enforce) - enforceWeight(b.enforce));
1179
- const visitor = {};
1180
- for (const key of visitorKeys) {
1181
- if (!ordered.some((macro) => typeof macro[key] === "function")) continue;
1182
- const callback = (node, context) => chain({
1183
- macros: ordered,
1184
- key,
1185
- node,
1186
- context
1187
- });
1188
- visitor[key] = callback;
1189
- }
1190
- return visitor;
1013
+ function createArrowFunction(input) {
1014
+ return {
1015
+ ...input,
1016
+ kind: "ArrowFunction"
1017
+ };
1191
1018
  }
1192
1019
  /**
1193
- * Runs a list of macros over a node tree and returns the rewritten tree. Keeps `transform`'s
1194
- * structural sharing, so an empty or no-op macro list returns the same reference. Pass
1195
- * `depth: 'shallow'` to rewrite the root node only.
1020
+ * Creates a {@link TextNode} representing a raw string fragment in the source output.
1196
1021
  *
1197
1022
  * @example
1198
1023
  * ```ts
1199
- * const next = applyMacros(root, [macroIntegerToString])
1024
+ * createText('return fetch(id)')
1025
+ * // { kind: 'Text', value: 'return fetch(id)' }
1200
1026
  * ```
1027
+ */
1028
+ function createText(value) {
1029
+ return {
1030
+ kind: "Text",
1031
+ value
1032
+ };
1033
+ }
1034
+ /**
1035
+ * Creates a {@link BreakNode} representing a line break in the source output.
1201
1036
  *
1202
- * @example Apply to the root node only
1037
+ * @example
1203
1038
  * ```ts
1204
- * const named = applyMacros(node, [macroEnumName({ parentName, propName, enumSuffix })], { depth: 'shallow' })
1039
+ * createBreak()
1040
+ * // { kind: 'Break' }
1205
1041
  * ```
1206
1042
  */
1207
- function applyMacros(root, macros, options) {
1208
- if (macros.length === 0) return root;
1209
- return transform(root, {
1210
- ...composeMacros(macros),
1211
- ...options
1212
- });
1043
+ function createBreak() {
1044
+ return { kind: "Break" };
1213
1045
  }
1214
- //#endregion
1215
- //#region src/createPrinter.ts
1216
1046
  /**
1217
- * Creates a schema printer: a function that takes a `SchemaNode` and emits
1218
- * code in your target language. Each plugin that produces code from schemas
1219
- * (TypeScript types, Zod schemas, Faker factories) ships a printer built
1220
- * with this helper.
1221
- *
1222
- * The builder receives resolved options and returns:
1223
- *
1224
- * - `name` unique identifier for the printer.
1225
- * - `options` stored on the returned printer instance.
1226
- * - `nodes` map of `SchemaType` → handler. Handlers return the rendered
1227
- * output (a string, a TypeScript AST node, ...) for that schema type.
1228
- * - `overrides` (optional), user-supplied handlers that win over `nodes`.
1229
- * An override can call `this.base(node)` to reuse the handler it replaced.
1230
- * - `print` (optional), top-level override exposed as `printer.print`.
1231
- * Use `this.transform(node)` inside it to dispatch to `nodes` recursively.
1232
- *
1233
- * Without a `print` override, `printer.print` falls back to `printer.transform`
1234
- * (the node-level dispatcher).
1047
+ * Creates a {@link JsxNode} representing a raw JSX fragment in the source output.
1235
1048
  *
1236
- * @example Tiny Zod printer
1049
+ * @example
1237
1050
  * ```ts
1238
- * import { createPrinter, type PrinterFactoryOptions } from '@kubb/ast'
1239
- *
1240
- * type PrinterZod = PrinterFactoryOptions<'zod', { strict?: boolean }, string>
1241
- *
1242
- * export const zodPrinter = createPrinter<PrinterZod>((options) => ({
1243
- * name: 'zod',
1244
- * options: { strict: options.strict ?? true },
1245
- * nodes: {
1246
- * string: () => 'z.string()',
1247
- * object(node) {
1248
- * const props = node.properties
1249
- * .map((p) => `${p.name}: ${this.transform(p.schema)}`)
1250
- * .join(', ')
1251
- * return `z.object({ ${props} })`
1252
- * },
1253
- * },
1254
- * }))
1051
+ * createJsx('<>\n <a href={href}>Open</a>\n</>')
1052
+ * // { kind: 'Jsx', value: '<>\n <a href={href}>Open</a>\n</>' }
1255
1053
  * ```
1256
1054
  */
1257
- function createPrinter(build) {
1258
- return (options) => {
1259
- const { name, options: resolvedOptions, nodes, overrides, print: printOverride } = build(options ?? {});
1260
- const merged = overrides ? {
1261
- ...nodes,
1262
- ...overrides
1263
- } : nodes;
1264
- const collectedImports = [];
1265
- const dispatch = (table) => (node) => {
1266
- const handler = table[node.type];
1267
- if (!handler) return null;
1268
- return handler.call(context, node);
1269
- };
1270
- const context = {
1271
- options: resolvedOptions,
1272
- import: (node) => {
1273
- collectedImports.push(node);
1274
- },
1275
- transform: dispatch(merged),
1276
- base: dispatch(nodes)
1277
- };
1278
- return {
1279
- name,
1280
- options: resolvedOptions,
1281
- transform: context.transform,
1282
- print: printOverride ? printOverride.bind(context) : context.transform,
1283
- drainImports: () => collectedImports.splice(0)
1284
- };
1055
+ function createJsx(value) {
1056
+ return {
1057
+ kind: "Jsx",
1058
+ value
1285
1059
  };
1286
1060
  }
1287
1061
  //#endregion
1288
- //#region src/utils/refs.ts
1062
+ //#region src/nodes/content.ts
1289
1063
  /**
1290
- * Resolves the emitted name of the schema a ref node points at. Prefers `targetName` (set when
1291
- * the referenced schema was renamed, e.g. to break a collision), then the last segment of `ref`,
1292
- * then `name`, then the nested `schema.name`.
1293
- *
1294
- * Returns `null` for non-ref nodes or when no name resolves.
1295
- *
1296
- * @example
1297
- * `resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Pet' }) // 'Pet'`
1298
- *
1299
- * @example Collision-renamed target
1300
- * `resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Order', targetName: 'OrderSchema' }) // 'OrderSchema'`
1064
+ * Creates a `ContentNode` for a single request-body or response content type.
1301
1065
  */
1302
- function resolveRefName(node) {
1303
- if (!node || node.type !== "ref") return null;
1304
- if (node.targetName) return node.targetName;
1305
- if (node.ref) return node.ref.split("/").at(-1) ?? node.ref;
1306
- return node.name ?? node.schema?.name ?? null;
1066
+ function createContent(input) {
1067
+ return {
1068
+ ...input,
1069
+ kind: "Content"
1070
+ };
1071
+ }
1072
+ //#endregion
1073
+ //#region src/utils/combineFileMembers.ts
1074
+ const IDENTIFIER_RUN = /[\w$]+/g;
1075
+ /**
1076
+ * How many imports a file needs before indexing the source beats scanning it once per name.
1077
+ */
1078
+ const INDEX_ABOVE_IMPORTS = 128;
1079
+ /**
1080
+ * Returns the local binding an import name item introduces: the alias when there is one, else the
1081
+ * imported name.
1082
+ */
1083
+ function importLocalName(item) {
1084
+ return typeof item === "string" ? item : item.name ?? item.propertyName;
1085
+ }
1086
+ /**
1087
+ * Computes a multi-level sort key for exports and imports:
1088
+ * non-array names first (wildcards/namespace aliases). Type-only before value. Alphabetical path. Unnamed before named.
1089
+ */
1090
+ function sortKey(node) {
1091
+ const isArray = Array.isArray(node.name) ? "1" : "0";
1092
+ const typeOnly = node.isTypeOnly ? "0" : "1";
1093
+ const hasName = node.name != null ? "1" : "0";
1094
+ const name = Array.isArray(node.name) ? node.name.toSorted().join("\0") : node.name ?? "";
1095
+ return `${isArray}:${typeOnly}:${node.path}:${hasName}:${name}`;
1096
+ }
1097
+ /**
1098
+ * Sorts members by {@link sortKey}. Keys are computed once per node, not per comparison.
1099
+ */
1100
+ function sortMembers(nodes) {
1101
+ return nodes.map((node) => ({
1102
+ node,
1103
+ key: sortKey(node)
1104
+ })).sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0).map(({ node }) => node);
1105
+ }
1106
+ /**
1107
+ * Returns a collector for array-named members. A member whose path and `isTypeOnly` were already
1108
+ * seen folds its names into that entry, otherwise it joins `result` as a new entry.
1109
+ */
1110
+ function mergeByPath(result) {
1111
+ const byPath = /* @__PURE__ */ new Map();
1112
+ return ({ node, names }) => {
1113
+ const key = `${node.path}:${node.isTypeOnly ?? false}`;
1114
+ const existing = byPath.get(key);
1115
+ if (existing && Array.isArray(existing.name)) {
1116
+ existing.name = [.../* @__PURE__ */ new Set([...existing.name, ...names])];
1117
+ return;
1118
+ }
1119
+ const item = {
1120
+ ...node,
1121
+ name: names
1122
+ };
1123
+ result.push(item);
1124
+ byPath.set(key, item);
1125
+ };
1126
+ }
1127
+ /**
1128
+ * Deduplicates `SourceNode` objects by `name + isExportable + isTypeOnly`, keeping the first of each
1129
+ * key. Unnamed sources fall back to their extracted node strings as the name part of the key. Returns
1130
+ * the deduplicated array in original order.
1131
+ */
1132
+ function combineSources(sources) {
1133
+ const seen = /* @__PURE__ */ new Map();
1134
+ for (const source of sources) {
1135
+ const key = `${source.name ?? extractStringsFromNodes(source.nodes)}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}`;
1136
+ if (!seen.has(key)) seen.set(key, source);
1137
+ }
1138
+ return [...seen.values()];
1307
1139
  }
1308
1140
  /**
1309
- * Merges a ref node with its resolved schema, giving the fields set at the usage site precedence.
1310
- *
1311
- * Every field on the ref except `kind`, `type`, `name`, `ref`, and `schema` overrides the same field
1312
- * of the resolved `schema` (for example `description`, `nullable`, `readOnly`, `deprecated`). Fields
1313
- * left `undefined` on the ref do not shadow the resolved schema. A ref without a resolved `schema`
1314
- * is returned as is.
1141
+ * Deduplicates and merges `ExportNode` objects by path and type.
1315
1142
  *
1316
- * @example
1317
- * ```ts
1318
- * const ref = createSchema({ type: 'ref', ref: '#/components/schemas/Pet', description: 'A pet', schema: petSchema })
1319
- * mergeRefWithSchema(ref) // petSchema's fields with description 'A pet'
1320
- * ```
1143
+ * Named exports with the same path and `isTypeOnly` flag have their names merged into a single export.
1144
+ * Non-array exports are deduplicated by exact identity. Returns a sorted, deduplicated array.
1321
1145
  */
1322
- function mergeRefWithSchema(node) {
1323
- if (!node.schema) return node;
1324
- const { kind: _kind, type: _type, name: _name, ref: _ref, schema: _schema, ...overrides } = node;
1325
- const definedOverrides = Object.fromEntries(Object.entries(overrides).filter(([, value]) => value !== void 0));
1326
- return {
1327
- ...node.schema,
1328
- ...definedOverrides
1329
- };
1146
+ function combineExports(exports) {
1147
+ const result = [];
1148
+ const merge = mergeByPath(result);
1149
+ const seen = /* @__PURE__ */ new Set();
1150
+ for (const curr of sortMembers(exports)) {
1151
+ const { name, path, isTypeOnly, asAlias } = curr;
1152
+ if (Array.isArray(name)) {
1153
+ if (name.length) merge({
1154
+ node: curr,
1155
+ names: [...new Set(name)]
1156
+ });
1157
+ continue;
1158
+ }
1159
+ const key = `${path}:${name ?? ""}:${isTypeOnly ?? false}:${asAlias ?? ""}`;
1160
+ if (!seen.has(key)) {
1161
+ result.push(curr);
1162
+ seen.add(key);
1163
+ }
1164
+ }
1165
+ return result;
1330
1166
  }
1331
1167
  /**
1332
- * Returns `true` when a schema is a bare ref without modifiers, metadata, or omit keys.
1168
+ * Deduplicates and merges `ImportNode` objects, filtering out unused imports.
1333
1169
  *
1334
- * Modifiers inherited from the resolved schema are included in the check because a ref emits the
1335
- * referenced schema variable with those modifiers already applied.
1170
+ * Retains imports that are referenced in `source` or re-exported. Imports with the same path and
1171
+ * `isTypeOnly` flag have their names merged. Returns a sorted, deduplicated, filtered array.
1336
1172
  */
1337
- function isBareRef(node, keysToOmit) {
1338
- if (!node || node.type !== "ref") return false;
1339
- const meta = mergeRefWithSchema(node);
1340
- return !meta.nullable && !meta.optional && !meta.nullish && meta.default === void 0 && !meta.description && !meta.examples?.length && !keysToOmit?.length;
1173
+ function combineImports(imports, exports, source) {
1174
+ const exportedNames = new Set(exports.flatMap((e) => Array.isArray(e.name) ? e.name : e.name ? [e.name] : []));
1175
+ const identifiers = source && imports.length > INDEX_ABOVE_IMPORTS ? new Set(source.match(IDENTIFIER_RUN)) : null;
1176
+ const isUsed = (importName) => !source || identifiers?.has(importName) || source.includes(importName) || exportedNames.has(importName);
1177
+ const importNameMemo = /* @__PURE__ */ new Map();
1178
+ const canonicalizeName = (n) => {
1179
+ if (typeof n === "string") return n;
1180
+ const key = `${n.propertyName}:${n.name ?? ""}`;
1181
+ if (!importNameMemo.has(key)) importNameMemo.set(key, n);
1182
+ return importNameMemo.get(key);
1183
+ };
1184
+ const pathsWithUsedNamedImport = /* @__PURE__ */ new Set();
1185
+ for (const node of imports) if (Array.isArray(node.name) && node.name.some((item) => isUsed(importLocalName(item)))) pathsWithUsedNamedImport.add(node.path);
1186
+ const result = [];
1187
+ const merge = mergeByPath(result);
1188
+ const seen = /* @__PURE__ */ new Set();
1189
+ for (const curr of sortMembers(imports)) {
1190
+ if (curr.path === curr.root) continue;
1191
+ const { name, path, isTypeOnly } = curr;
1192
+ if (Array.isArray(name)) {
1193
+ const names = [...new Set(name.map(canonicalizeName))].filter((item) => isUsed(importLocalName(item)));
1194
+ if (names.length) merge({
1195
+ node: curr,
1196
+ names
1197
+ });
1198
+ continue;
1199
+ }
1200
+ if (!isUsed(name) && !pathsWithUsedNamedImport.has(path)) continue;
1201
+ const key = `${path}:${name}:${isTypeOnly ?? false}`;
1202
+ if (!seen.has(key)) {
1203
+ result.push(curr);
1204
+ seen.add(key);
1205
+ }
1206
+ }
1207
+ return result;
1341
1208
  }
1342
1209
  //#endregion
1343
- //#region ../../internals/utils/src/promise.ts
1210
+ //#region src/nodes/file.ts
1344
1211
  /**
1345
- * Wraps `factory` with a keyed cache backed by the provided store.
1346
- *
1347
- * Pass a `WeakMap` for object keys (results are GC-eligible when the key is
1348
- * collected) or a `Map` for primitive keys. For multi-argument functions,
1349
- * nest two `memoize` calls — the outer keyed by the first argument, the
1350
- * inner (created once per outer miss) keyed by the second.
1351
- *
1352
- * Because the cache is owned by the caller, it can be shared, inspected, or
1353
- * cleared independently of the memoized function.
1354
- *
1355
- * @example Single WeakMap key
1356
- * ```ts
1357
- * const cache = new WeakMap<SchemaNode, Set<string>>()
1358
- * const getRefs = memoize(cache, (node) => collectRefs(node))
1359
- * ```
1360
- *
1361
- * @example Single Map key (primitive)
1362
- * ```ts
1363
- * const cache = new Map<string, Resolver>()
1364
- * const getResolver = memoize(cache, (name) => buildResolver(name))
1365
- * ```
1212
+ * Creates an `ImportNode` representing a language-agnostic import/dependency declaration.
1366
1213
  *
1367
- * @example Two-level (object + primitive)
1214
+ * @example Named import
1368
1215
  * ```ts
1369
- * const outer = new WeakMap<Params[], Map<string, Params[]>>()
1370
- * const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
1371
- * fn(params)('camelcase')
1216
+ * createImport({ name: ['useState'], path: 'react' })
1217
+ * // import { useState } from 'react'
1372
1218
  * ```
1373
1219
  */
1374
- function memoize(store, factory) {
1375
- return (key) => {
1376
- if (store.has(key)) return store.get(key);
1377
- const value = factory(key);
1378
- store.set(key, value);
1379
- return value;
1220
+ function createImport(input) {
1221
+ return {
1222
+ ...input,
1223
+ kind: "Import"
1380
1224
  };
1381
1225
  }
1382
- //#endregion
1383
- //#region src/utils/schemaGraph.ts
1384
1226
  /**
1385
- * Collects the names of every schema a node's subtree references, resolved with `resolveRefName`.
1386
- * Each ref contributes its name only, so the schema it points to is never traversed. The walk is
1387
- * memoized by node identity, which lets `findCircularSchemas` and `containsCircularRef` share one
1388
- * scan per schema.
1227
+ * Creates an `ExportNode` representing a language-agnostic export/public API declaration.
1389
1228
  *
1390
- * @example
1229
+ * @example Named export
1391
1230
  * ```ts
1392
- * collectSchemaRefs(petSchema)
1393
- * // Set { 'Category', 'Tag' }
1231
+ * createExport({ name: ['Pet'], path: './Pet' })
1232
+ * // export { Pet } from './Pet'
1394
1233
  * ```
1395
1234
  */
1396
- const collectSchemaRefs = memoize(/* @__PURE__ */ new WeakMap(), (node) => {
1397
- const refs = /* @__PURE__ */ new Set();
1398
- collectSync(node, { schema(child) {
1399
- if (child.type === "ref") {
1400
- const name = resolveRefName(child);
1401
- if (name) refs.add(name);
1402
- }
1403
- } });
1404
- return refs;
1405
- });
1235
+ function createExport(input) {
1236
+ return {
1237
+ ...input,
1238
+ kind: "Export"
1239
+ };
1240
+ }
1406
1241
  /**
1407
- * Collects the de-duplicated target names of every pointer-carrying ref in a node's subtree, in
1408
- * first-occurrence order. The walk is memoized by node identity, so the subtree is scanned once and
1409
- * `resolver.imports` reads the same result across the ts, zod, and faker plugins instead of
1410
- * re-scanning the same schema per plugin.
1411
- *
1412
- * Only refs that carry a `$ref` pointer count, so a synthesized ref pointing at a sibling in the
1413
- * same file (a union member created by name) is left out. That leaves exactly the set
1414
- * `resolver.imports` emits.
1242
+ * Creates a `SourceNode` representing a fragment of source code within a file.
1415
1243
  *
1416
1244
  * @example
1417
1245
  * ```ts
1418
- * collectImportedRefNames(petSchema)
1419
- * // ['Category', 'Tag']
1246
+ * createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')], isExportable: true })
1420
1247
  * ```
1421
1248
  */
1422
- const collectImportedRefNames = memoize(/* @__PURE__ */ new WeakMap(), (node) => {
1423
- const seen = /* @__PURE__ */ new Set();
1424
- const names = [];
1425
- collectSync(node, { schema(child) {
1426
- if (child.type !== "ref" || !child.ref) return;
1427
- const name = resolveRefName(child);
1428
- if (name && !seen.has(name)) {
1429
- seen.add(name);
1430
- names.push(name);
1431
- }
1432
- } });
1433
- return names;
1434
- });
1249
+ function createSource(input) {
1250
+ return {
1251
+ ...input,
1252
+ kind: "Source"
1253
+ };
1254
+ }
1435
1255
  /**
1436
- * Collects the names of all top-level schemas transitively used by a set of operations.
1256
+ * Creates a fully resolved `FileNode` from a file input descriptor.
1437
1257
  *
1438
- * An operation uses a schema when its parameters, request body, or responses reference it, directly
1439
- * or through other named schemas. Once a name is added to the result it is not revisited, so
1440
- * reference cycles terminate.
1258
+ * Computes:
1259
+ * - `id` SHA256 hash of the file path
1260
+ * - `name` `baseName` without extension
1261
+ * - `extname` extension extracted from `baseName`
1441
1262
  *
1442
- * Pair it with `include` filters so schemas reachable only from excluded operations stay ungenerated.
1263
+ * Deduplicates:
1264
+ * - `sources` via `combineSources`
1265
+ * - `exports` via `combineExports`
1266
+ * - `imports` via `combineImports` (also filters unused imports)
1443
1267
  *
1444
- * @example Only generate schemas referenced by included operations
1268
+ * @throws {Error} when `baseName` has no extension.
1269
+ *
1270
+ * @example
1445
1271
  * ```ts
1446
- * const includedOps = operations.filter((op) => resolver.default.options(op, { options, include }) !== null)
1447
- * const allowed = collectUsedSchemaNames(includedOps, schemas)
1272
+ * const file = createFile({
1273
+ * baseName: 'petStore.ts',
1274
+ * path: 'src/models/petStore.ts',
1275
+ * sources: [createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')] })],
1276
+ * imports: [createImport({ name: ['z'], path: 'zod' })],
1277
+ * exports: [createExport({ name: ['Pet'], path: './petStore' })],
1278
+ * })
1279
+ * // file.id = SHA256 hash of 'src/models/petStore.ts'
1280
+ * // file.name = 'petStore'
1281
+ * // file.extname = '.ts'
1282
+ * ```
1448
1283
  *
1449
- * for (const schema of schemas) {
1450
- * if (schema.name && !allowed.has(schema.name)) continue
1451
- * // generate schema
1452
- * }
1284
+ * @example Copy a real file into the output
1285
+ * ```ts
1286
+ * const file = createFile({
1287
+ * baseName: 'client.ts',
1288
+ * path: 'src/gen/client.ts',
1289
+ * copy: '/abs/path/to/templates/client.ts',
1290
+ * })
1453
1291
  * ```
1454
1292
  */
1455
- function collectUsedSchemaNames(operations, schemas) {
1456
- const schemaMap = /* @__PURE__ */ new Map();
1457
- for (const schema of schemas) if (schema.name) schemaMap.set(schema.name, schema);
1458
- const result = /* @__PURE__ */ new Set();
1459
- function visitSchema(schema) {
1460
- for (const name of collectSchemaRefs(schema)) if (!result.has(name)) {
1461
- result.add(name);
1462
- const namedSchema = schemaMap.get(name);
1463
- if (namedSchema) visitSchema(namedSchema);
1293
+ function createFile(input) {
1294
+ const extname = path.extname(input.baseName);
1295
+ if (!extname) throw new Error(`No extname found for ${input.baseName}`);
1296
+ const resolvedExports = input.exports?.length ? combineExports(input.exports) : [];
1297
+ const resolvedImports = (() => {
1298
+ if (!input.imports?.length) return [];
1299
+ const sourceParts = [];
1300
+ const localNames = /* @__PURE__ */ new Set();
1301
+ for (const item of input.sources ?? []) {
1302
+ const extracted = item.nodes && extractStringsFromNodes(item.nodes);
1303
+ if (extracted) sourceParts.push(extracted);
1304
+ if (item.name) localNames.add(item.name);
1464
1305
  }
1465
- }
1466
- for (const op of operations) for (const schema of collect(op, {
1467
- depth: "shallow",
1468
- schema: (node) => node
1469
- })) visitSchema(schema);
1470
- return result;
1306
+ const source = sourceParts.join("\n") || void 0;
1307
+ return combineImports(input.imports, resolvedExports, source).flatMap((imp) => {
1308
+ if (imp.path === input.path) return [];
1309
+ if (!Array.isArray(imp.name)) return typeof imp.name === "string" && localNames.has(imp.name) ? [] : [imp];
1310
+ const kept = imp.name.filter((item) => !localNames.has(importLocalName(item)));
1311
+ if (!kept.length) return [];
1312
+ return [kept.length === imp.name.length ? imp : {
1313
+ ...imp,
1314
+ name: kept
1315
+ }];
1316
+ });
1317
+ })();
1318
+ const resolvedSources = input.sources?.length ? combineSources(input.sources) : [];
1319
+ return {
1320
+ ...input,
1321
+ kind: "File",
1322
+ id: hash("sha256", input.path, "hex"),
1323
+ name: path.basename(input.baseName, extname),
1324
+ extname,
1325
+ imports: resolvedImports,
1326
+ exports: resolvedExports,
1327
+ sources: resolvedSources,
1328
+ meta: input.meta ?? {}
1329
+ };
1471
1330
  }
1331
+ //#endregion
1332
+ //#region src/nodes/input.ts
1472
1333
  /**
1473
- * Finds every schema that takes part in a circular dependency chain in a schema dependency graph
1474
- * that maps each schema name to the names it references directly.
1475
- *
1476
- * Use this when the graph was already collected during another pass (e.g. the adapter's convert
1477
- * walk), so the schema nodes are not swept a second time. `findCircularSchemas` builds the graph
1478
- * from schema nodes and delegates here.
1334
+ * Creates an `InputNode`, defaulting `schemas`/`operations` to empty arrays and `meta` to empty
1335
+ * name lists. Every call allocates fresh defaults so no two inputs share them.
1479
1336
  *
1480
1337
  * @example
1481
1338
  * ```ts
1482
- * const graph = new Map([
1483
- * ['Pet', new Set(['Category'])],
1484
- * ['Category', new Set(['Pet'])],
1485
- * ])
1486
- * findCircularSchemasFromGraph(graph) // Set { 'Pet', 'Category' }
1339
+ * const input = createInput()
1340
+ * // { kind: 'Input', schemas: [], operations: [] }
1487
1341
  * ```
1488
1342
  */
1489
- function findCircularSchemasFromGraph(graph) {
1490
- const circular = /* @__PURE__ */ new Set();
1491
- for (const start of graph.keys()) {
1492
- const visited = /* @__PURE__ */ new Set();
1493
- const stack = [...graph.get(start) ?? []];
1494
- while (stack.length > 0) {
1495
- const node = stack.pop();
1496
- if (node === start) {
1497
- circular.add(start);
1498
- break;
1499
- }
1500
- if (visited.has(node)) continue;
1501
- visited.add(node);
1502
- const next = graph.get(node);
1503
- if (next) for (const r of next) stack.push(r);
1504
- }
1505
- }
1506
- return circular;
1343
+ function createInput(overrides = {}) {
1344
+ return {
1345
+ schemas: [],
1346
+ operations: [],
1347
+ meta: {
1348
+ circularNames: [],
1349
+ enumNames: []
1350
+ },
1351
+ ...overrides,
1352
+ kind: "Input"
1353
+ };
1507
1354
  }
1355
+ //#endregion
1356
+ //#region src/nodes/requestBody.ts
1508
1357
  /**
1509
- * Finds every schema that takes part in a circular dependency chain, including direct self-loops.
1510
- *
1511
- * Wrap the returned schema positions in a deferred construct (a lazy getter or `z.lazy(() => …)`) so
1512
- * the generated code does not recurse forever. Refs are followed by name only, so the walk stays
1513
- * linear in the size of the schema graph.
1514
- *
1515
- * @note Call this once on the full graph, then check individual schemas with `containsCircularRef()`.
1358
+ * Creates a `RequestBodyNode`.
1516
1359
  */
1517
- function findCircularSchemas(schemas) {
1518
- const graph = /* @__PURE__ */ new Map();
1519
- for (const schema of schemas) if (schema.name) graph.set(schema.name, collectSchemaRefs(schema));
1520
- return findCircularSchemasFromGraph(graph);
1360
+ function createRequestBody(input) {
1361
+ return {
1362
+ ...input,
1363
+ kind: "RequestBody"
1364
+ };
1521
1365
  }
1522
1366
  //#endregion
1523
- //#region src/utils/schemaProperties.ts
1367
+ //#region src/nodes/operation.ts
1368
+ function createOperation(props) {
1369
+ const { requestBody, ...rest } = props;
1370
+ const isHttp = rest.method !== void 0 && rest.path !== void 0;
1371
+ return {
1372
+ tags: [],
1373
+ parameters: [],
1374
+ responses: [],
1375
+ ...rest,
1376
+ kind: "Operation",
1377
+ ...isHttp ? { protocol: "http" } : {},
1378
+ requestBody: requestBody ? createRequestBody(requestBody) : void 0
1379
+ };
1380
+ }
1381
+ //#endregion
1382
+ //#region src/nodes/output.ts
1524
1383
  /**
1525
- * Finds every matching property exposed by an object, resolved reference, or intersection.
1526
- *
1527
- * References are followed through their parsed `schema`. Circular references terminate without
1528
- * revisiting a node. Unions are not traversed because a property on one branch does not describe
1529
- * every branch.
1384
+ * Creates an `OutputNode` with a stable default for `files`.
1530
1385
  *
1531
- * @example Faker discriminator narrowing
1386
+ * @example
1532
1387
  * ```ts
1533
- * const values = resolveSchemaProperties({ node: member, propertyName: union.discriminatorPropertyName }).flatMap(({ schema }) =>
1534
- * getSchemaLiteralValues(schema),
1535
- * )
1536
- * const typeName = `Extract<Pet, { type: ${values.map(JSON.stringify).join(' | ')} }>`
1388
+ * const output = createOutput()
1389
+ * // { kind: 'Output', files: [] }
1537
1390
  * ```
1391
+ */
1392
+ function createOutput(overrides = {}) {
1393
+ return {
1394
+ files: [],
1395
+ ...overrides,
1396
+ kind: "Output"
1397
+ };
1398
+ }
1399
+ //#endregion
1400
+ //#region src/nodes/parameter.ts
1401
+ /**
1402
+ * Creates a `ParameterNode`. `required` defaults to `false`, and the schema's
1403
+ * `optional`/`nullish` flags are derived from it through {@link withRequired}.
1538
1404
  *
1539
- * @example Zod discriminated unions
1405
+ * @example
1540
1406
  * ```ts
1541
- * const canDiscriminate = union.members?.every(
1542
- * (member) => resolveSchemaProperties({ node: member, propertyName: 'type' }).length > 0,
1543
- * )
1544
- * const expression = canDiscriminate ? `z.discriminatedUnion('type', members)` : `z.union(members)`
1407
+ * const param = createParameter({
1408
+ * name: 'petId',
1409
+ * in: 'path',
1410
+ * required: true,
1411
+ * schema: createSchema({ type: 'string' }),
1412
+ * })
1545
1413
  * ```
1414
+ */
1415
+ function createParameter(input) {
1416
+ return {
1417
+ ...withRequired(input),
1418
+ kind: "Parameter"
1419
+ };
1420
+ }
1421
+ //#endregion
1422
+ //#region src/nodes/property.ts
1423
+ /**
1424
+ * Creates a `PropertyNode`. `required` defaults to `false`, and the schema's
1425
+ * `optional`/`nullish` flags are derived from it through {@link withRequired}.
1546
1426
  *
1547
- * @example TypeScript property lookup
1427
+ * @example
1548
1428
  * ```ts
1549
- * const properties = resolveSchemaProperties({ node: schema, propertyName: 'status' })
1550
- * const statusSchemas = properties.map((property) => print(property.schema))
1429
+ * const property = createProperty({
1430
+ * name: 'status',
1431
+ * required: true,
1432
+ * schema: createSchema({ type: 'string', nullable: true }),
1433
+ * })
1434
+ * // required=true, no optional/nullish
1551
1435
  * ```
1552
1436
  */
1553
- function resolveSchemaProperties({ node, propertyName }) {
1554
- const properties = [];
1555
- const visited = /* @__PURE__ */ new WeakSet();
1556
- function visit(current) {
1557
- if (!current || visited.has(current)) return;
1558
- visited.add(current);
1559
- if (current.type === "object") {
1560
- const property = current.properties.find((candidate) => candidate.name === propertyName);
1561
- if (property) properties.push(property);
1562
- return;
1563
- }
1564
- if (current.type === "ref") {
1565
- visit(current.schema);
1566
- return;
1567
- }
1568
- if (current.type === "intersection") for (const member of current.members ?? []) visit(member);
1569
- }
1570
- visit(node);
1571
- return properties;
1437
+ function createProperty(input) {
1438
+ return {
1439
+ ...withRequired(input),
1440
+ kind: "Property"
1441
+ };
1572
1442
  }
1443
+ //#endregion
1444
+ //#region src/nodes/response.ts
1573
1445
  /**
1574
- * Reads the unique literal values represented by an enum, resolved reference, or union.
1575
- *
1576
- * Named enum values take precedence over simple enum values, matching how renderers consume
1577
- * `EnumSchemaNode`.
1446
+ * Creates a `ResponseNode`. A single legacy `schema` (with optional `mediaType`/`keysToOmit`)
1447
+ * is normalized into one `content` entry.
1578
1448
  *
1579
1449
  * @example
1580
1450
  * ```ts
1581
- * const values = getSchemaLiteralValues(enumNode)
1582
- * // ['cat', 'dog']
1451
+ * const response = createResponse({
1452
+ * statusCode: '200',
1453
+ * content: [createContent({ contentType: 'application/json', schema: createSchema({ type: 'object', properties: [] }) })],
1454
+ * })
1583
1455
  * ```
1584
1456
  */
1585
- function getSchemaLiteralValues(node) {
1586
- const values = /* @__PURE__ */ new Set();
1587
- const visited = /* @__PURE__ */ new WeakSet();
1588
- function visit(current) {
1589
- if (!current || visited.has(current)) return;
1590
- visited.add(current);
1591
- if (current.type === "enum") {
1592
- const enumValues = current.namedEnumValues?.map(({ value }) => value) ?? current.enumValues ?? [];
1593
- for (const value of enumValues) values.add(value);
1594
- return;
1595
- }
1596
- if (current.type === "ref") {
1597
- visit(current.schema);
1598
- return;
1599
- }
1600
- if (current.type === "union") for (const member of current.members ?? []) visit(member);
1601
- }
1602
- visit(node);
1603
- return [...values];
1604
- }
1605
- function collectPropertyNames(node, visited = /* @__PURE__ */ new WeakSet()) {
1606
- if (visited.has(node)) return /* @__PURE__ */ new Set();
1607
- visited.add(node);
1608
- if (node.type === "object") return new Set(node.properties.map((property) => property.name));
1609
- if (node.type === "ref") return node.schema ? collectPropertyNames(node.schema, visited) : /* @__PURE__ */ new Set();
1610
- if (node.type === "intersection") {
1611
- const names = /* @__PURE__ */ new Set();
1612
- for (const member of node.members ?? []) for (const name of collectPropertyNames(member, visited)) names.add(name);
1613
- return names;
1614
- }
1615
- return /* @__PURE__ */ new Set();
1457
+ function createResponse(props) {
1458
+ const { schema, mediaType, keysToOmit, content, ...rest } = props;
1459
+ const entries = content ?? (schema ? [createContent({
1460
+ contentType: mediaType ?? "application/json",
1461
+ schema,
1462
+ keysToOmit: keysToOmit ?? null
1463
+ })] : void 0);
1464
+ return {
1465
+ ...rest,
1466
+ content: entries,
1467
+ kind: "Response"
1468
+ };
1616
1469
  }
1470
+ //#endregion
1471
+ //#region src/nodes/schema.ts
1617
1472
  /**
1618
- * Infers an implicit discriminator: a property every member exposes with a distinct single
1619
- * literal value, the same shape a declared OpenAPI `discriminator` already narrows on.
1473
+ * Maps schema `type` to its underlying `primitive`.
1474
+ * Primitive types map to themselves and special string formats map to `'string'`.
1475
+ * Any type not listed here (such as `ref`, `enum`, `union`, `intersection`, `tuple`, `ipv4`, `ipv6`, `blob`) has no `primitive`.
1620
1476
  */
1621
- function inferDiscriminatorPropertyName(members) {
1622
- if (members.length < 2) return void 0;
1623
- let candidates;
1624
- for (const member of members) {
1625
- const names = collectPropertyNames(member);
1626
- candidates = candidates ? new Set([...candidates].filter((name) => names.has(name))) : names;
1627
- }
1628
- for (const name of candidates ?? []) {
1629
- const values = members.map((member) => {
1630
- const literals = new Set(resolveSchemaProperties({
1631
- node: member,
1632
- propertyName: name
1633
- }).flatMap((property) => getSchemaLiteralValues(property.schema)));
1634
- return literals.size === 1 ? [...literals][0] : void 0;
1635
- });
1636
- if (values.every((value) => value !== void 0) && new Set(values).size === members.length) return name;
1637
- }
1477
+ const TYPE_TO_PRIMITIVE = {
1478
+ string: "string",
1479
+ number: "number",
1480
+ integer: "integer",
1481
+ bigint: "bigint",
1482
+ boolean: "boolean",
1483
+ null: "null",
1484
+ any: "any",
1485
+ unknown: "unknown",
1486
+ void: "void",
1487
+ never: "never",
1488
+ object: "object",
1489
+ array: "array",
1490
+ date: "date",
1491
+ uuid: "string",
1492
+ email: "string",
1493
+ url: "string",
1494
+ datetime: "string",
1495
+ time: "string"
1496
+ };
1497
+ function createSchema(props) {
1498
+ if (props.type === "object") return {
1499
+ properties: [],
1500
+ primitive: "object",
1501
+ ...props,
1502
+ kind: "Schema"
1503
+ };
1504
+ return {
1505
+ primitive: TYPE_TO_PRIMITIVE[props.type],
1506
+ ...props,
1507
+ kind: "Schema"
1508
+ };
1638
1509
  }
1639
1510
  //#endregion
1640
1511
  //#region src/factory.ts
@@ -1685,56 +1556,35 @@ function update(node, changes) {
1685
1556
  return node;
1686
1557
  }
1687
1558
  //#endregion
1688
- //#region src/exports.ts
1689
- var exports_exports = /* @__PURE__ */ __exportAll({
1559
+ //#region src/index.ts
1560
+ var src_exports = /* @__PURE__ */ __exportAll({
1690
1561
  applyMacros: () => applyMacros,
1691
- arrowFunctionDef: () => arrowFunctionDef,
1692
- breakDef: () => breakDef,
1562
+ ast: () => src_exports,
1693
1563
  collect: () => collect,
1694
1564
  collectImportedRefNames: () => collectImportedRefNames,
1695
1565
  collectSchemaRefs: () => collectSchemaRefs,
1696
1566
  collectSync: () => collectSync,
1697
1567
  collectUsedSchemaNames: () => collectUsedSchemaNames,
1698
1568
  composeMacros: () => composeMacros,
1699
- constDef: () => constDef,
1700
- contentDef: () => contentDef,
1701
1569
  createPrinter: () => createPrinter,
1702
1570
  defineMacro: () => defineMacro,
1703
- defineNode: () => defineNode,
1704
- exportDef: () => exportDef,
1705
1571
  extractStringsFromNodes: () => extractStringsFromNodes,
1706
1572
  factory: () => factory_exports,
1707
- fileDef: () => fileDef,
1708
1573
  findCircularSchemas: () => findCircularSchemas,
1709
1574
  findCircularSchemasFromGraph: () => findCircularSchemasFromGraph,
1710
- functionDef: () => functionDef,
1711
1575
  getSchemaLiteralValues: () => getSchemaLiteralValues,
1712
- importDef: () => importDef,
1713
1576
  inferDiscriminatorPropertyName: () => inferDiscriminatorPropertyName,
1714
- inputDef: () => inputDef,
1715
1577
  isBareRef: () => isBareRef,
1716
1578
  isHttpOperationNode: () => isHttpOperationNode,
1717
- jsxDef: () => jsxDef,
1718
1579
  mergeRefWithSchema: () => mergeRefWithSchema,
1719
1580
  narrowSchema: () => narrowSchema,
1720
- nodeDefs: () => nodeDefs,
1721
- operationDef: () => operationDef,
1722
1581
  optionality: () => optionality,
1723
- outputDef: () => outputDef,
1724
- parameterDef: () => parameterDef,
1725
- propertyDef: () => propertyDef,
1726
- requestBodyDef: () => requestBodyDef,
1727
1582
  resolveRefName: () => resolveRefName,
1728
1583
  resolveSchemaProperties: () => resolveSchemaProperties,
1729
- responseDef: () => responseDef,
1730
- schemaDef: () => schemaDef,
1731
1584
  schemaTypes: () => schemaTypes,
1732
- sourceDef: () => sourceDef,
1733
- textDef: () => textDef,
1734
- transform: () => transform,
1735
- typeDef: () => typeDef
1585
+ transform: () => transform
1736
1586
  });
1737
1587
  //#endregion
1738
- export { applyMacros, arrowFunctionDef, exports_exports as ast, breakDef, collect, collectImportedRefNames, collectSchemaRefs, collectSync, collectUsedSchemaNames, composeMacros, constDef, contentDef, createPrinter, defineMacro, defineNode, exportDef, extractStringsFromNodes, factory_exports as factory, fileDef, findCircularSchemas, findCircularSchemasFromGraph, functionDef, getSchemaLiteralValues, importDef, inferDiscriminatorPropertyName, inputDef, isBareRef, isHttpOperationNode, jsxDef, mergeRefWithSchema, narrowSchema, nodeDefs, operationDef, optionality, outputDef, parameterDef, propertyDef, requestBodyDef, resolveRefName, resolveSchemaProperties, responseDef, schemaDef, schemaTypes, sourceDef, textDef, transform, typeDef };
1588
+ export { applyMacros, src_exports as ast, collect, collectImportedRefNames, collectSchemaRefs, collectSync, collectUsedSchemaNames, composeMacros, createPrinter, defineMacro, extractStringsFromNodes, factory_exports as factory, findCircularSchemas, findCircularSchemasFromGraph, getSchemaLiteralValues, inferDiscriminatorPropertyName, isBareRef, isHttpOperationNode, mergeRefWithSchema, narrowSchema, optionality, resolveRefName, resolveSchemaProperties, schemaTypes, transform };
1739
1589
 
1740
1590
  //# sourceMappingURL=index.js.map