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