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