@kubb/ast 5.0.0-beta.75 → 5.0.0-beta.76
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/LICENSE +17 -10
- package/README.md +53 -27
- package/dist/defineMacro-C58x6uaa.cjs +114 -0
- package/dist/defineMacro-C58x6uaa.cjs.map +1 -0
- package/dist/defineMacro-DzsACbFo.d.ts +466 -0
- package/dist/defineMacro-Zagno12u.js +98 -0
- package/dist/defineMacro-Zagno12u.js.map +1 -0
- package/dist/index-Cu2zmNxv.d.ts +2188 -0
- package/dist/index.cjs +183 -2179
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +88 -3364
- package/dist/index.js +139 -2101
- package/dist/index.js.map +1 -1
- package/dist/macros.cjs +130 -0
- package/dist/macros.cjs.map +1 -0
- package/dist/macros.d.ts +61 -0
- package/dist/macros.js +128 -0
- package/dist/macros.js.map +1 -0
- package/dist/refs-DhraOHHv.cjs +135 -0
- package/dist/refs-DhraOHHv.cjs.map +1 -0
- package/dist/refs-DliAPaUa.js +101 -0
- package/dist/refs-DliAPaUa.js.map +1 -0
- package/dist/rolldown-runtime-CNktS9qV.js +17 -0
- package/dist/types-Ctz5NB1o.d.ts +244 -0
- package/dist/types.cjs +0 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +1 -0
- package/dist/utils.cjs +613 -0
- package/dist/utils.cjs.map +1 -0
- package/dist/utils.d.ts +353 -0
- package/dist/utils.js +589 -0
- package/dist/utils.js.map +1 -0
- package/dist/visitor-CDa9Cn6x.cjs +1547 -0
- package/dist/visitor-CDa9Cn6x.cjs.map +1 -0
- package/dist/visitor-Ns-njjbG.js +1183 -0
- package/dist/visitor-Ns-njjbG.js.map +1 -0
- package/package.json +18 -7
- package/dist/chunk--u3MIqq1.js +0 -8
- package/src/constants.ts +0 -228
- package/src/factory.ts +0 -742
- package/src/guards.ts +0 -110
- package/src/index.ts +0 -45
- package/src/infer.ts +0 -130
- package/src/mocks.ts +0 -176
- package/src/nodes/base.ts +0 -56
- package/src/nodes/code.ts +0 -304
- package/src/nodes/file.ts +0 -230
- package/src/nodes/function.ts +0 -223
- package/src/nodes/http.ts +0 -119
- package/src/nodes/index.ts +0 -86
- package/src/nodes/operation.ts +0 -111
- package/src/nodes/output.ts +0 -26
- package/src/nodes/parameter.ts +0 -41
- package/src/nodes/property.ts +0 -34
- package/src/nodes/response.ts +0 -43
- package/src/nodes/root.ts +0 -64
- package/src/nodes/schema.ts +0 -656
- package/src/printer.ts +0 -250
- package/src/refs.ts +0 -67
- package/src/resolvers.ts +0 -45
- package/src/transformers.ts +0 -159
- package/src/types.ts +0 -70
- package/src/utils.ts +0 -833
- package/src/visitor.ts +0 -592
|
@@ -0,0 +1,1183 @@
|
|
|
1
|
+
import "./rolldown-runtime-CNktS9qV.js";
|
|
2
|
+
import { hash } from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
//#region src/constants.ts
|
|
5
|
+
const visitorDepths = {
|
|
6
|
+
shallow: "shallow",
|
|
7
|
+
deep: "deep"
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Schema type discriminators used by all AST schema nodes.
|
|
11
|
+
*
|
|
12
|
+
* Each value is a stable discriminator across the AST (for example `schema.type === schemaTypes.object`).
|
|
13
|
+
*/
|
|
14
|
+
const schemaTypes = {
|
|
15
|
+
/**
|
|
16
|
+
* Text value.
|
|
17
|
+
*/
|
|
18
|
+
string: "string",
|
|
19
|
+
/**
|
|
20
|
+
* Floating-point number (`float`, `double`).
|
|
21
|
+
*/
|
|
22
|
+
number: "number",
|
|
23
|
+
/**
|
|
24
|
+
* Whole number (`int32`). Use `bigint` for `int64`.
|
|
25
|
+
*/
|
|
26
|
+
integer: "integer",
|
|
27
|
+
/**
|
|
28
|
+
* 64-bit integer (`int64`). Only used when `integerType` is set to `'bigint'`.
|
|
29
|
+
*/
|
|
30
|
+
bigint: "bigint",
|
|
31
|
+
/**
|
|
32
|
+
* Boolean value.
|
|
33
|
+
*/
|
|
34
|
+
boolean: "boolean",
|
|
35
|
+
/**
|
|
36
|
+
* Explicit null value.
|
|
37
|
+
*/
|
|
38
|
+
null: "null",
|
|
39
|
+
/**
|
|
40
|
+
* Any value (no type restriction).
|
|
41
|
+
*/
|
|
42
|
+
any: "any",
|
|
43
|
+
/**
|
|
44
|
+
* Unknown value (must be narrowed before usage).
|
|
45
|
+
*/
|
|
46
|
+
unknown: "unknown",
|
|
47
|
+
/**
|
|
48
|
+
* No return value (`void`).
|
|
49
|
+
*/
|
|
50
|
+
void: "void",
|
|
51
|
+
/**
|
|
52
|
+
* Object with named properties.
|
|
53
|
+
*/
|
|
54
|
+
object: "object",
|
|
55
|
+
/**
|
|
56
|
+
* Sequential list of items.
|
|
57
|
+
*/
|
|
58
|
+
array: "array",
|
|
59
|
+
/**
|
|
60
|
+
* Fixed-length list with position-specific items.
|
|
61
|
+
*/
|
|
62
|
+
tuple: "tuple",
|
|
63
|
+
/**
|
|
64
|
+
* "One of" multiple schema members.
|
|
65
|
+
*/
|
|
66
|
+
union: "union",
|
|
67
|
+
/**
|
|
68
|
+
* "All of" multiple schema members.
|
|
69
|
+
*/
|
|
70
|
+
intersection: "intersection",
|
|
71
|
+
/**
|
|
72
|
+
* Enum schema.
|
|
73
|
+
*/
|
|
74
|
+
enum: "enum",
|
|
75
|
+
/**
|
|
76
|
+
* Reference to another schema.
|
|
77
|
+
*/
|
|
78
|
+
ref: "ref",
|
|
79
|
+
/**
|
|
80
|
+
* Calendar date (for example `2026-03-24`).
|
|
81
|
+
*/
|
|
82
|
+
date: "date",
|
|
83
|
+
/**
|
|
84
|
+
* Date-time value (for example `2026-03-24T09:00:00Z`).
|
|
85
|
+
*/
|
|
86
|
+
datetime: "datetime",
|
|
87
|
+
/**
|
|
88
|
+
* Time-only value (for example `09:00:00`).
|
|
89
|
+
*/
|
|
90
|
+
time: "time",
|
|
91
|
+
/**
|
|
92
|
+
* UUID value.
|
|
93
|
+
*/
|
|
94
|
+
uuid: "uuid",
|
|
95
|
+
/**
|
|
96
|
+
* Email address value.
|
|
97
|
+
*/
|
|
98
|
+
email: "email",
|
|
99
|
+
/**
|
|
100
|
+
* URL value.
|
|
101
|
+
*/
|
|
102
|
+
url: "url",
|
|
103
|
+
/**
|
|
104
|
+
* IPv4 address value.
|
|
105
|
+
*/
|
|
106
|
+
ipv4: "ipv4",
|
|
107
|
+
/**
|
|
108
|
+
* IPv6 address value.
|
|
109
|
+
*/
|
|
110
|
+
ipv6: "ipv6",
|
|
111
|
+
/**
|
|
112
|
+
* Binary/blob value.
|
|
113
|
+
*/
|
|
114
|
+
blob: "blob",
|
|
115
|
+
/**
|
|
116
|
+
* Impossible value (`never`).
|
|
117
|
+
*/
|
|
118
|
+
never: "never"
|
|
119
|
+
};
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/guards.ts
|
|
122
|
+
/**
|
|
123
|
+
* Narrows a `SchemaNode` to the variant that matches `type`.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* const schema = createSchema({ type: 'string' })
|
|
128
|
+
* const stringNode = narrowSchema(schema, 'string') // StringSchemaNode | null
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
function narrowSchema(node, type) {
|
|
132
|
+
return node?.type === type ? node : null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Narrows an `OperationNode` to an `HttpOperationNode` so `method` and `path` are present.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```ts
|
|
139
|
+
* if (isHttpOperationNode(node)) {
|
|
140
|
+
* console.log(node.method, node.path)
|
|
141
|
+
* }
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
function isHttpOperationNode(node) {
|
|
145
|
+
return node.protocol === "http" || node.method !== void 0 && node.path !== void 0;
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
//#region src/defineNode.ts
|
|
149
|
+
/**
|
|
150
|
+
* Visitor callback names, one per traversable node kind, in traversal order.
|
|
151
|
+
* Kept in sync with the keys of `Visitor` in `visitor.ts`.
|
|
152
|
+
*/
|
|
153
|
+
const visitorKeys = [
|
|
154
|
+
"input",
|
|
155
|
+
"output",
|
|
156
|
+
"operation",
|
|
157
|
+
"schema",
|
|
158
|
+
"property",
|
|
159
|
+
"parameter",
|
|
160
|
+
"response"
|
|
161
|
+
];
|
|
162
|
+
/**
|
|
163
|
+
* Builds a type guard that matches nodes of the given `kind`.
|
|
164
|
+
*/
|
|
165
|
+
function isKind(kind) {
|
|
166
|
+
return (node) => node?.kind === kind;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Defines a node once and derives its `create` builder, `is` guard, and traversal
|
|
170
|
+
* metadata. `create` merges `defaults`, the `build` hook (or the raw input), and the
|
|
171
|
+
* `kind`, so node construction lives in one place without scattered `as` casts.
|
|
172
|
+
*
|
|
173
|
+
* @example Simple node
|
|
174
|
+
* ```ts
|
|
175
|
+
* const importDef = defineNode<ImportNode>({ kind: 'Import' })
|
|
176
|
+
* const createImport = importDef.create
|
|
177
|
+
* ```
|
|
178
|
+
*
|
|
179
|
+
* @example Node with a build hook
|
|
180
|
+
* ```ts
|
|
181
|
+
* const propertyDef = defineNode<PropertyNode, UserPropertyNode>({
|
|
182
|
+
* kind: 'Property',
|
|
183
|
+
* build: (props) => ({ ...props, required: props.required ?? false }),
|
|
184
|
+
* children: ['schema'],
|
|
185
|
+
* visitorKey: 'property',
|
|
186
|
+
* })
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
function defineNode(config) {
|
|
190
|
+
const { kind, defaults, build, children, visitorKey } = config;
|
|
191
|
+
function create(input) {
|
|
192
|
+
const base = build ? build(input) : input;
|
|
193
|
+
return {
|
|
194
|
+
...defaults,
|
|
195
|
+
...base,
|
|
196
|
+
kind
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
kind,
|
|
201
|
+
create,
|
|
202
|
+
is: isKind(kind),
|
|
203
|
+
children,
|
|
204
|
+
visitorKey
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
//#endregion
|
|
208
|
+
//#region src/nodes/code.ts
|
|
209
|
+
/**
|
|
210
|
+
* Definition for the {@link ConstNode}.
|
|
211
|
+
*/
|
|
212
|
+
const constDef = defineNode({ kind: "Const" });
|
|
213
|
+
/**
|
|
214
|
+
* Definition for the {@link TypeNode}.
|
|
215
|
+
*/
|
|
216
|
+
const typeDef = defineNode({ kind: "Type" });
|
|
217
|
+
/**
|
|
218
|
+
* Definition for the {@link FunctionNode}.
|
|
219
|
+
*/
|
|
220
|
+
const functionDef = defineNode({ kind: "Function" });
|
|
221
|
+
/**
|
|
222
|
+
* Definition for the {@link ArrowFunctionNode}.
|
|
223
|
+
*/
|
|
224
|
+
const arrowFunctionDef = defineNode({ kind: "ArrowFunction" });
|
|
225
|
+
/**
|
|
226
|
+
* Definition for the {@link TextNode}.
|
|
227
|
+
*/
|
|
228
|
+
const textDef = defineNode({
|
|
229
|
+
kind: "Text",
|
|
230
|
+
build: (value) => ({ value })
|
|
231
|
+
});
|
|
232
|
+
/**
|
|
233
|
+
* Definition for the {@link BreakNode}.
|
|
234
|
+
*/
|
|
235
|
+
const breakDef = defineNode({
|
|
236
|
+
kind: "Break",
|
|
237
|
+
build: () => ({})
|
|
238
|
+
});
|
|
239
|
+
/**
|
|
240
|
+
* Definition for the {@link JsxNode}.
|
|
241
|
+
*/
|
|
242
|
+
const jsxDef = defineNode({
|
|
243
|
+
kind: "Jsx",
|
|
244
|
+
build: (value) => ({ value })
|
|
245
|
+
});
|
|
246
|
+
/**
|
|
247
|
+
* Creates a `ConstNode` representing a TypeScript `const` declaration.
|
|
248
|
+
*
|
|
249
|
+
* @example Exported constant with type and `as const`
|
|
250
|
+
* ```ts
|
|
251
|
+
* createConst({ name: 'pets', export: true, type: 'Pet[]', asConst: true })
|
|
252
|
+
* // export const pets: Pet[] = ... as const
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
const createConst = constDef.create;
|
|
256
|
+
/**
|
|
257
|
+
* Creates a `TypeNode` representing a TypeScript `type` alias declaration.
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```ts
|
|
261
|
+
* createType({ name: 'Pet', export: true })
|
|
262
|
+
* // export type Pet = ...
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
const createType = typeDef.create;
|
|
266
|
+
/**
|
|
267
|
+
* Creates a `FunctionNode` representing a TypeScript `function` declaration.
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```ts
|
|
271
|
+
* createFunction({ name: 'fetchPet', export: true, async: true, returnType: 'Pet' })
|
|
272
|
+
* // export async function fetchPet(): Promise<Pet> { ... }
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
const createFunction = functionDef.create;
|
|
276
|
+
/**
|
|
277
|
+
* Creates an `ArrowFunctionNode` representing a TypeScript arrow function.
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* ```ts
|
|
281
|
+
* createArrowFunction({ name: 'double', export: true, params: 'n: number', singleLine: true })
|
|
282
|
+
* // export const double = (n: number) => ...
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
const createArrowFunction = arrowFunctionDef.create;
|
|
286
|
+
/**
|
|
287
|
+
* Creates a {@link TextNode} representing a raw string fragment in the source output.
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* ```ts
|
|
291
|
+
* createText('return fetch(id)')
|
|
292
|
+
* // { kind: 'Text', value: 'return fetch(id)' }
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
const createText = textDef.create;
|
|
296
|
+
/**
|
|
297
|
+
* Creates a {@link BreakNode} representing a line break in the source output.
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* ```ts
|
|
301
|
+
* createBreak()
|
|
302
|
+
* // { kind: 'Break' }
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
function createBreak() {
|
|
306
|
+
return breakDef.create();
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Creates a {@link JsxNode} representing a raw JSX fragment in the source output.
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```ts
|
|
313
|
+
* createJsx('<>\n <a href={href}>Open</a>\n</>')
|
|
314
|
+
* // { kind: 'Jsx', value: '<>\n <a href={href}>Open</a>\n</>' }
|
|
315
|
+
* ```
|
|
316
|
+
*/
|
|
317
|
+
const createJsx = jsxDef.create;
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/nodes/content.ts
|
|
320
|
+
/**
|
|
321
|
+
* Definition for the {@link ContentNode}.
|
|
322
|
+
*/
|
|
323
|
+
const contentDef = defineNode({
|
|
324
|
+
kind: "Content",
|
|
325
|
+
children: ["schema"]
|
|
326
|
+
});
|
|
327
|
+
/**
|
|
328
|
+
* Creates a `ContentNode` for a single request-body or response content type.
|
|
329
|
+
*/
|
|
330
|
+
const createContent = contentDef.create;
|
|
331
|
+
//#endregion
|
|
332
|
+
//#region ../../internals/utils/src/casing.ts
|
|
333
|
+
/**
|
|
334
|
+
* Shared implementation for camelCase and PascalCase conversion.
|
|
335
|
+
* Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
|
|
336
|
+
* and capitalizes each word according to `pascal`.
|
|
337
|
+
*
|
|
338
|
+
* When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
|
|
339
|
+
*/
|
|
340
|
+
function toCamelOrPascal(text, pascal) {
|
|
341
|
+
return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
|
|
342
|
+
if (word.length > 1 && word === word.toUpperCase()) return word;
|
|
343
|
+
return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
|
|
344
|
+
}).join("").replace(/[^a-zA-Z0-9]/g, "");
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Converts `text` to PascalCase.
|
|
348
|
+
*
|
|
349
|
+
* @example Word boundaries
|
|
350
|
+
* `pascalCase('hello-world') // 'HelloWorld'`
|
|
351
|
+
*
|
|
352
|
+
* @example With a suffix
|
|
353
|
+
* `pascalCase('tag', { suffix: 'schema' }) // 'TagSchema'`
|
|
354
|
+
*/
|
|
355
|
+
function pascalCase(text, { prefix = "", suffix = "" } = {}) {
|
|
356
|
+
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
|
|
357
|
+
}
|
|
358
|
+
//#endregion
|
|
359
|
+
//#region ../../internals/utils/src/fs.ts
|
|
360
|
+
/**
|
|
361
|
+
* Strips the file extension from a path or file name.
|
|
362
|
+
* Only removes the last `.ext` segment when the dot is not part of a directory name.
|
|
363
|
+
*
|
|
364
|
+
* @example
|
|
365
|
+
* trimExtName('petStore.ts') // 'petStore'
|
|
366
|
+
* trimExtName('/src/models/pet.ts') // '/src/models/pet'
|
|
367
|
+
* trimExtName('/project.v2/gen/pet.ts') // '/project.v2/gen/pet'
|
|
368
|
+
* trimExtName('noExtension') // 'noExtension'
|
|
369
|
+
*/
|
|
370
|
+
function trimExtName(text) {
|
|
371
|
+
const dotIndex = text.lastIndexOf(".");
|
|
372
|
+
if (dotIndex > 0 && !text.includes("/", dotIndex)) return text.slice(0, dotIndex);
|
|
373
|
+
return text;
|
|
374
|
+
}
|
|
375
|
+
//#endregion
|
|
376
|
+
//#region src/utils/extractStringsFromNodes.ts
|
|
377
|
+
/**
|
|
378
|
+
* Extracts all string content from a `CodeNode` tree recursively.
|
|
379
|
+
*
|
|
380
|
+
* Collects text node values, identifier references in string fields (`params`, `generics`, `returnType`, `type`),
|
|
381
|
+
* and nested node content. Used to build the full source string for import filtering.
|
|
382
|
+
*/
|
|
383
|
+
function extractStringsFromNodes(nodes) {
|
|
384
|
+
if (!nodes?.length) return "";
|
|
385
|
+
return nodes.map((node) => {
|
|
386
|
+
if (typeof node === "string") return node;
|
|
387
|
+
if (node.kind === "Text") return node.value;
|
|
388
|
+
if (node.kind === "Break") return "";
|
|
389
|
+
if (node.kind === "Jsx") return node.value;
|
|
390
|
+
const parts = [];
|
|
391
|
+
if ("params" in node && node.params) parts.push(node.params);
|
|
392
|
+
if ("generics" in node && node.generics) parts.push(Array.isArray(node.generics) ? node.generics.join(", ") : node.generics);
|
|
393
|
+
if ("returnType" in node && node.returnType) parts.push(node.returnType);
|
|
394
|
+
if ("type" in node && typeof node.type === "string") parts.push(node.type);
|
|
395
|
+
const nested = extractStringsFromNodes(node.nodes);
|
|
396
|
+
if (nested) parts.push(nested);
|
|
397
|
+
return parts.join("\n");
|
|
398
|
+
}).filter(Boolean).join("\n");
|
|
399
|
+
}
|
|
400
|
+
//#endregion
|
|
401
|
+
//#region src/utils/fileMerge.ts
|
|
402
|
+
function sourceKey(source) {
|
|
403
|
+
return `${source.name ?? extractStringsFromNodes(source.nodes)}:${source.isExportable ?? false}:${source.isTypeOnly ?? false}`;
|
|
404
|
+
}
|
|
405
|
+
function pathTypeKey(path, isTypeOnly) {
|
|
406
|
+
return `${path}:${isTypeOnly ?? false}`;
|
|
407
|
+
}
|
|
408
|
+
function exportKey(path, name, isTypeOnly, asAlias) {
|
|
409
|
+
return `${path}:${name ?? ""}:${isTypeOnly ?? false}:${asAlias ?? ""}`;
|
|
410
|
+
}
|
|
411
|
+
function importKey(path, name, isTypeOnly) {
|
|
412
|
+
return `${path}:${name ?? ""}:${isTypeOnly ?? false}`;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Computes a multi-level sort key for exports and imports:
|
|
416
|
+
* non-array names first (wildcards/namespace aliases). Type-only before value. Alphabetical path. Unnamed before named.
|
|
417
|
+
*/
|
|
418
|
+
function sortKey(node) {
|
|
419
|
+
const isArray = Array.isArray(node.name) ? "1" : "0";
|
|
420
|
+
const typeOnly = node.isTypeOnly ? "0" : "1";
|
|
421
|
+
const hasName = node.name != null ? "1" : "0";
|
|
422
|
+
const name = Array.isArray(node.name) ? node.name.toSorted().join("\0") : node.name ?? "";
|
|
423
|
+
return `${isArray}:${typeOnly}:${node.path}:${hasName}:${name}`;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Deduplicates `SourceNode` objects by `name + isExportable + isTypeOnly`, keeping the first of each
|
|
427
|
+
* key. Unnamed sources fall back to their extracted node strings as the name part of the key. Returns
|
|
428
|
+
* the deduplicated array in original order.
|
|
429
|
+
*/
|
|
430
|
+
function combineSources(sources) {
|
|
431
|
+
const seen = /* @__PURE__ */ new Map();
|
|
432
|
+
for (const source of sources) {
|
|
433
|
+
const key = sourceKey(source);
|
|
434
|
+
if (!seen.has(key)) seen.set(key, source);
|
|
435
|
+
}
|
|
436
|
+
return [...seen.values()];
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Merges `incoming` names into `existing`, preserving order and dropping duplicates.
|
|
440
|
+
*
|
|
441
|
+
* Shared by `combineExports` and `combineImports` for the same-path name-merge case.
|
|
442
|
+
*/
|
|
443
|
+
function mergeNameArrays(existing, incoming) {
|
|
444
|
+
const merged = new Set(existing);
|
|
445
|
+
for (const name of incoming) merged.add(name);
|
|
446
|
+
return [...merged];
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Deduplicates and merges `ExportNode` objects by path and type.
|
|
450
|
+
*
|
|
451
|
+
* Named exports with the same path and `isTypeOnly` flag have their names merged into a single export.
|
|
452
|
+
* Non-array exports are deduplicated by exact identity. Returns a sorted, deduplicated array.
|
|
453
|
+
*/
|
|
454
|
+
function combineExports(exports) {
|
|
455
|
+
const result = [];
|
|
456
|
+
const namedByPath = /* @__PURE__ */ new Map();
|
|
457
|
+
const seen = /* @__PURE__ */ new Set();
|
|
458
|
+
const keyed = exports.map((node) => ({
|
|
459
|
+
node,
|
|
460
|
+
key: sortKey(node)
|
|
461
|
+
}));
|
|
462
|
+
keyed.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
463
|
+
for (const { node: curr } of keyed) {
|
|
464
|
+
const { name, path, isTypeOnly, asAlias } = curr;
|
|
465
|
+
if (Array.isArray(name)) {
|
|
466
|
+
if (!name.length) continue;
|
|
467
|
+
const key = pathTypeKey(path, isTypeOnly);
|
|
468
|
+
const existing = namedByPath.get(key);
|
|
469
|
+
if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
|
|
470
|
+
else {
|
|
471
|
+
const newItem = {
|
|
472
|
+
...curr,
|
|
473
|
+
name: [...new Set(name)]
|
|
474
|
+
};
|
|
475
|
+
result.push(newItem);
|
|
476
|
+
namedByPath.set(key, newItem);
|
|
477
|
+
}
|
|
478
|
+
} else {
|
|
479
|
+
const key = exportKey(path, name, isTypeOnly, asAlias);
|
|
480
|
+
if (!seen.has(key)) {
|
|
481
|
+
result.push(curr);
|
|
482
|
+
seen.add(key);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Deduplicates and merges `ImportNode` objects, filtering out unused imports.
|
|
490
|
+
*
|
|
491
|
+
* Retains imports that are referenced in `source` or re-exported. Imports with the same path and
|
|
492
|
+
* `isTypeOnly` flag have their names merged. Returns a sorted, deduplicated, filtered array.
|
|
493
|
+
*/
|
|
494
|
+
function combineImports(imports, exports, source) {
|
|
495
|
+
const exportedNames = new Set(exports.flatMap((e) => Array.isArray(e.name) ? e.name : e.name ? [e.name] : []));
|
|
496
|
+
const isUsed = (importName) => !source || source.includes(importName) || exportedNames.has(importName);
|
|
497
|
+
const importNameMemo = /* @__PURE__ */ new Map();
|
|
498
|
+
const canonicalizeName = (n) => {
|
|
499
|
+
if (typeof n === "string") return n;
|
|
500
|
+
const key = `${n.propertyName}:${n.name ?? ""}`;
|
|
501
|
+
if (!importNameMemo.has(key)) importNameMemo.set(key, n);
|
|
502
|
+
return importNameMemo.get(key);
|
|
503
|
+
};
|
|
504
|
+
const pathsWithUsedNamedImport = /* @__PURE__ */ new Set();
|
|
505
|
+
for (const node of imports) {
|
|
506
|
+
if (!Array.isArray(node.name)) continue;
|
|
507
|
+
if (node.name.some((item) => typeof item === "string" ? isUsed(item) : isUsed(item.name ?? item.propertyName))) pathsWithUsedNamedImport.add(node.path);
|
|
508
|
+
}
|
|
509
|
+
const result = [];
|
|
510
|
+
const namedByPath = /* @__PURE__ */ new Map();
|
|
511
|
+
const seen = /* @__PURE__ */ new Set();
|
|
512
|
+
const keyed = imports.map((node) => ({
|
|
513
|
+
node,
|
|
514
|
+
key: sortKey(node)
|
|
515
|
+
}));
|
|
516
|
+
keyed.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
517
|
+
for (const { node: curr } of keyed) {
|
|
518
|
+
if (curr.path === curr.root) continue;
|
|
519
|
+
const { path, isTypeOnly } = curr;
|
|
520
|
+
let { name } = curr;
|
|
521
|
+
if (Array.isArray(name)) {
|
|
522
|
+
name = [...new Set(name.map(canonicalizeName))].filter((item) => typeof item === "string" ? isUsed(item) : isUsed(item.name ?? item.propertyName));
|
|
523
|
+
if (!name.length) continue;
|
|
524
|
+
const key = pathTypeKey(path, isTypeOnly);
|
|
525
|
+
const existing = namedByPath.get(key);
|
|
526
|
+
if (existing && Array.isArray(existing.name)) existing.name = mergeNameArrays(existing.name, name);
|
|
527
|
+
else {
|
|
528
|
+
const newItem = {
|
|
529
|
+
...curr,
|
|
530
|
+
name
|
|
531
|
+
};
|
|
532
|
+
result.push(newItem);
|
|
533
|
+
namedByPath.set(key, newItem);
|
|
534
|
+
}
|
|
535
|
+
} else {
|
|
536
|
+
if (name && !isUsed(name) && !pathsWithUsedNamedImport.has(path)) continue;
|
|
537
|
+
const key = importKey(path, name, isTypeOnly);
|
|
538
|
+
if (!seen.has(key)) {
|
|
539
|
+
result.push(curr);
|
|
540
|
+
seen.add(key);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return result;
|
|
545
|
+
}
|
|
546
|
+
//#endregion
|
|
547
|
+
//#region src/nodes/file.ts
|
|
548
|
+
/**
|
|
549
|
+
* Definition for the {@link ImportNode}.
|
|
550
|
+
*/
|
|
551
|
+
const importDef = defineNode({ kind: "Import" });
|
|
552
|
+
/**
|
|
553
|
+
* Definition for the {@link ExportNode}.
|
|
554
|
+
*/
|
|
555
|
+
const exportDef = defineNode({ kind: "Export" });
|
|
556
|
+
/**
|
|
557
|
+
* Definition for the {@link SourceNode}.
|
|
558
|
+
*/
|
|
559
|
+
const sourceDef = defineNode({ kind: "Source" });
|
|
560
|
+
/**
|
|
561
|
+
* Definition for the {@link FileNode}. The fully resolved builder lives in
|
|
562
|
+
* `createFile`, so this definition only supplies the guard.
|
|
563
|
+
*/
|
|
564
|
+
const fileDef = defineNode({ kind: "File" });
|
|
565
|
+
/**
|
|
566
|
+
* Creates an `ImportNode` representing a language-agnostic import/dependency declaration.
|
|
567
|
+
*
|
|
568
|
+
* @example Named import
|
|
569
|
+
* ```ts
|
|
570
|
+
* createImport({ name: ['useState'], path: 'react' })
|
|
571
|
+
* // import { useState } from 'react'
|
|
572
|
+
* ```
|
|
573
|
+
*/
|
|
574
|
+
const createImport = importDef.create;
|
|
575
|
+
/**
|
|
576
|
+
* Creates an `ExportNode` representing a language-agnostic export/public API declaration.
|
|
577
|
+
*
|
|
578
|
+
* @example Named export
|
|
579
|
+
* ```ts
|
|
580
|
+
* createExport({ name: ['Pet'], path: './Pet' })
|
|
581
|
+
* // export { Pet } from './Pet'
|
|
582
|
+
* ```
|
|
583
|
+
*/
|
|
584
|
+
const createExport = exportDef.create;
|
|
585
|
+
/**
|
|
586
|
+
* Creates a `SourceNode` representing a fragment of source code within a file.
|
|
587
|
+
*
|
|
588
|
+
* @example
|
|
589
|
+
* ```ts
|
|
590
|
+
* createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')], isExportable: true })
|
|
591
|
+
* ```
|
|
592
|
+
*/
|
|
593
|
+
const createSource = sourceDef.create;
|
|
594
|
+
/**
|
|
595
|
+
* Creates a fully resolved `FileNode` from a file input descriptor.
|
|
596
|
+
*
|
|
597
|
+
* Computes:
|
|
598
|
+
* - `id` SHA256 hash of the file path
|
|
599
|
+
* - `name` `baseName` without extension
|
|
600
|
+
* - `extname` extension extracted from `baseName`
|
|
601
|
+
*
|
|
602
|
+
* Deduplicates:
|
|
603
|
+
* - `sources` via `combineSources`
|
|
604
|
+
* - `exports` via `combineExports`
|
|
605
|
+
* - `imports` via `combineImports` (also filters unused imports)
|
|
606
|
+
*
|
|
607
|
+
* @throws {Error} when `baseName` has no extension.
|
|
608
|
+
*
|
|
609
|
+
* @example
|
|
610
|
+
* ```ts
|
|
611
|
+
* const file = createFile({
|
|
612
|
+
* baseName: 'petStore.ts',
|
|
613
|
+
* path: 'src/models/petStore.ts',
|
|
614
|
+
* sources: [createSource({ name: 'Pet', nodes: [createText('export type Pet = { id: number }')] })],
|
|
615
|
+
* imports: [createImport({ name: ['z'], path: 'zod' })],
|
|
616
|
+
* exports: [createExport({ name: ['Pet'], path: './petStore' })],
|
|
617
|
+
* })
|
|
618
|
+
* // file.id = SHA256 hash of 'src/models/petStore.ts'
|
|
619
|
+
* // file.name = 'petStore'
|
|
620
|
+
* // file.extname = '.ts'
|
|
621
|
+
* ```
|
|
622
|
+
*
|
|
623
|
+
* @example Copy a real file into the output verbatim
|
|
624
|
+
* ```ts
|
|
625
|
+
* const file = createFile({
|
|
626
|
+
* baseName: 'client.ts',
|
|
627
|
+
* path: 'src/gen/client.ts',
|
|
628
|
+
* copy: '/abs/path/to/templates/client.ts',
|
|
629
|
+
* })
|
|
630
|
+
* ```
|
|
631
|
+
*/
|
|
632
|
+
function createFile(input) {
|
|
633
|
+
const extname = path.extname(input.baseName) || (input.baseName.startsWith(".") ? input.baseName : "");
|
|
634
|
+
if (!extname) throw new Error(`No extname found for ${input.baseName}`);
|
|
635
|
+
const source = (input.sources ?? []).flatMap((item) => item.nodes ?? []).map((node) => extractStringsFromNodes([node])).filter(Boolean).join("\n\n");
|
|
636
|
+
const resolvedExports = input.exports?.length ? combineExports(input.exports) : [];
|
|
637
|
+
const combinedImports = input.imports?.length ? combineImports(input.imports, resolvedExports, source || void 0) : [];
|
|
638
|
+
const localNames = new Set((input.sources ?? []).map((item) => item.name).filter((name) => Boolean(name)));
|
|
639
|
+
const nameOf = (item) => typeof item === "string" ? item : item.name ?? item.propertyName;
|
|
640
|
+
const resolvedImports = combinedImports.filter((imp) => imp.path !== input.path).flatMap((imp) => {
|
|
641
|
+
if (!Array.isArray(imp.name)) return typeof imp.name === "string" && localNames.has(imp.name) ? [] : [imp];
|
|
642
|
+
const kept = imp.name.filter((item) => !localNames.has(nameOf(item)));
|
|
643
|
+
if (!kept.length) return [];
|
|
644
|
+
return [kept.length === imp.name.length ? imp : {
|
|
645
|
+
...imp,
|
|
646
|
+
name: kept
|
|
647
|
+
}];
|
|
648
|
+
});
|
|
649
|
+
const resolvedSources = input.sources?.length ? combineSources(input.sources) : [];
|
|
650
|
+
return {
|
|
651
|
+
kind: "File",
|
|
652
|
+
...input,
|
|
653
|
+
id: hash("sha256", input.path, "hex"),
|
|
654
|
+
name: trimExtName(input.baseName),
|
|
655
|
+
extname,
|
|
656
|
+
imports: resolvedImports,
|
|
657
|
+
exports: resolvedExports,
|
|
658
|
+
sources: resolvedSources,
|
|
659
|
+
meta: input.meta ?? {}
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
//#endregion
|
|
663
|
+
//#region src/nodes/input.ts
|
|
664
|
+
/**
|
|
665
|
+
* Definition for the {@link InputNode}.
|
|
666
|
+
*/
|
|
667
|
+
const inputDef = defineNode({
|
|
668
|
+
kind: "Input",
|
|
669
|
+
defaults: {
|
|
670
|
+
schemas: [],
|
|
671
|
+
operations: [],
|
|
672
|
+
meta: {
|
|
673
|
+
circularNames: [],
|
|
674
|
+
enumNames: []
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
children: ["schemas", "operations"],
|
|
678
|
+
visitorKey: "input"
|
|
679
|
+
});
|
|
680
|
+
/**
|
|
681
|
+
* Creates an `InputNode`. Pass `stream: true` for the streaming variant whose `schemas` and
|
|
682
|
+
* `operations` are `AsyncIterable` sources. Otherwise it builds the eager variant with array
|
|
683
|
+
* `schemas`/`operations`. Both variants get the defaulted `meta`.
|
|
684
|
+
*
|
|
685
|
+
* @example Eager
|
|
686
|
+
* ```ts
|
|
687
|
+
* const input = createInput()
|
|
688
|
+
* // { kind: 'Input', schemas: [], operations: [] }
|
|
689
|
+
* ```
|
|
690
|
+
*
|
|
691
|
+
* @example Streaming
|
|
692
|
+
* ```ts
|
|
693
|
+
* const node = createInput({ stream: true, schemas: schemasIterable, operations: operationsIterable, meta: { title: 'My API' } })
|
|
694
|
+
* ```
|
|
695
|
+
*/
|
|
696
|
+
function createInput(options = {}) {
|
|
697
|
+
const { stream, ...overrides } = options;
|
|
698
|
+
if (stream) return {
|
|
699
|
+
kind: "Input",
|
|
700
|
+
meta: {
|
|
701
|
+
circularNames: [],
|
|
702
|
+
enumNames: []
|
|
703
|
+
},
|
|
704
|
+
...overrides
|
|
705
|
+
};
|
|
706
|
+
return inputDef.create(overrides);
|
|
707
|
+
}
|
|
708
|
+
//#endregion
|
|
709
|
+
//#region src/nodes/requestBody.ts
|
|
710
|
+
/**
|
|
711
|
+
* Definition for the {@link RequestBodyNode}. Content entries are built upfront with
|
|
712
|
+
* {@link createContent}, mirroring how `parameters` and `responses` take prebuilt nodes.
|
|
713
|
+
*/
|
|
714
|
+
const requestBodyDef = defineNode({
|
|
715
|
+
kind: "RequestBody",
|
|
716
|
+
children: ["content"]
|
|
717
|
+
});
|
|
718
|
+
/**
|
|
719
|
+
* Creates a `RequestBodyNode`.
|
|
720
|
+
*/
|
|
721
|
+
const createRequestBody = requestBodyDef.create;
|
|
722
|
+
//#endregion
|
|
723
|
+
//#region src/nodes/operation.ts
|
|
724
|
+
/**
|
|
725
|
+
* Definition for the {@link OperationNode}. HTTP operations (those carrying both
|
|
726
|
+
* `method` and `path`) are tagged with `protocol: 'http'`, and the request body is
|
|
727
|
+
* normalized into a `RequestBodyNode`.
|
|
728
|
+
*/
|
|
729
|
+
const operationDef = defineNode({
|
|
730
|
+
kind: "Operation",
|
|
731
|
+
build: (props) => {
|
|
732
|
+
const { requestBody, ...rest } = props;
|
|
733
|
+
const isHttp = rest.method !== void 0 && rest.path !== void 0;
|
|
734
|
+
return {
|
|
735
|
+
tags: [],
|
|
736
|
+
parameters: [],
|
|
737
|
+
responses: [],
|
|
738
|
+
...rest,
|
|
739
|
+
...isHttp ? { protocol: "http" } : {},
|
|
740
|
+
requestBody: requestBody ? createRequestBody(requestBody) : void 0
|
|
741
|
+
};
|
|
742
|
+
},
|
|
743
|
+
children: [
|
|
744
|
+
"parameters",
|
|
745
|
+
"requestBody",
|
|
746
|
+
"responses"
|
|
747
|
+
],
|
|
748
|
+
visitorKey: "operation"
|
|
749
|
+
});
|
|
750
|
+
function createOperation(props) {
|
|
751
|
+
return operationDef.create(props);
|
|
752
|
+
}
|
|
753
|
+
//#endregion
|
|
754
|
+
//#region src/nodes/output.ts
|
|
755
|
+
/**
|
|
756
|
+
* Definition for the {@link OutputNode}.
|
|
757
|
+
*/
|
|
758
|
+
const outputDef = defineNode({
|
|
759
|
+
kind: "Output",
|
|
760
|
+
defaults: { files: [] },
|
|
761
|
+
visitorKey: "output"
|
|
762
|
+
});
|
|
763
|
+
/**
|
|
764
|
+
* Creates an `OutputNode` with a stable default for `files`.
|
|
765
|
+
*
|
|
766
|
+
* @example
|
|
767
|
+
* ```ts
|
|
768
|
+
* const output = createOutput()
|
|
769
|
+
* // { kind: 'Output', files: [] }
|
|
770
|
+
* ```
|
|
771
|
+
*/
|
|
772
|
+
function createOutput(overrides = {}) {
|
|
773
|
+
return outputDef.create(overrides);
|
|
774
|
+
}
|
|
775
|
+
//#endregion
|
|
776
|
+
//#region src/optionality.ts
|
|
777
|
+
/**
|
|
778
|
+
* Generic JSON Schema optionality: a non-required field is optional, and a
|
|
779
|
+
* non-required nullable field is nullish.
|
|
780
|
+
*/
|
|
781
|
+
function optionality(schema, required) {
|
|
782
|
+
const nullable = schema.nullable ?? false;
|
|
783
|
+
return {
|
|
784
|
+
...schema,
|
|
785
|
+
optional: !required && !nullable ? true : void 0,
|
|
786
|
+
nullish: !required && nullable ? true : void 0
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
//#endregion
|
|
790
|
+
//#region src/nodes/parameter.ts
|
|
791
|
+
/**
|
|
792
|
+
* Definition for the {@link ParameterNode}. `required` defaults to `false`, and the schema's
|
|
793
|
+
* `optional`/`nullish` flags are derived from it through {@link optionality}.
|
|
794
|
+
*/
|
|
795
|
+
const parameterDef = defineNode({
|
|
796
|
+
kind: "Parameter",
|
|
797
|
+
build: (props) => {
|
|
798
|
+
const required = props.required ?? false;
|
|
799
|
+
return {
|
|
800
|
+
...props,
|
|
801
|
+
required,
|
|
802
|
+
schema: optionality(props.schema, required)
|
|
803
|
+
};
|
|
804
|
+
},
|
|
805
|
+
children: ["schema"],
|
|
806
|
+
visitorKey: "parameter"
|
|
807
|
+
});
|
|
808
|
+
/**
|
|
809
|
+
* Creates a `ParameterNode`.
|
|
810
|
+
*
|
|
811
|
+
* @example
|
|
812
|
+
* ```ts
|
|
813
|
+
* const param = createParameter({
|
|
814
|
+
* name: 'petId',
|
|
815
|
+
* in: 'path',
|
|
816
|
+
* required: true,
|
|
817
|
+
* schema: createSchema({ type: 'string' }),
|
|
818
|
+
* })
|
|
819
|
+
* ```
|
|
820
|
+
*/
|
|
821
|
+
const createParameter = parameterDef.create;
|
|
822
|
+
//#endregion
|
|
823
|
+
//#region src/nodes/property.ts
|
|
824
|
+
/**
|
|
825
|
+
* Definition for the {@link PropertyNode}. `required` defaults to `false`, and the schema's
|
|
826
|
+
* `optional`/`nullish` flags are derived from it through {@link optionality}.
|
|
827
|
+
*/
|
|
828
|
+
const propertyDef = defineNode({
|
|
829
|
+
kind: "Property",
|
|
830
|
+
build: (props) => {
|
|
831
|
+
const required = props.required ?? false;
|
|
832
|
+
return {
|
|
833
|
+
...props,
|
|
834
|
+
required,
|
|
835
|
+
schema: optionality(props.schema, required)
|
|
836
|
+
};
|
|
837
|
+
},
|
|
838
|
+
children: ["schema"],
|
|
839
|
+
visitorKey: "property"
|
|
840
|
+
});
|
|
841
|
+
/**
|
|
842
|
+
* Creates a `PropertyNode`.
|
|
843
|
+
*
|
|
844
|
+
* @example
|
|
845
|
+
* ```ts
|
|
846
|
+
* const property = createProperty({
|
|
847
|
+
* name: 'status',
|
|
848
|
+
* required: true,
|
|
849
|
+
* schema: createSchema({ type: 'string', nullable: true }),
|
|
850
|
+
* })
|
|
851
|
+
* // required=true, no optional/nullish
|
|
852
|
+
* ```
|
|
853
|
+
*/
|
|
854
|
+
const createProperty = propertyDef.create;
|
|
855
|
+
//#endregion
|
|
856
|
+
//#region src/nodes/response.ts
|
|
857
|
+
/**
|
|
858
|
+
* Definition for the {@link ResponseNode}. A single legacy `schema` (with optional
|
|
859
|
+
* `mediaType`/`keysToOmit`) is normalized into one `content` entry.
|
|
860
|
+
*/
|
|
861
|
+
const responseDef = defineNode({
|
|
862
|
+
kind: "Response",
|
|
863
|
+
build: (props) => {
|
|
864
|
+
const { schema, mediaType, keysToOmit, content, ...rest } = props;
|
|
865
|
+
const entries = content ?? (schema ? [createContent({
|
|
866
|
+
contentType: mediaType ?? "application/json",
|
|
867
|
+
schema,
|
|
868
|
+
keysToOmit: keysToOmit ?? null
|
|
869
|
+
})] : void 0);
|
|
870
|
+
return {
|
|
871
|
+
...rest,
|
|
872
|
+
content: entries
|
|
873
|
+
};
|
|
874
|
+
},
|
|
875
|
+
children: ["content"],
|
|
876
|
+
visitorKey: "response"
|
|
877
|
+
});
|
|
878
|
+
/**
|
|
879
|
+
* Creates a `ResponseNode`.
|
|
880
|
+
*
|
|
881
|
+
* @example
|
|
882
|
+
* ```ts
|
|
883
|
+
* const response = createResponse({
|
|
884
|
+
* statusCode: '200',
|
|
885
|
+
* content: [createContent({ contentType: 'application/json', schema: createSchema({ type: 'object', properties: [] }) })],
|
|
886
|
+
* })
|
|
887
|
+
* ```
|
|
888
|
+
*/
|
|
889
|
+
const createResponse = responseDef.create;
|
|
890
|
+
//#endregion
|
|
891
|
+
//#region src/nodes/schema.ts
|
|
892
|
+
/**
|
|
893
|
+
* Maps schema `type` to its underlying `primitive`.
|
|
894
|
+
* Primitive types map to themselves and special string formats map to `'string'`.
|
|
895
|
+
* Any type not listed here (such as `ref`, `enum`, `union`, `intersection`, `tuple`, `ipv4`, `ipv6`, `blob`) has no `primitive`.
|
|
896
|
+
*/
|
|
897
|
+
const TYPE_TO_PRIMITIVE = {
|
|
898
|
+
string: "string",
|
|
899
|
+
number: "number",
|
|
900
|
+
integer: "integer",
|
|
901
|
+
bigint: "bigint",
|
|
902
|
+
boolean: "boolean",
|
|
903
|
+
null: "null",
|
|
904
|
+
any: "any",
|
|
905
|
+
unknown: "unknown",
|
|
906
|
+
void: "void",
|
|
907
|
+
never: "never",
|
|
908
|
+
object: "object",
|
|
909
|
+
array: "array",
|
|
910
|
+
date: "date",
|
|
911
|
+
uuid: "string",
|
|
912
|
+
email: "string",
|
|
913
|
+
url: "string",
|
|
914
|
+
datetime: "string",
|
|
915
|
+
time: "string"
|
|
916
|
+
};
|
|
917
|
+
/**
|
|
918
|
+
* Definition for the {@link SchemaNode}. Object schemas default `properties` to an
|
|
919
|
+
* empty array, and `primitive` is inferred from `type` when not explicitly provided.
|
|
920
|
+
*/
|
|
921
|
+
const schemaDef = defineNode({
|
|
922
|
+
kind: "Schema",
|
|
923
|
+
build: (props) => {
|
|
924
|
+
if (props.type === "object") return {
|
|
925
|
+
properties: [],
|
|
926
|
+
primitive: "object",
|
|
927
|
+
...props
|
|
928
|
+
};
|
|
929
|
+
return {
|
|
930
|
+
primitive: TYPE_TO_PRIMITIVE[props.type],
|
|
931
|
+
...props
|
|
932
|
+
};
|
|
933
|
+
},
|
|
934
|
+
children: [
|
|
935
|
+
"properties",
|
|
936
|
+
"items",
|
|
937
|
+
"members",
|
|
938
|
+
"additionalProperties"
|
|
939
|
+
],
|
|
940
|
+
visitorKey: "schema"
|
|
941
|
+
});
|
|
942
|
+
function createSchema(props) {
|
|
943
|
+
return schemaDef.create(props);
|
|
944
|
+
}
|
|
945
|
+
//#endregion
|
|
946
|
+
//#region src/registry.ts
|
|
947
|
+
/**
|
|
948
|
+
* Every node definition. Adding a node means adding its `defineNode` to one
|
|
949
|
+
* `nodes/*.ts` file and listing it here. The visitor tables in `visitor.ts` derive from it.
|
|
950
|
+
*/
|
|
951
|
+
const nodeDefs = [
|
|
952
|
+
inputDef,
|
|
953
|
+
outputDef,
|
|
954
|
+
operationDef,
|
|
955
|
+
requestBodyDef,
|
|
956
|
+
contentDef,
|
|
957
|
+
responseDef,
|
|
958
|
+
schemaDef,
|
|
959
|
+
propertyDef,
|
|
960
|
+
parameterDef,
|
|
961
|
+
constDef,
|
|
962
|
+
typeDef,
|
|
963
|
+
functionDef,
|
|
964
|
+
arrowFunctionDef,
|
|
965
|
+
textDef,
|
|
966
|
+
breakDef,
|
|
967
|
+
jsxDef,
|
|
968
|
+
importDef,
|
|
969
|
+
exportDef,
|
|
970
|
+
sourceDef,
|
|
971
|
+
fileDef
|
|
972
|
+
];
|
|
973
|
+
//#endregion
|
|
974
|
+
//#region src/visitor.ts
|
|
975
|
+
/**
|
|
976
|
+
* Child node fields per node kind, in traversal order (Babel's `VISITOR_KEYS`).
|
|
977
|
+
* Derived from each definition's `children`.
|
|
978
|
+
*/
|
|
979
|
+
const VISITOR_KEYS = Object.fromEntries(nodeDefs.flatMap((def) => def.children ? [[def.kind, def.children]] : []));
|
|
980
|
+
/**
|
|
981
|
+
* Maps a node kind to the matching visitor callback name. Derived from each
|
|
982
|
+
* definition's `visitorKey`.
|
|
983
|
+
*/
|
|
984
|
+
const VISITOR_KEY_BY_KIND = Object.fromEntries(nodeDefs.flatMap((def) => def.visitorKey ? [[def.kind, def.visitorKey]] : []));
|
|
985
|
+
/**
|
|
986
|
+
* Creates a small async concurrency limiter.
|
|
987
|
+
*
|
|
988
|
+
* At most `concurrency` tasks are in flight at once. Extra tasks are queued.
|
|
989
|
+
*
|
|
990
|
+
* @example
|
|
991
|
+
* ```ts
|
|
992
|
+
* const limit = createLimit(2)
|
|
993
|
+
* for (const task of [taskA, taskB, taskC]) {
|
|
994
|
+
* await limit(() => task())
|
|
995
|
+
* }
|
|
996
|
+
* // only 2 tasks run at the same time
|
|
997
|
+
* ```
|
|
998
|
+
*/
|
|
999
|
+
function createLimit(concurrency) {
|
|
1000
|
+
let active = 0;
|
|
1001
|
+
const queue = [];
|
|
1002
|
+
function next() {
|
|
1003
|
+
if (active < concurrency && queue.length > 0) {
|
|
1004
|
+
active++;
|
|
1005
|
+
queue.shift()();
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
return function limit(fn) {
|
|
1009
|
+
return new Promise((resolve, reject) => {
|
|
1010
|
+
queue.push(() => {
|
|
1011
|
+
Promise.resolve(fn()).then(resolve, reject).finally(() => {
|
|
1012
|
+
active--;
|
|
1013
|
+
next();
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
1016
|
+
next();
|
|
1017
|
+
});
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
const visitorKeysByKind = VISITOR_KEYS;
|
|
1021
|
+
/**
|
|
1022
|
+
* Returns `true` when `value` is an AST node (an object carrying a `kind`).
|
|
1023
|
+
*/
|
|
1024
|
+
function isNode(value) {
|
|
1025
|
+
return typeof value === "object" && value !== null && "kind" in value;
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Returns the immediate traversable children of `node` based on {@link VISITOR_KEYS}.
|
|
1029
|
+
*
|
|
1030
|
+
* `Schema` children are only included when `recurse` is `true`. Shallow mode skips them.
|
|
1031
|
+
*
|
|
1032
|
+
* @example
|
|
1033
|
+
* ```ts
|
|
1034
|
+
* const children = getChildren(operationNode, true)
|
|
1035
|
+
* // returns parameters, the request body, and responses
|
|
1036
|
+
* ```
|
|
1037
|
+
*/
|
|
1038
|
+
function* getChildren(node, recurse) {
|
|
1039
|
+
if (node.kind === "Schema" && !recurse) return;
|
|
1040
|
+
const keys = visitorKeysByKind[node.kind];
|
|
1041
|
+
if (!keys) return;
|
|
1042
|
+
const record = node;
|
|
1043
|
+
for (const key of keys) {
|
|
1044
|
+
const value = record[key];
|
|
1045
|
+
if (Array.isArray(value)) {
|
|
1046
|
+
for (const item of value) if (isNode(item)) yield item;
|
|
1047
|
+
} else if (isNode(value)) yield value;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Runs the visitor callback that matches `node.kind` with the traversal
|
|
1052
|
+
* context. The result is a replacement node, a collected value, or `undefined`
|
|
1053
|
+
* when no callback is registered for the kind.
|
|
1054
|
+
*
|
|
1055
|
+
* Shared by `walk`, `transform`, and `collectLazy` so node-kind dispatch lives
|
|
1056
|
+
* in one place. `TResult` is the caller's expected return: the same node type
|
|
1057
|
+
* for `transform`, the collected value type for `collectLazy`, ignored for `walk`.
|
|
1058
|
+
*/
|
|
1059
|
+
function applyVisitor(node, visitor, parent) {
|
|
1060
|
+
const key = VISITOR_KEY_BY_KIND[node.kind];
|
|
1061
|
+
if (!key) return void 0;
|
|
1062
|
+
const fn = visitor[key];
|
|
1063
|
+
return fn?.(node, { parent });
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Async depth-first traversal for side effects. Visitor return values are
|
|
1067
|
+
* ignored. Use `transform` when you want to rewrite nodes.
|
|
1068
|
+
*
|
|
1069
|
+
* Sibling nodes at each depth run concurrently up to `options.concurrency`
|
|
1070
|
+
* (defaults to `WALK_CONCURRENCY`). Higher values overlap I/O-bound visitor
|
|
1071
|
+
* work. Lower values reduce memory pressure.
|
|
1072
|
+
*
|
|
1073
|
+
* @example Log every operation
|
|
1074
|
+
* ```ts
|
|
1075
|
+
* await walk(root, {
|
|
1076
|
+
* operation(node) {
|
|
1077
|
+
* console.log(node.operationId)
|
|
1078
|
+
* },
|
|
1079
|
+
* })
|
|
1080
|
+
* ```
|
|
1081
|
+
*
|
|
1082
|
+
* @example Only visit the root node
|
|
1083
|
+
* ```ts
|
|
1084
|
+
* await walk(root, { depth: 'shallow', input: () => {} })
|
|
1085
|
+
* ```
|
|
1086
|
+
*/
|
|
1087
|
+
async function walk(node, options) {
|
|
1088
|
+
return _walk(node, options, (options.depth ?? visitorDepths.deep) === visitorDepths.deep, createLimit(options.concurrency ?? 30), void 0);
|
|
1089
|
+
}
|
|
1090
|
+
async function _walk(node, visitor, recurse, limit, parent) {
|
|
1091
|
+
await limit(() => applyVisitor(node, visitor, parent));
|
|
1092
|
+
const children = Array.from(getChildren(node, recurse));
|
|
1093
|
+
if (children.length === 0) return;
|
|
1094
|
+
await Promise.all(children.map((child) => _walk(child, visitor, recurse, limit, node)));
|
|
1095
|
+
}
|
|
1096
|
+
function transform(node, options) {
|
|
1097
|
+
const { depth, parent, ...visitor } = options;
|
|
1098
|
+
const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
|
|
1099
|
+
return transformChildren(applyVisitor(node, visitor, parent) ?? node, options, recurse);
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Immutably rebuilds a node's children using {@link VISITOR_KEYS}, transforming
|
|
1103
|
+
* each child node and leaving non-node values (e.g. `additionalProperties: true`) intact.
|
|
1104
|
+
* `Schema` children are skipped in shallow mode.
|
|
1105
|
+
*/
|
|
1106
|
+
function transformChildren(node, options, recurse) {
|
|
1107
|
+
if (node.kind === "Schema" && !recurse) return node;
|
|
1108
|
+
const keys = visitorKeysByKind[node.kind];
|
|
1109
|
+
if (!keys) return node;
|
|
1110
|
+
const record = node;
|
|
1111
|
+
const childOptions = {
|
|
1112
|
+
...options,
|
|
1113
|
+
parent: node
|
|
1114
|
+
};
|
|
1115
|
+
let updates;
|
|
1116
|
+
for (const key of keys) {
|
|
1117
|
+
if (!(key in record)) continue;
|
|
1118
|
+
const value = record[key];
|
|
1119
|
+
if (Array.isArray(value)) {
|
|
1120
|
+
let changed = false;
|
|
1121
|
+
const mapped = value.map((item) => {
|
|
1122
|
+
if (!isNode(item)) return item;
|
|
1123
|
+
const next = transform(item, childOptions);
|
|
1124
|
+
if (next !== item) changed = true;
|
|
1125
|
+
return next;
|
|
1126
|
+
});
|
|
1127
|
+
if (changed) (updates ??= {})[key] = mapped;
|
|
1128
|
+
} else if (isNode(value)) {
|
|
1129
|
+
const next = transform(value, childOptions);
|
|
1130
|
+
if (next !== value) (updates ??= {})[key] = next;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return updates ? {
|
|
1134
|
+
...node,
|
|
1135
|
+
...updates
|
|
1136
|
+
} : node;
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Lazy depth-first collection pass. Yields every non-null value returned by
|
|
1140
|
+
* the visitor callbacks. Use `collect` for the eager array form.
|
|
1141
|
+
*
|
|
1142
|
+
* @example Collect every operationId
|
|
1143
|
+
* ```ts
|
|
1144
|
+
* const ids: string[] = []
|
|
1145
|
+
* for (const id of collectLazy<string>(root, {
|
|
1146
|
+
* operation(node) {
|
|
1147
|
+
* return node.operationId
|
|
1148
|
+
* },
|
|
1149
|
+
* })) {
|
|
1150
|
+
* ids.push(id)
|
|
1151
|
+
* }
|
|
1152
|
+
* ```
|
|
1153
|
+
*/
|
|
1154
|
+
function* collectLazy(node, options) {
|
|
1155
|
+
const { depth, parent, ...visitor } = options;
|
|
1156
|
+
const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep;
|
|
1157
|
+
const v = applyVisitor(node, visitor, parent);
|
|
1158
|
+
if (v != null) yield v;
|
|
1159
|
+
for (const child of getChildren(node, recurse)) yield* collectLazy(child, {
|
|
1160
|
+
...options,
|
|
1161
|
+
parent: node
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Eager depth-first collection pass. Gathers every non-null value the visitor
|
|
1166
|
+
* callbacks return into an array.
|
|
1167
|
+
*
|
|
1168
|
+
* @example Collect every operationId
|
|
1169
|
+
* ```ts
|
|
1170
|
+
* const ids = collect<string>(root, {
|
|
1171
|
+
* operation(node) {
|
|
1172
|
+
* return node.operationId
|
|
1173
|
+
* },
|
|
1174
|
+
* })
|
|
1175
|
+
* ```
|
|
1176
|
+
*/
|
|
1177
|
+
function collect(node, options) {
|
|
1178
|
+
return Array.from(collectLazy(node, options));
|
|
1179
|
+
}
|
|
1180
|
+
//#endregion
|
|
1181
|
+
export { schemaTypes as $, sourceDef as A, createConst as B, createExport as C, exportDef as D, createSource as E, arrowFunctionDef as F, functionDef as G, createJsx as H, breakDef as I, typeDef as J, jsxDef as K, constDef as L, pascalCase as M, contentDef as N, fileDef as O, createContent as P, narrowSchema as Q, createArrowFunction as R, inputDef as S, createImport as T, createText as U, createFunction as V, createType as W, visitorKeys as X, defineNode as Y, isHttpOperationNode as Z, createOperation as _, nodeDefs as a, requestBodyDef as b, createResponse as c, propertyDef as d, createParameter as f, outputDef as g, createOutput as h, walk as i, extractStringsFromNodes as j, importDef as k, responseDef as l, optionality as m, collectLazy as n, createSchema as o, parameterDef as p, textDef as q, transform as r, schemaDef as s, collect as t, createProperty as u, operationDef as v, createFile as w, createInput as x, createRequestBody as y, createBreak as z };
|
|
1182
|
+
|
|
1183
|
+
//# sourceMappingURL=visitor-Ns-njjbG.js.map
|