@kubb/core 5.0.0-beta.84 → 5.0.0-beta.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{diagnostics-DuaXn2bT.d.ts → Diagnostics-aWJg-H2d.d.ts} +344 -440
- package/dist/index.cjs +850 -1122
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +38 -94
- package/dist/index.js +812 -1084
- package/dist/index.js.map +1 -1
- package/dist/mocks.cjs +7 -14
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.ts +1 -1
- package/dist/mocks.js +3 -10
- package/dist/mocks.js.map +1 -1
- package/dist/{memoryStorage-DQ6qXJ8o.cjs → usingCtx-BN2OKJRx.cjs} +182 -520
- package/dist/usingCtx-BN2OKJRx.cjs.map +1 -0
- package/dist/{memoryStorage-BjUIqpYE.js → usingCtx-Cnrm3TcM.js} +179 -481
- package/dist/usingCtx-Cnrm3TcM.js.map +1 -0
- package/package.json +2 -2
- package/dist/memoryStorage-BjUIqpYE.js.map +0 -1
- package/dist/memoryStorage-DQ6qXJ8o.cjs.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,14 +1,49 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const
|
|
3
|
-
let
|
|
2
|
+
const require_usingCtx = require("./usingCtx-BN2OKJRx.cjs");
|
|
3
|
+
let node_async_hooks = require("node:async_hooks");
|
|
4
4
|
let node_util = require("node:util");
|
|
5
|
+
let node_crypto = require("node:crypto");
|
|
5
6
|
let node_fs_promises = require("node:fs/promises");
|
|
6
7
|
let node_path = require("node:path");
|
|
7
|
-
node_path =
|
|
8
|
-
let node_async_hooks = require("node:async_hooks");
|
|
8
|
+
node_path = require_usingCtx.__toESM(node_path, 1);
|
|
9
9
|
let _kubb_ast = require("@kubb/ast");
|
|
10
10
|
let node_process = require("node:process");
|
|
11
|
-
node_process =
|
|
11
|
+
node_process = require_usingCtx.__toESM(node_process, 1);
|
|
12
|
+
//#region src/createAdapter.ts
|
|
13
|
+
/**
|
|
14
|
+
* Defines a custom adapter that translates a spec format into Kubb's universal
|
|
15
|
+
* AST, for example GraphQL, gRPC, or AsyncAPI. The built-in `@kubb/adapter-oas`
|
|
16
|
+
* handles OpenAPI/Swagger documents.
|
|
17
|
+
*
|
|
18
|
+
* Adapters must return an `InputNode` from `parse`. That node is what every
|
|
19
|
+
* plugin in the build consumes.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { createAdapter, type AdapterFactoryOptions } from '@kubb/core'
|
|
24
|
+
* import { ast } from '@kubb/ast'
|
|
25
|
+
*
|
|
26
|
+
* type MyAdapter = AdapterFactoryOptions<'my-adapter', { validate?: boolean }>
|
|
27
|
+
*
|
|
28
|
+
* export const myAdapter = createAdapter<MyAdapter>((options) => ({
|
|
29
|
+
* name: 'my-adapter',
|
|
30
|
+
* options,
|
|
31
|
+
* document: null,
|
|
32
|
+
* async parse(_source) {
|
|
33
|
+
* // Convert the source (path or inline data) into an InputNode.
|
|
34
|
+
* return ast.factory.createInput()
|
|
35
|
+
* },
|
|
36
|
+
* getImports: () => [],
|
|
37
|
+
* async validate() {
|
|
38
|
+
* // Throw here when the spec is invalid.
|
|
39
|
+
* },
|
|
40
|
+
* }))
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
function createAdapter(build) {
|
|
44
|
+
return (options) => build(options ?? {});
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
12
47
|
//#region ../../internals/utils/src/time.ts
|
|
13
48
|
/**
|
|
14
49
|
* Calculates elapsed time in milliseconds from a high-resolution `process.hrtime` start time.
|
|
@@ -92,21 +127,11 @@ const randomColors = [
|
|
|
92
127
|
*/
|
|
93
128
|
function randomCliColor(text) {
|
|
94
129
|
if (!text) return "";
|
|
95
|
-
|
|
130
|
+
const index = (0, node_crypto.hash)("sha256", text, "buffer").readUInt32BE(0) % randomColors.length;
|
|
131
|
+
return (0, node_util.styleText)(randomColors[index] ?? "white", text);
|
|
96
132
|
}
|
|
97
133
|
//#endregion
|
|
98
134
|
//#region ../../internals/utils/src/promise.ts
|
|
99
|
-
/** Returns `true` when `result` is a thenable `Promise`.
|
|
100
|
-
*
|
|
101
|
-
* @example
|
|
102
|
-
* ```ts
|
|
103
|
-
* isPromise(Promise.resolve(1)) // true
|
|
104
|
-
* isPromise(42) // false
|
|
105
|
-
* ```
|
|
106
|
-
*/
|
|
107
|
-
function isPromise(result) {
|
|
108
|
-
return result !== null && result !== void 0 && typeof result["then"] === "function";
|
|
109
|
-
}
|
|
110
135
|
/**
|
|
111
136
|
* Wraps `factory` with a keyed cache backed by the provided store.
|
|
112
137
|
*
|
|
@@ -145,258 +170,116 @@ function memoize(store, factory) {
|
|
|
145
170
|
return value;
|
|
146
171
|
};
|
|
147
172
|
}
|
|
148
|
-
/**
|
|
149
|
-
* Wraps a plain array in a reusable `AsyncIterable`.
|
|
150
|
-
* Each `[Symbol.asyncIterator]()` call returns a fresh generator so the
|
|
151
|
-
* iterable can be consumed multiple times (e.g. once per plugin pre-scan).
|
|
152
|
-
*
|
|
153
|
-
* @example
|
|
154
|
-
* ```ts
|
|
155
|
-
* const stream = arrayToAsyncIterable([1, 2, 3])
|
|
156
|
-
* for await (const n of stream) console.log(n) // 1, 2, 3
|
|
157
|
-
* ```
|
|
158
|
-
*/
|
|
159
|
-
function arrayToAsyncIterable(arr) {
|
|
160
|
-
return { [Symbol.asyncIterator]() {
|
|
161
|
-
return (async function* () {
|
|
162
|
-
yield* arr;
|
|
163
|
-
})();
|
|
164
|
-
} };
|
|
165
|
-
}
|
|
166
173
|
//#endregion
|
|
167
|
-
//#region
|
|
174
|
+
//#region package.json
|
|
175
|
+
var version = "5.0.0-beta.86";
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/constants.ts
|
|
168
178
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
179
|
+
* Plugin `include` filter types that select operations directly. When one of these is set
|
|
180
|
+
* without a `schemaName` include, the generate phase pre-scans operations to compute the set
|
|
181
|
+
* of schemas they reach, so unreachable schemas can be pruned for that plugin.
|
|
171
182
|
*/
|
|
172
|
-
const
|
|
173
|
-
"
|
|
174
|
-
"
|
|
175
|
-
"
|
|
176
|
-
"
|
|
177
|
-
"
|
|
178
|
-
"case",
|
|
179
|
-
"catch",
|
|
180
|
-
"char",
|
|
181
|
-
"class",
|
|
182
|
-
"const",
|
|
183
|
-
"continue",
|
|
184
|
-
"debugger",
|
|
185
|
-
"default",
|
|
186
|
-
"delete",
|
|
187
|
-
"do",
|
|
188
|
-
"double",
|
|
189
|
-
"else",
|
|
190
|
-
"enum",
|
|
191
|
-
"eval",
|
|
192
|
-
"export",
|
|
193
|
-
"extends",
|
|
194
|
-
"false",
|
|
195
|
-
"final",
|
|
196
|
-
"finally",
|
|
197
|
-
"float",
|
|
198
|
-
"for",
|
|
199
|
-
"function",
|
|
200
|
-
"goto",
|
|
201
|
-
"if",
|
|
202
|
-
"implements",
|
|
203
|
-
"import",
|
|
204
|
-
"in",
|
|
205
|
-
"instanceof",
|
|
206
|
-
"int",
|
|
207
|
-
"interface",
|
|
208
|
-
"let",
|
|
209
|
-
"long",
|
|
210
|
-
"native",
|
|
211
|
-
"new",
|
|
212
|
-
"null",
|
|
213
|
-
"package",
|
|
214
|
-
"private",
|
|
215
|
-
"protected",
|
|
216
|
-
"public",
|
|
217
|
-
"return",
|
|
218
|
-
"short",
|
|
219
|
-
"static",
|
|
220
|
-
"super",
|
|
221
|
-
"switch",
|
|
222
|
-
"synchronized",
|
|
223
|
-
"this",
|
|
224
|
-
"throw",
|
|
225
|
-
"throws",
|
|
226
|
-
"transient",
|
|
227
|
-
"true",
|
|
228
|
-
"try",
|
|
229
|
-
"typeof",
|
|
230
|
-
"var",
|
|
231
|
-
"void",
|
|
232
|
-
"volatile",
|
|
233
|
-
"while",
|
|
234
|
-
"with",
|
|
235
|
-
"yield",
|
|
236
|
-
"Array",
|
|
237
|
-
"Date",
|
|
238
|
-
"hasOwnProperty",
|
|
239
|
-
"Infinity",
|
|
240
|
-
"isFinite",
|
|
241
|
-
"isNaN",
|
|
242
|
-
"isPrototypeOf",
|
|
243
|
-
"length",
|
|
244
|
-
"Math",
|
|
245
|
-
"name",
|
|
246
|
-
"NaN",
|
|
247
|
-
"Number",
|
|
248
|
-
"Object",
|
|
249
|
-
"prototype",
|
|
250
|
-
"String",
|
|
251
|
-
"toString",
|
|
252
|
-
"undefined",
|
|
253
|
-
"valueOf"
|
|
183
|
+
const OPERATION_FILTER_TYPES = /* @__PURE__ */ new Set([
|
|
184
|
+
"tag",
|
|
185
|
+
"operationId",
|
|
186
|
+
"path",
|
|
187
|
+
"method",
|
|
188
|
+
"contentType"
|
|
254
189
|
]);
|
|
255
190
|
/**
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* ```ts
|
|
260
|
-
* isValidVarName('status') // true
|
|
261
|
-
* isValidVarName('class') // false (reserved word)
|
|
262
|
-
* isValidVarName('42foo') // false (starts with digit)
|
|
263
|
-
* ```
|
|
191
|
+
* Stable codes Kubb attaches to a `Diagnostic`. Each maps to a known failure mode
|
|
192
|
+
* and stays stable so it can be referenced in tooling and (later) docs. Reference
|
|
193
|
+
* these instead of inlining the string at a throw site.
|
|
264
194
|
*/
|
|
265
|
-
|
|
266
|
-
if (!name || reservedWords.has(name)) return false;
|
|
267
|
-
return isIdentifier(name);
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Returns `true` when `name` is syntactically a valid identifier, ignoring reserved words.
|
|
271
|
-
*
|
|
272
|
-
* Reserved words and globals (`class`, `name`, `Date`, …) are valid as bare object-literal keys
|
|
273
|
-
* even though they are not valid variable names, so use this (not {@link isValidVarName}) when
|
|
274
|
-
* deciding whether an object key needs quoting.
|
|
275
|
-
*
|
|
276
|
-
* @example
|
|
277
|
-
* ```ts
|
|
278
|
-
* isIdentifier('name') // true
|
|
279
|
-
* isIdentifier('x-total')// false
|
|
280
|
-
* ```
|
|
281
|
-
*/
|
|
282
|
-
function isIdentifier(name) {
|
|
283
|
-
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
|
|
284
|
-
}
|
|
285
|
-
//#endregion
|
|
286
|
-
//#region ../../internals/utils/src/url.ts
|
|
287
|
-
function transformParam(raw, casing) {
|
|
288
|
-
const param = isValidVarName(raw) ? raw : require_memoryStorage.camelCase(raw);
|
|
289
|
-
return casing === "camelcase" ? require_memoryStorage.camelCase(param) : param;
|
|
290
|
-
}
|
|
291
|
-
function toParamsObject(path, { replacer, casing } = {}) {
|
|
292
|
-
const params = {};
|
|
293
|
-
for (const match of path.matchAll(/\{([^}]+)\}/g)) {
|
|
294
|
-
const param = transformParam(match[1], casing);
|
|
295
|
-
const key = replacer ? replacer(param) : param;
|
|
296
|
-
params[key] = key;
|
|
297
|
-
}
|
|
298
|
-
return Object.keys(params).length > 0 ? params : null;
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* Helpers for OpenAPI/Swagger paths, plus a thin wrapper over the native `URL`.
|
|
302
|
-
*/
|
|
303
|
-
var Url = class Url {
|
|
195
|
+
const diagnosticCode = {
|
|
304
196
|
/**
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
* @example
|
|
308
|
-
* Url.toPath('/pet/{petId}') // '/pet/:petId'
|
|
197
|
+
* Fallback for an unstructured error with no specific code.
|
|
309
198
|
*/
|
|
310
|
-
|
|
311
|
-
return path.replace(/\{([^}]+)\}/g, ":$1");
|
|
312
|
-
}
|
|
199
|
+
unknown: "KUBB_UNKNOWN",
|
|
313
200
|
/**
|
|
314
|
-
*
|
|
315
|
-
* `prefix` is prepended inside the literal, `replacer` transforms each parameter name,
|
|
316
|
-
* and `casing` controls parameter identifier casing.
|
|
317
|
-
*
|
|
318
|
-
* @example
|
|
319
|
-
* Url.toTemplateString('/pet/{petId}') // '`/pet/${petId}`'
|
|
320
|
-
*
|
|
321
|
-
* @example
|
|
322
|
-
* Url.toTemplateString('/pet/{petId}', { prefix: 'https://api' }) // '`https://api/pet/${petId}`'
|
|
201
|
+
* The `input.path` file or URL could not be read.
|
|
323
202
|
*/
|
|
324
|
-
|
|
325
|
-
const result = path.split(/\{([^}]+)\}/).map((part, i) => {
|
|
326
|
-
if (i % 2 === 0) return part;
|
|
327
|
-
const param = transformParam(part, casing);
|
|
328
|
-
return `\${${replacer ? replacer(param) : param}}`;
|
|
329
|
-
}).join("");
|
|
330
|
-
return `\`${prefix ?? ""}${result}\``;
|
|
331
|
-
}
|
|
203
|
+
inputNotFound: "KUBB_INPUT_NOT_FOUND",
|
|
332
204
|
/**
|
|
333
|
-
*
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
*
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
205
|
+
* An adapter was configured without an `input`.
|
|
206
|
+
*/
|
|
207
|
+
inputRequired: "KUBB_INPUT_REQUIRED",
|
|
208
|
+
/**
|
|
209
|
+
* A `$ref` (or equivalent reference) could not be resolved in the source document.
|
|
210
|
+
*/
|
|
211
|
+
refNotFound: "KUBB_REF_NOT_FOUND",
|
|
212
|
+
/**
|
|
213
|
+
* A server variable value is not allowed by its `enum`.
|
|
214
|
+
*/
|
|
215
|
+
invalidServerVariable: "KUBB_INVALID_SERVER_VARIABLE",
|
|
216
|
+
/**
|
|
217
|
+
* A required plugin is missing from the config.
|
|
218
|
+
*/
|
|
219
|
+
pluginNotFound: "KUBB_PLUGIN_NOT_FOUND",
|
|
220
|
+
/**
|
|
221
|
+
* A plugin threw while generating.
|
|
222
|
+
*/
|
|
223
|
+
pluginFailed: "KUBB_PLUGIN_FAILED",
|
|
224
|
+
/**
|
|
225
|
+
* A plugin reported a non-fatal warning through `ctx.warn`.
|
|
226
|
+
*/
|
|
227
|
+
pluginWarning: "KUBB_PLUGIN_WARNING",
|
|
228
|
+
/**
|
|
229
|
+
* A plugin reported an informational message through `ctx.info`.
|
|
230
|
+
*/
|
|
231
|
+
pluginInfo: "KUBB_PLUGIN_INFO",
|
|
232
|
+
/**
|
|
233
|
+
* A schema uses a `format` Kubb does not map to a specific type. Reserved for
|
|
234
|
+
* adapters to emit as a `warning`.
|
|
235
|
+
*/
|
|
236
|
+
unsupportedFormat: "KUBB_UNSUPPORTED_FORMAT",
|
|
237
|
+
/**
|
|
238
|
+
* A referenced schema or operation is marked `deprecated`. Reserved for adapters
|
|
239
|
+
* to emit as an `info`.
|
|
240
|
+
*/
|
|
241
|
+
deprecated: "KUBB_DEPRECATED",
|
|
242
|
+
/**
|
|
243
|
+
* An adapter is required but the config has none. The build cannot read the input
|
|
244
|
+
* without one.
|
|
245
|
+
*/
|
|
246
|
+
adapterRequired: "KUBB_ADAPTER_REQUIRED",
|
|
247
|
+
/**
|
|
248
|
+
* A resolved output path escapes the output directory, which can stem from a path
|
|
249
|
+
* traversal in the spec or a misconfigured `group.name`.
|
|
250
|
+
*/
|
|
251
|
+
pathTraversal: "KUBB_PATH_TRAVERSAL",
|
|
252
|
+
/**
|
|
253
|
+
* A plugin's options are invalid, for example `output.mode: 'file'` paired with a `group` option.
|
|
254
|
+
*/
|
|
255
|
+
invalidPluginOptions: "KUBB_INVALID_PLUGIN_OPTIONS",
|
|
256
|
+
/**
|
|
257
|
+
* A post-generate shell hook (`hooks.done`) exited with a failure.
|
|
258
|
+
*/
|
|
259
|
+
hookFailed: "KUBB_HOOK_FAILED",
|
|
260
|
+
/**
|
|
261
|
+
* The formatter pass over the generated files failed.
|
|
262
|
+
*/
|
|
263
|
+
formatFailed: "KUBB_FORMAT_FAILED",
|
|
264
|
+
/**
|
|
265
|
+
* The linter pass over the generated files failed.
|
|
266
|
+
*/
|
|
267
|
+
lintFailed: "KUBB_LINT_FAILED",
|
|
268
|
+
/**
|
|
269
|
+
* Not a failure. Carries a plugin's elapsed time, summed into the run total.
|
|
270
|
+
*/
|
|
271
|
+
performance: "KUBB_PERFORMANCE",
|
|
272
|
+
/**
|
|
273
|
+
* Not a failure. A newer Kubb version is available on npm.
|
|
274
|
+
*/
|
|
275
|
+
updateAvailable: "KUBB_UPDATE_AVAILABLE"
|
|
358
276
|
};
|
|
359
277
|
//#endregion
|
|
360
|
-
//#region src/
|
|
361
|
-
/**
|
|
362
|
-
* Defines a custom adapter that translates a spec format into Kubb's universal
|
|
363
|
-
* AST, for example GraphQL, gRPC, or AsyncAPI. The built-in `@kubb/adapter-oas`
|
|
364
|
-
* handles OpenAPI/Swagger documents.
|
|
365
|
-
*
|
|
366
|
-
* Adapters must return an `InputNode` from `parse`. That node is what every
|
|
367
|
-
* plugin in the build consumes.
|
|
368
|
-
*
|
|
369
|
-
* @example
|
|
370
|
-
* ```ts
|
|
371
|
-
* import { createAdapter, type AdapterFactoryOptions } from '@kubb/core'
|
|
372
|
-
* import { ast } from '@kubb/ast'
|
|
373
|
-
*
|
|
374
|
-
* type MyAdapter = AdapterFactoryOptions<'my-adapter', { validate?: boolean }>
|
|
375
|
-
*
|
|
376
|
-
* export const myAdapter = createAdapter<MyAdapter>((options) => ({
|
|
377
|
-
* name: 'my-adapter',
|
|
378
|
-
* options,
|
|
379
|
-
* document: null,
|
|
380
|
-
* async parse(_source) {
|
|
381
|
-
* // Convert the source (path or inline data) into an InputNode.
|
|
382
|
-
* return ast.factory.createInput()
|
|
383
|
-
* },
|
|
384
|
-
* getImports: () => [],
|
|
385
|
-
* async validate() {
|
|
386
|
-
* // Throw here when the spec is invalid.
|
|
387
|
-
* },
|
|
388
|
-
* }))
|
|
389
|
-
* ```
|
|
390
|
-
*/
|
|
391
|
-
function createAdapter(build) {
|
|
392
|
-
return (options) => build(options ?? {});
|
|
393
|
-
}
|
|
394
|
-
//#endregion
|
|
395
|
-
//#region src/diagnostics.ts
|
|
278
|
+
//#region src/Diagnostics.ts
|
|
396
279
|
/**
|
|
397
280
|
* Docs major version, derived from the package version so the link tracks the published major.
|
|
398
281
|
*/
|
|
399
|
-
const docsMajor =
|
|
282
|
+
const docsMajor = version.split(".")[0] ?? "5";
|
|
400
283
|
/**
|
|
401
284
|
* Narrows a {@link Diagnostic} to the variant for `code`, or `null` when it does not match.
|
|
402
285
|
*
|
|
@@ -454,106 +337,106 @@ const isUpdate = isKind("update");
|
|
|
454
337
|
* blue info).
|
|
455
338
|
*/
|
|
456
339
|
const severityStyle = {
|
|
457
|
-
error:
|
|
458
|
-
warning:
|
|
459
|
-
info:
|
|
340
|
+
error: "red",
|
|
341
|
+
warning: "yellow",
|
|
342
|
+
info: "blue"
|
|
460
343
|
};
|
|
461
344
|
/**
|
|
462
345
|
* Explanation for every {@link diagnosticCode}. Use {@link Diagnostics.explain} to look one up
|
|
463
346
|
* and `Diagnostics.docsUrl` for the matching kubb.dev page.
|
|
464
347
|
*/
|
|
465
348
|
const diagnosticCatalog = {
|
|
466
|
-
[
|
|
349
|
+
[diagnosticCode.unknown]: {
|
|
467
350
|
title: "Unknown error",
|
|
468
351
|
cause: "An error was thrown without a stable Kubb code, so it is reported as-is.",
|
|
469
352
|
fix: "Read the underlying message and stack. If it comes from a plugin or adapter, check its configuration; otherwise report it as a possible Kubb bug."
|
|
470
353
|
},
|
|
471
|
-
[
|
|
354
|
+
[diagnosticCode.inputNotFound]: {
|
|
472
355
|
title: "Input not found",
|
|
473
356
|
cause: "The file or URL set in `input.path` (or passed as `kubb generate PATH`) could not be read.",
|
|
474
357
|
fix: "Check that the path or URL exists and is readable, then set it in `input.path` or pass it on the CLI."
|
|
475
358
|
},
|
|
476
|
-
[
|
|
359
|
+
[diagnosticCode.inputRequired]: {
|
|
477
360
|
title: "Input required",
|
|
478
361
|
cause: "An adapter is configured but no `input` was provided.",
|
|
479
362
|
fix: "Set `input.path` (a file or URL) or `input.data` (an inline spec) in your Kubb config."
|
|
480
363
|
},
|
|
481
|
-
[
|
|
364
|
+
[diagnosticCode.refNotFound]: {
|
|
482
365
|
title: "Reference not found",
|
|
483
366
|
cause: "A `$ref` could not be resolved in the source document.",
|
|
484
367
|
fix: "Add the missing definition (for example under `components.schemas`) or fix the `$ref`. Run `kubb validate` to check the spec."
|
|
485
368
|
},
|
|
486
|
-
[
|
|
369
|
+
[diagnosticCode.invalidServerVariable]: {
|
|
487
370
|
title: "Invalid server variable",
|
|
488
371
|
cause: "A server variable value is not allowed by its `enum`.",
|
|
489
372
|
fix: "Use one of the values listed in the server variable `enum`, or update the spec."
|
|
490
373
|
},
|
|
491
|
-
[
|
|
374
|
+
[diagnosticCode.pluginNotFound]: {
|
|
492
375
|
title: "Plugin not found",
|
|
493
376
|
cause: "A plugin that another plugin depends on is missing from the config.",
|
|
494
377
|
fix: "Add the required plugin to the `plugins` array in kubb.config.ts, or remove the dependency on it."
|
|
495
378
|
},
|
|
496
|
-
[
|
|
379
|
+
[diagnosticCode.pluginFailed]: {
|
|
497
380
|
title: "Plugin failed",
|
|
498
381
|
cause: "A plugin threw while generating, or reported an error through `ctx.error`.",
|
|
499
382
|
fix: "Read the underlying error and check the plugin options and the schema or operation it failed on."
|
|
500
383
|
},
|
|
501
|
-
[
|
|
384
|
+
[diagnosticCode.pluginWarning]: {
|
|
502
385
|
title: "Plugin warning",
|
|
503
386
|
cause: "A plugin reported a non-fatal warning through `ctx.warn`.",
|
|
504
387
|
fix: "Review the message. It does not fail the build; adjust the plugin options or input if the warning is unwanted."
|
|
505
388
|
},
|
|
506
|
-
[
|
|
389
|
+
[diagnosticCode.pluginInfo]: {
|
|
507
390
|
title: "Plugin info",
|
|
508
391
|
cause: "A plugin reported an informational message through `ctx.info`.",
|
|
509
392
|
fix: "Informational only. No action is required."
|
|
510
393
|
},
|
|
511
|
-
[
|
|
394
|
+
[diagnosticCode.unsupportedFormat]: {
|
|
512
395
|
title: "Unsupported format",
|
|
513
396
|
cause: "A schema uses a `format` Kubb does not map to a specific type, so it falls back to the base type.",
|
|
514
397
|
fix: "Use a format Kubb supports, or handle the custom format with a parser or plugin."
|
|
515
398
|
},
|
|
516
|
-
[
|
|
399
|
+
[diagnosticCode.deprecated]: {
|
|
517
400
|
title: "Deprecated",
|
|
518
401
|
cause: "A referenced schema or operation is marked `deprecated`.",
|
|
519
402
|
fix: "Migrate off the deprecated definition if the warning is unwanted."
|
|
520
403
|
},
|
|
521
|
-
[
|
|
404
|
+
[diagnosticCode.adapterRequired]: {
|
|
522
405
|
title: "Adapter required",
|
|
523
406
|
cause: "An action needs an adapter but none is configured.",
|
|
524
407
|
fix: "Set `adapter` in kubb.config.ts, for example `adapterOas()`."
|
|
525
408
|
},
|
|
526
|
-
[
|
|
409
|
+
[diagnosticCode.pathTraversal]: {
|
|
527
410
|
title: "Path traversal",
|
|
528
411
|
cause: "A resolved output path escaped the output directory, which can stem from a path traversal in the spec or a misconfigured `group.name`.",
|
|
529
412
|
fix: "Keep generated paths within the output directory. Review the `group.name` function and the names coming from the spec."
|
|
530
413
|
},
|
|
531
|
-
[
|
|
414
|
+
[diagnosticCode.invalidPluginOptions]: {
|
|
532
415
|
title: "Invalid plugin options",
|
|
533
416
|
cause: "A plugin was configured with options that cannot be honored, for example `output.mode: 'file'` paired with a `group` option.",
|
|
534
417
|
fix: "Fix the plugin options. A single-file output has nothing to group, so remove the `group` option or use `output.mode: 'directory'`."
|
|
535
418
|
},
|
|
536
|
-
[
|
|
419
|
+
[diagnosticCode.hookFailed]: {
|
|
537
420
|
title: "Hook failed",
|
|
538
421
|
cause: "A post-generate shell hook (`hooks.done`) exited with a non-zero status.",
|
|
539
422
|
fix: "Check the command is installed and correct, and run it manually to see the error."
|
|
540
423
|
},
|
|
541
|
-
[
|
|
424
|
+
[diagnosticCode.formatFailed]: {
|
|
542
425
|
title: "Format failed",
|
|
543
426
|
cause: "The formatter pass over the generated files failed.",
|
|
544
427
|
fix: "Check the formatter (oxfmt, biome, or prettier) is installed and its config is valid, then run it manually on the output."
|
|
545
428
|
},
|
|
546
|
-
[
|
|
429
|
+
[diagnosticCode.lintFailed]: {
|
|
547
430
|
title: "Lint failed",
|
|
548
431
|
cause: "The linter pass over the generated files failed.",
|
|
549
432
|
fix: "Check the linter (oxlint, biome, or eslint) is installed and its config is valid, then run it manually on the output."
|
|
550
433
|
},
|
|
551
|
-
[
|
|
434
|
+
[diagnosticCode.performance]: {
|
|
552
435
|
title: "Performance",
|
|
553
436
|
cause: "Not a failure. Records a plugin’s elapsed time, summed into the run total.",
|
|
554
437
|
fix: "No action. This is an informational metric."
|
|
555
438
|
},
|
|
556
|
-
[
|
|
439
|
+
[diagnosticCode.updateAvailable]: {
|
|
557
440
|
title: "Update available",
|
|
558
441
|
cause: "A newer Kubb version is published on npm than the one running.",
|
|
559
442
|
fix: "Update the `@kubb/*` packages, for example `npm install -g @kubb/cli`, to get the latest fixes."
|
|
@@ -565,15 +448,15 @@ const diagnosticCatalog = {
|
|
|
565
448
|
*
|
|
566
449
|
* The sink lives in a single `AsyncLocalStorage` in the `@kubb/core` bundle.
|
|
567
450
|
* `Diagnostics.scope` activates it for a run, so anything inside that run (the
|
|
568
|
-
* adapter parse, a
|
|
569
|
-
*
|
|
451
|
+
* adapter parse, a generator) reports through `Diagnostics.report` and lands
|
|
452
|
+
* in the same run.
|
|
570
453
|
*/
|
|
571
454
|
var Diagnostics = class Diagnostics {
|
|
572
455
|
static #reporterStorage = new node_async_hooks.AsyncLocalStorage();
|
|
573
456
|
/**
|
|
574
457
|
* The diagnostic code catalog, exposed as `Diagnostics.code` (e.g. `Diagnostics.code.refNotFound`).
|
|
575
458
|
*/
|
|
576
|
-
static code =
|
|
459
|
+
static code = diagnosticCode;
|
|
577
460
|
/**
|
|
578
461
|
* Type guard for a build {@link ProblemDiagnostic}.
|
|
579
462
|
*/
|
|
@@ -636,12 +519,12 @@ var Diagnostics = class Diagnostics {
|
|
|
636
519
|
return true;
|
|
637
520
|
}
|
|
638
521
|
/**
|
|
639
|
-
* Emits a diagnostic on the run's `kubb:diagnostic`
|
|
640
|
-
* Use it instead of calling `hooks.
|
|
522
|
+
* Emits a diagnostic on the run's `kubb:diagnostic` hook so the loggers render it live.
|
|
523
|
+
* Use it instead of calling `hooks.callHook('kubb:diagnostic', ...)` directly. To collect a
|
|
641
524
|
* diagnostic into the build result from deep in a run, use {@link Diagnostics.report} instead.
|
|
642
525
|
*/
|
|
643
526
|
static async emit(hooks, diagnostic) {
|
|
644
|
-
await hooks.
|
|
527
|
+
await hooks.callHook("kubb:diagnostic", { diagnostic });
|
|
645
528
|
}
|
|
646
529
|
/**
|
|
647
530
|
* Coerces any thrown value into a {@link ProblemDiagnostic}. A {@link Diagnostics.Error}
|
|
@@ -658,9 +541,9 @@ var Diagnostics = class Diagnostics {
|
|
|
658
541
|
current = current.cause;
|
|
659
542
|
}
|
|
660
543
|
return {
|
|
661
|
-
code:
|
|
544
|
+
code: diagnosticCode.unknown,
|
|
662
545
|
severity: "error",
|
|
663
|
-
message: root ? root.message :
|
|
546
|
+
message: root ? root.message : require_usingCtx.getErrorMessage(error),
|
|
664
547
|
cause: root
|
|
665
548
|
};
|
|
666
549
|
}
|
|
@@ -670,7 +553,7 @@ var Diagnostics = class Diagnostics {
|
|
|
670
553
|
static performance({ plugin, duration }) {
|
|
671
554
|
return {
|
|
672
555
|
kind: "performance",
|
|
673
|
-
code:
|
|
556
|
+
code: diagnosticCode.performance,
|
|
674
557
|
severity: "info",
|
|
675
558
|
message: `${plugin} generated in ${Math.round(duration)}ms`,
|
|
676
559
|
plugin,
|
|
@@ -683,7 +566,7 @@ var Diagnostics = class Diagnostics {
|
|
|
683
566
|
static update({ currentVersion, latestVersion }) {
|
|
684
567
|
return {
|
|
685
568
|
kind: "update",
|
|
686
|
-
code:
|
|
569
|
+
code: diagnosticCode.updateAvailable,
|
|
687
570
|
severity: "info",
|
|
688
571
|
message: `Update available: v${currentVersion} → v${latestVersion}. Run \`npm install -g @kubb/cli\` to update.`,
|
|
689
572
|
currentVersion,
|
|
@@ -752,7 +635,8 @@ var Diagnostics = class Diagnostics {
|
|
|
752
635
|
* `KUBB_REF_NOT_FOUND` → `https://kubb.dev/docs/5.x/reference/diagnostics/kubb-ref-not-found`.
|
|
753
636
|
*/
|
|
754
637
|
static docsUrl(code) {
|
|
755
|
-
|
|
638
|
+
const slug = code.toLowerCase().replaceAll("_", "-");
|
|
639
|
+
return `https://kubb.dev/docs/${docsMajor}.x/reference/diagnostics/${slug}`;
|
|
756
640
|
}
|
|
757
641
|
/**
|
|
758
642
|
* The catalog entry for a code: its title, cause, and fix. Mirrors the kubb.dev
|
|
@@ -775,7 +659,7 @@ var Diagnostics = class Diagnostics {
|
|
|
775
659
|
...problem?.location ? { location: problem.location } : {},
|
|
776
660
|
...problem?.help ? { help: problem.help } : {},
|
|
777
661
|
...problem?.plugin ? { plugin: problem.plugin } : {},
|
|
778
|
-
...diagnostic.code ===
|
|
662
|
+
...diagnostic.code === diagnosticCode.unknown ? {} : { docsUrl: Diagnostics.docsUrl(diagnostic.code) }
|
|
779
663
|
};
|
|
780
664
|
}
|
|
781
665
|
/**
|
|
@@ -788,14 +672,14 @@ var Diagnostics = class Diagnostics {
|
|
|
788
672
|
*/
|
|
789
673
|
static format(diagnostic) {
|
|
790
674
|
const { code, severity, message } = diagnostic;
|
|
791
|
-
const
|
|
675
|
+
const color = severityStyle[severity];
|
|
792
676
|
const problem = isProblem(diagnostic) ? diagnostic : void 0;
|
|
793
677
|
const tag = (0, node_util.styleText)(color, (0, node_util.styleText)("bold", `[${code}]`));
|
|
794
678
|
const headline = problem?.plugin ? `${tag} ${problem.plugin}: ${message}` : `${tag}: ${message}`;
|
|
795
679
|
const details = [];
|
|
796
680
|
if (problem?.location && "pointer" in problem.location) details.push(` ${(0, node_util.styleText)("dim", "at:")} ${(0, node_util.styleText)("cyan", problem.location.pointer)}`);
|
|
797
681
|
if (problem?.help) details.push(` ${(0, node_util.styleText)("cyan", "fix:")} ${problem.help}`);
|
|
798
|
-
if (code !==
|
|
682
|
+
if (code !== diagnosticCode.unknown) details.push(` ${(0, node_util.styleText)("dim", "see:")} ${(0, node_util.styleText)("cyan", Diagnostics.docsUrl(code))}`);
|
|
799
683
|
return {
|
|
800
684
|
headline,
|
|
801
685
|
details
|
|
@@ -820,7 +704,7 @@ var Diagnostics = class Diagnostics {
|
|
|
820
704
|
function normalizeOutput({ output, group, pluginName }) {
|
|
821
705
|
const mode = output.mode ?? "directory";
|
|
822
706
|
if (mode === "file" && group) throw new Diagnostics.Error({
|
|
823
|
-
code:
|
|
707
|
+
code: diagnosticCode.invalidPluginOptions,
|
|
824
708
|
severity: "error",
|
|
825
709
|
message: `Plugin "${pluginName}" sets \`output.mode: 'file'\` but also configures a \`group\` option.`,
|
|
826
710
|
help: "A single-file output has nothing to group. Remove the `group` option, or use `output.mode: 'directory'` to organize files into subdirectories.",
|
|
@@ -858,412 +742,286 @@ function definePlugin(factory) {
|
|
|
858
742
|
return (options) => factory(options ?? {});
|
|
859
743
|
}
|
|
860
744
|
//#endregion
|
|
861
|
-
//#region src/
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
* `banner`/`footer` function. Missing fields default to empty/`false` so the object shape
|
|
865
|
-
* is stable even when a caller (e.g. the barrel plugin) has no document metadata.
|
|
866
|
-
*/
|
|
867
|
-
function buildBannerMeta({ meta, file }) {
|
|
868
|
-
return {
|
|
869
|
-
title: meta?.title,
|
|
870
|
-
description: meta?.description,
|
|
871
|
-
version: meta?.version,
|
|
872
|
-
baseURL: meta?.baseURL,
|
|
873
|
-
circularNames: meta?.circularNames ?? [],
|
|
874
|
-
enumNames: meta?.enumNames ?? [],
|
|
875
|
-
filePath: file?.path ?? "",
|
|
876
|
-
baseName: file?.baseName ?? "",
|
|
877
|
-
isBarrel: file?.isBarrel ?? false,
|
|
878
|
-
isAggregation: file?.isAggregation ?? false
|
|
879
|
-
};
|
|
880
|
-
}
|
|
881
|
-
const stringPatternCache = /* @__PURE__ */ new Map();
|
|
882
|
-
function testPattern(value, pattern) {
|
|
883
|
-
if (typeof pattern === "string") {
|
|
884
|
-
let regex = stringPatternCache.get(pattern);
|
|
885
|
-
if (!regex) {
|
|
886
|
-
regex = new RegExp(pattern);
|
|
887
|
-
stringPatternCache.set(pattern, regex);
|
|
888
|
-
}
|
|
889
|
-
return regex.test(value);
|
|
890
|
-
}
|
|
891
|
-
return value.match(pattern) !== null;
|
|
745
|
+
//#region src/Resolver.ts
|
|
746
|
+
function isNamespace(value) {
|
|
747
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
892
748
|
}
|
|
893
749
|
/**
|
|
894
|
-
*
|
|
895
|
-
*/
|
|
896
|
-
function matchesOperationPattern(node, type, pattern) {
|
|
897
|
-
if (type === "tag") return node.tags.some((tag) => testPattern(tag, pattern));
|
|
898
|
-
if (type === "operationId") return testPattern(node.operationId, pattern);
|
|
899
|
-
if (type === "path") return node.path !== void 0 && testPattern(node.path, pattern);
|
|
900
|
-
if (type === "method") return node.method !== void 0 && testPattern(node.method.toLowerCase(), pattern);
|
|
901
|
-
if (type === "contentType") return node.requestBody?.content?.some((c) => testPattern(c.contentType, pattern)) ?? false;
|
|
902
|
-
return false;
|
|
903
|
-
}
|
|
904
|
-
/**
|
|
905
|
-
* Checks if a schema matches a pattern for a given filter type (`schemaName`).
|
|
906
|
-
*
|
|
907
|
-
* Returns `null` when the filter type doesn't apply to schemas.
|
|
908
|
-
*/
|
|
909
|
-
function matchesSchemaPattern(node, type, pattern) {
|
|
910
|
-
if (type === "schemaName") return node.name ? testPattern(node.name, pattern) : false;
|
|
911
|
-
return null;
|
|
912
|
-
}
|
|
913
|
-
/**
|
|
914
|
-
* Default name resolver used by `defineResolver`.
|
|
915
|
-
*
|
|
916
|
-
* - `camelCase` for `file`, with dotted names split into `/`-joined nested paths.
|
|
917
|
-
* - `PascalCase` for `type`.
|
|
918
|
-
* - `camelCase` for `function` and everything else.
|
|
919
|
-
*/
|
|
920
|
-
function defaultResolver(name, type) {
|
|
921
|
-
if (type === "file") return require_memoryStorage.toFilePath(name);
|
|
922
|
-
if (type === "type") return require_memoryStorage.pascalCase(name);
|
|
923
|
-
return require_memoryStorage.camelCase(name);
|
|
924
|
-
}
|
|
925
|
-
/**
|
|
926
|
-
* Default option resolver. Applies include/exclude filters and merges matching override options.
|
|
750
|
+
* Base constraint for all plugin resolver objects.
|
|
927
751
|
*
|
|
928
|
-
*
|
|
752
|
+
* The built-in machinery lives under `default`. Generators call the top-level `name` and
|
|
753
|
+
* `file`, and a plugin overrides them to set its conventions. Extend with top-level helpers
|
|
754
|
+
* (`typeName`, …) and/or grouped namespaces (`query`, `schema`, …).
|
|
929
755
|
*
|
|
930
|
-
* @example
|
|
756
|
+
* @example Top-level helper
|
|
931
757
|
* ```ts
|
|
932
|
-
*
|
|
933
|
-
*
|
|
934
|
-
*
|
|
935
|
-
* })
|
|
936
|
-
* // → null when node has tag 'internal'
|
|
758
|
+
* type MyResolver = Resolver & {
|
|
759
|
+
* typeName(name: string): string
|
|
760
|
+
* }
|
|
937
761
|
* ```
|
|
938
762
|
*
|
|
939
|
-
* @example
|
|
763
|
+
* @example Grouped namespace
|
|
940
764
|
* ```ts
|
|
941
|
-
*
|
|
942
|
-
*
|
|
943
|
-
*
|
|
944
|
-
*
|
|
945
|
-
*
|
|
765
|
+
* type MyResolver = Resolver & {
|
|
766
|
+
* query: {
|
|
767
|
+
* name(node: OperationNode): string
|
|
768
|
+
* keyName(node: OperationNode): string
|
|
769
|
+
* }
|
|
770
|
+
* }
|
|
946
771
|
* ```
|
|
947
772
|
*/
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
773
|
+
var Resolver = class Resolver {
|
|
774
|
+
static #patternCache = /* @__PURE__ */ new Map();
|
|
775
|
+
static #optionsCache = /* @__PURE__ */ new WeakMap();
|
|
776
|
+
pluginName;
|
|
777
|
+
#options;
|
|
778
|
+
constructor(options) {
|
|
779
|
+
this.pluginName = options.pluginName;
|
|
780
|
+
this.#options = options;
|
|
781
|
+
this.#apply(options);
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* The built-in resolution machinery. Always reaches the untouched defaults, even when a
|
|
785
|
+
* plugin overrides the top-level `name` or `file`.
|
|
786
|
+
*/
|
|
787
|
+
get default() {
|
|
954
788
|
return {
|
|
955
|
-
|
|
956
|
-
|
|
789
|
+
name: require_usingCtx.camelCase,
|
|
790
|
+
options: this.#resolveOptions.bind(this),
|
|
791
|
+
path: this.#resolvePath.bind(this),
|
|
792
|
+
file: this.#resolveFile.bind(this),
|
|
793
|
+
banner: this.#resolveBanner.bind(this),
|
|
794
|
+
footer: this.#resolveFooter.bind(this)
|
|
957
795
|
};
|
|
958
796
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
797
|
+
name(name) {
|
|
798
|
+
return this.default.name(name);
|
|
799
|
+
}
|
|
800
|
+
file(params, context) {
|
|
801
|
+
return this.default.file(params, context);
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Merges `override` over `base` and returns a new resolver with helpers re-bound.
|
|
805
|
+
* Each key is replaced wholesale. Used when applying `setResolver` partial overrides.
|
|
806
|
+
*/
|
|
807
|
+
static merge(base, override) {
|
|
808
|
+
const patch = override instanceof Resolver ? override.#options : override;
|
|
809
|
+
return new Resolver({
|
|
810
|
+
...base.#options,
|
|
811
|
+
...patch
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Binds each entry of `options` onto the resolver, so `this.name`, `this.default`, and
|
|
816
|
+
* `this.file` resolve there for top-level helpers and namespace methods alike. `default`
|
|
817
|
+
* is skipped so it can't be shadowed.
|
|
818
|
+
*/
|
|
819
|
+
#apply(options) {
|
|
820
|
+
const root = this;
|
|
821
|
+
const bind = (value) => typeof value === "function" ? value.bind(root) : value;
|
|
822
|
+
for (const [key, value] of Object.entries(options)) {
|
|
823
|
+
if (key === "pluginName" || key === "default" || value === void 0) continue;
|
|
824
|
+
root[key] = isNamespace(value) ? Object.fromEntries(Object.entries(value).map(([method, member]) => [method, bind(member)])) : bind(value);
|
|
964
825
|
}
|
|
965
|
-
const overrideOptions = override.find(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)?.options;
|
|
966
|
-
return {
|
|
967
|
-
...options,
|
|
968
|
-
...overrideOptions
|
|
969
|
-
};
|
|
970
826
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
*
|
|
990
|
-
*
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
* @example Tag-based grouping
|
|
1004
|
-
* ```ts
|
|
1005
|
-
* defaultResolvePath(
|
|
1006
|
-
* { baseName: 'petTypes.ts', tag: 'pets' },
|
|
1007
|
-
* { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
|
|
1008
|
-
* )
|
|
1009
|
-
* // → '/src/types/pets/petTypes.ts'
|
|
1010
|
-
* ```
|
|
1011
|
-
*
|
|
1012
|
-
* @example Path-based grouping
|
|
1013
|
-
* ```ts
|
|
1014
|
-
* defaultResolvePath(
|
|
1015
|
-
* { baseName: 'petTypes.ts', path: '/pets/list' },
|
|
1016
|
-
* { root: '/src', output: { path: 'types' }, group: { type: 'path' } },
|
|
1017
|
-
* )
|
|
1018
|
-
* // → '/src/types/pets/petTypes.ts'
|
|
1019
|
-
* ```
|
|
1020
|
-
*
|
|
1021
|
-
* @example Single file (`mode: 'file'`)
|
|
1022
|
-
* ```ts
|
|
1023
|
-
* defaultResolvePath(
|
|
1024
|
-
* { baseName: 'petTypes.ts' },
|
|
1025
|
-
* { root: '/src', output: { path: 'types.ts', mode: 'file' } },
|
|
1026
|
-
* )
|
|
1027
|
-
* // → '/src/types.ts'
|
|
1028
|
-
* ```
|
|
1029
|
-
*/
|
|
1030
|
-
function defaultResolvePath({ baseName, tag, path: groupPath }, { root, output, group }) {
|
|
1031
|
-
if ((output.mode ?? "directory") === "file") return node_path.default.resolve(root, output.path);
|
|
1032
|
-
const result = (() => {
|
|
1033
|
-
if (group && (groupPath || tag)) {
|
|
1034
|
-
const groupValue = group.type === "path" ? groupPath : tag;
|
|
1035
|
-
const defaultName = group.type === "tag" ? ({ group: groupName }) => require_memoryStorage.camelCase(groupName) : ({ group: groupName }) => {
|
|
1036
|
-
const segment = groupName.split("/").filter((part) => part !== "" && part !== "." && part !== "..")[0];
|
|
1037
|
-
return segment ? require_memoryStorage.camelCase(segment) : "";
|
|
827
|
+
static #testPattern(value, pattern) {
|
|
828
|
+
if (typeof pattern === "string") {
|
|
829
|
+
let regex = Resolver.#patternCache.get(pattern);
|
|
830
|
+
regex ??= new RegExp(pattern);
|
|
831
|
+
Resolver.#patternCache.set(pattern, regex);
|
|
832
|
+
return regex.test(value);
|
|
833
|
+
}
|
|
834
|
+
return value.match(pattern) !== null;
|
|
835
|
+
}
|
|
836
|
+
static #matchesOperation(node, { type, pattern }) {
|
|
837
|
+
if (type === "tag") return node.tags.some((tag) => Resolver.#testPattern(tag, pattern));
|
|
838
|
+
if (type === "operationId") return Resolver.#testPattern(node.operationId, pattern);
|
|
839
|
+
if (type === "path") return node.path !== void 0 && Resolver.#testPattern(node.path, pattern);
|
|
840
|
+
if (type === "method") return node.method !== void 0 && Resolver.#testPattern(node.method.toLowerCase(), pattern);
|
|
841
|
+
if (type === "contentType") return node.requestBody?.content?.some((c) => Resolver.#testPattern(c.contentType, pattern)) ?? false;
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Returns `null` when the filter type doesn't apply to schemas, so include rules built
|
|
846
|
+
* from operation filters (e.g. `tag`) don't exclude every schema.
|
|
847
|
+
*/
|
|
848
|
+
static #matchesSchema(node, { type, pattern }) {
|
|
849
|
+
if (type === "schemaName") return node.name ? Resolver.#testPattern(node.name, pattern) : false;
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
static #computeOptions(node, { options, exclude = [], include, override = [] }) {
|
|
853
|
+
if (_kubb_ast.operationDef.is(node)) {
|
|
854
|
+
if (exclude.some((filter) => Resolver.#matchesOperation(node, filter))) return null;
|
|
855
|
+
if (include && !include.some((filter) => Resolver.#matchesOperation(node, filter))) return null;
|
|
856
|
+
return {
|
|
857
|
+
...options,
|
|
858
|
+
...override.find((filter) => Resolver.#matchesOperation(node, filter))?.options
|
|
1038
859
|
};
|
|
1039
|
-
const groupName = (group.name ?? defaultName)({ group: groupValue });
|
|
1040
|
-
return node_path.default.resolve(root, output.path, groupName, baseName);
|
|
1041
860
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
code: Diagnostics.code.pathTraversal,
|
|
1048
|
-
severity: "error",
|
|
1049
|
-
message: `Resolved path "${result}" is outside the output directory "${outputDir}".`,
|
|
1050
|
-
help: "This can stem from a path traversal in the OpenAPI specification or a misconfigured `group.name` function. Keep generated paths within the output directory.",
|
|
1051
|
-
location: { kind: "config" }
|
|
1052
|
-
});
|
|
1053
|
-
return result;
|
|
1054
|
-
}
|
|
1055
|
-
/**
|
|
1056
|
-
* Default file resolver used by `defineResolver`.
|
|
1057
|
-
*
|
|
1058
|
-
* Resolves a `FileNode` by combining name resolution (`resolver.default`) with
|
|
1059
|
-
* path resolution (`resolver.resolvePath`). The resolved file always has empty
|
|
1060
|
-
* `sources`, `imports`, and `exports` arrays, which consumers populate separately.
|
|
1061
|
-
*
|
|
1062
|
-
* In `mode: 'file'` the name is omitted and the file sits directly at the output path.
|
|
1063
|
-
*
|
|
1064
|
-
* @example Resolve a schema file
|
|
1065
|
-
* ```ts
|
|
1066
|
-
* const file = defaultResolveFile.call(
|
|
1067
|
-
* resolver,
|
|
1068
|
-
* { name: 'pet', extname: '.ts' },
|
|
1069
|
-
* { root: '/src', output: { path: 'types' } },
|
|
1070
|
-
* )
|
|
1071
|
-
* // → { baseName: 'pet.ts', path: '/src/types/pet.ts', sources: [], ... }
|
|
1072
|
-
* ```
|
|
1073
|
-
*
|
|
1074
|
-
* @example Resolve an operation file with tag grouping
|
|
1075
|
-
* ```ts
|
|
1076
|
-
* const file = defaultResolveFile.call(
|
|
1077
|
-
* resolver,
|
|
1078
|
-
* { name: 'listPets', extname: '.ts', tag: 'pets' },
|
|
1079
|
-
* { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
|
|
1080
|
-
* )
|
|
1081
|
-
* // → { baseName: 'listPets.ts', path: '/src/types/pets/listPets.ts', ... }
|
|
1082
|
-
* ```
|
|
1083
|
-
*/
|
|
1084
|
-
function defaultResolveFile({ name, extname, tag, path: groupPath }, context) {
|
|
1085
|
-
const baseName = `${(context.output.mode ?? "directory") === "file" ? "" : this.default(name, "file")}${extname}`;
|
|
1086
|
-
const filePath = this.resolvePath({
|
|
1087
|
-
baseName,
|
|
1088
|
-
tag,
|
|
1089
|
-
path: groupPath
|
|
1090
|
-
}, context);
|
|
1091
|
-
return _kubb_ast.ast.factory.createFile({
|
|
1092
|
-
path: filePath,
|
|
1093
|
-
baseName: node_path.default.basename(filePath),
|
|
1094
|
-
meta: { pluginName: this.pluginName },
|
|
1095
|
-
sources: [],
|
|
1096
|
-
imports: [],
|
|
1097
|
-
exports: []
|
|
1098
|
-
});
|
|
1099
|
-
}
|
|
1100
|
-
/**
|
|
1101
|
-
* Generates the default "Generated by Kubb" banner from config and optional node metadata.
|
|
1102
|
-
*/
|
|
1103
|
-
function buildDefaultBanner({ title, description, version, config }) {
|
|
1104
|
-
try {
|
|
1105
|
-
const source = (() => {
|
|
1106
|
-
if (Array.isArray(config.input)) {
|
|
1107
|
-
const first = config.input[0];
|
|
1108
|
-
if (first && "path" in first) return node_path.default.basename(first.path);
|
|
1109
|
-
return "";
|
|
861
|
+
if (_kubb_ast.schemaDef.is(node)) {
|
|
862
|
+
if (exclude.some((filter) => Resolver.#matchesSchema(node, filter) === true)) return null;
|
|
863
|
+
if (include) {
|
|
864
|
+
const applicable = include.map((filter) => Resolver.#matchesSchema(node, filter)).filter((result) => result !== null);
|
|
865
|
+
if (applicable.length > 0 && !applicable.includes(true)) return null;
|
|
1110
866
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
let banner = "/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n";
|
|
1116
|
-
if (config.output.defaultBanner === "simple") {
|
|
1117
|
-
banner += "*/\n";
|
|
1118
|
-
return banner;
|
|
867
|
+
return {
|
|
868
|
+
...options,
|
|
869
|
+
...override.find((filter) => Resolver.#matchesSchema(node, filter) === true)?.options
|
|
870
|
+
};
|
|
1119
871
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
872
|
+
return options;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Applies include/exclude filters and merges matching override options, caching the result
|
|
876
|
+
* per `(options, node)` pair. Returns `null` when the node is filtered out.
|
|
877
|
+
*/
|
|
878
|
+
#resolveOptions(node, context) {
|
|
879
|
+
const { options } = context;
|
|
880
|
+
if (typeof options !== "object" || options === null) return Resolver.#computeOptions(node, context);
|
|
881
|
+
let byOptions = Resolver.#optionsCache.get(options);
|
|
882
|
+
if (!byOptions) {
|
|
883
|
+
byOptions = /* @__PURE__ */ new WeakMap();
|
|
884
|
+
Resolver.#optionsCache.set(options, byOptions);
|
|
1125
885
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
return
|
|
886
|
+
const cached = byOptions.get(node);
|
|
887
|
+
if (cached) return cached.value;
|
|
888
|
+
const result = Resolver.#computeOptions(node, context);
|
|
889
|
+
byOptions.set(node, { value: result });
|
|
890
|
+
return result;
|
|
1131
891
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
*
|
|
1135
|
-
*
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
*
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
*
|
|
1164
|
-
*
|
|
1165
|
-
*
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
*
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
892
|
+
/**
|
|
893
|
+
* A custom `group.name` wins; otherwise `tag` groups use the camelCased tag and `path`
|
|
894
|
+
* groups use the first non-traversal segment (`''` when none remain, placing the file in
|
|
895
|
+
* the output root, kept safe by the caller's boundary check).
|
|
896
|
+
*/
|
|
897
|
+
static #resolveGroupDir(group, groupValue) {
|
|
898
|
+
if (group.name) return group.name({ group: groupValue });
|
|
899
|
+
if (group.type === "tag") return require_usingCtx.camelCase(groupValue);
|
|
900
|
+
const segment = groupValue.split("/").filter((part) => part !== "" && part !== "." && part !== "..")[0];
|
|
901
|
+
return segment ? require_usingCtx.camelCase(segment) : "";
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* `mode: 'file'` resolves directly to `output.path`. `mode: 'directory'` (default) resolves
|
|
905
|
+
* to `output.path/{baseName}`, or into a subdirectory when `group` and a `tag`/`path` value
|
|
906
|
+
* are provided.
|
|
907
|
+
*/
|
|
908
|
+
#resolvePath({ baseName, tag, path: groupPath }, { root, output, group }) {
|
|
909
|
+
if (output.mode === "file") return node_path.default.resolve(root, output.path);
|
|
910
|
+
const outputDir = node_path.default.resolve(root, output.path);
|
|
911
|
+
const result = group && (groupPath || tag) ? node_path.default.resolve(outputDir, Resolver.#resolveGroupDir(group, group.type === "path" ? groupPath : tag), baseName) : node_path.default.resolve(outputDir, baseName);
|
|
912
|
+
const outputDirWithSep = outputDir.endsWith(node_path.default.sep) ? outputDir : `${outputDir}${node_path.default.sep}`;
|
|
913
|
+
if (result !== outputDir && !result.startsWith(outputDirWithSep)) throw new Diagnostics.Error({
|
|
914
|
+
code: Diagnostics.code.pathTraversal,
|
|
915
|
+
severity: "error",
|
|
916
|
+
message: `Resolved path "${result}" is outside the output directory "${outputDir}".`,
|
|
917
|
+
help: "This can stem from a path traversal in the OpenAPI specification or a misconfigured `group.name` function. Keep generated paths within the output directory.",
|
|
918
|
+
location: { kind: "config" }
|
|
919
|
+
});
|
|
920
|
+
return result;
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Builds a `FileNode` by combining file-name casing (`params.resolveName`) with path
|
|
924
|
+
* resolution. The resolved file starts with empty `sources`, `imports`, and `exports`,
|
|
925
|
+
* which consumers populate separately.
|
|
926
|
+
*/
|
|
927
|
+
#resolveFile({ name, extname, tag, path: groupPath, resolveName = require_usingCtx.toFilePath }, context) {
|
|
928
|
+
const resolvedName = context.output.mode === "file" ? "" : resolveName(name);
|
|
929
|
+
const filePath = this.#resolvePath({
|
|
930
|
+
baseName: `${resolvedName}${extname}`,
|
|
931
|
+
tag,
|
|
932
|
+
path: groupPath
|
|
933
|
+
}, context);
|
|
934
|
+
return _kubb_ast.ast.factory.createFile({
|
|
935
|
+
path: filePath,
|
|
936
|
+
baseName: node_path.default.basename(filePath),
|
|
937
|
+
meta: { pluginName: this.pluginName },
|
|
938
|
+
sources: [],
|
|
939
|
+
imports: [],
|
|
940
|
+
exports: []
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Missing fields default to empty/`false` so the `BannerMeta` shape stays stable even when
|
|
945
|
+
* a caller (e.g. the barrel plugin) has no document metadata.
|
|
946
|
+
*/
|
|
947
|
+
static #buildBannerMeta(meta, file) {
|
|
948
|
+
return {
|
|
949
|
+
title: meta?.title,
|
|
950
|
+
description: meta?.description,
|
|
951
|
+
version: meta?.version,
|
|
952
|
+
baseURL: meta?.baseURL,
|
|
953
|
+
circularNames: meta?.circularNames ?? [],
|
|
954
|
+
enumNames: meta?.enumNames ?? [],
|
|
955
|
+
filePath: file?.path ?? "",
|
|
956
|
+
baseName: file?.baseName ?? "",
|
|
957
|
+
isBarrel: file?.isBarrel ?? false,
|
|
958
|
+
isAggregation: file?.isAggregation ?? false
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Resolves a user-configured banner/footer value. `undefined` means not configured.
|
|
963
|
+
*/
|
|
964
|
+
static #resolveUserText(value, meta, file) {
|
|
965
|
+
if (typeof value === "function") return value(Resolver.#buildBannerMeta(meta, file));
|
|
966
|
+
if (typeof value === "string") return value;
|
|
967
|
+
}
|
|
968
|
+
static #buildDefaultBanner({ title, version, config }) {
|
|
969
|
+
const lines = [
|
|
970
|
+
"/**",
|
|
971
|
+
"* Generated by Kubb (https://kubb.dev/).",
|
|
972
|
+
"* Do not edit manually."
|
|
973
|
+
];
|
|
974
|
+
if (config.output.defaultBanner !== "simple") {
|
|
975
|
+
const input = Array.isArray(config.input) ? config.input[0] : config.input;
|
|
976
|
+
const source = input && "path" in input ? node_path.default.basename(input.path) : input && "data" in input ? "text content" : "";
|
|
977
|
+
if (source) lines.push(`* Source: ${source}`);
|
|
978
|
+
if (title) lines.push(`* Title: ${title}`);
|
|
979
|
+
if (version) lines.push(`* OpenAPI spec version: ${version}`);
|
|
980
|
+
}
|
|
981
|
+
return `${lines.join("\n")}\n*/\n`;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* A user-supplied `output.banner` overrides the default Kubb notice. When
|
|
985
|
+
* `config.output.defaultBanner` is `false` and no user banner is set, returns `null`.
|
|
986
|
+
*/
|
|
987
|
+
#resolveBanner(meta, { output, config, file }) {
|
|
988
|
+
const userBanner = Resolver.#resolveUserText(output?.banner, meta, file);
|
|
989
|
+
if (userBanner !== void 0) return userBanner;
|
|
990
|
+
if (config.output.defaultBanner === false) return null;
|
|
991
|
+
return Resolver.#buildDefaultBanner({
|
|
992
|
+
title: meta?.title,
|
|
993
|
+
version: meta?.version,
|
|
994
|
+
config
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
#resolveFooter(meta, { output, file }) {
|
|
998
|
+
return Resolver.#resolveUserText(output?.footer, meta, file) ?? null;
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
//#endregion
|
|
1002
|
+
//#region src/createResolver.ts
|
|
1215
1003
|
/**
|
|
1216
|
-
* Defines a plugin resolver
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
1219
|
-
*
|
|
1220
|
-
*
|
|
1221
|
-
* - `default` sets the name casing strategy (camelCase or PascalCase).
|
|
1222
|
-
* - `resolveOptions` does include/exclude/override filtering.
|
|
1223
|
-
* - `resolvePath` computes the output path.
|
|
1224
|
-
* - `resolveFile` builds the full `FileNode`.
|
|
1225
|
-
* - `resolveBanner` and `resolveFooter` produce the top and bottom of file text.
|
|
1226
|
-
*
|
|
1227
|
-
* Methods in the returned object can call sibling resolver methods via `this`.
|
|
1228
|
-
* A custom rule can delegate to a default, for example `this.default(name, 'type')`.
|
|
1004
|
+
* Defines a plugin resolver, the object that decides what every generated symbol and file
|
|
1005
|
+
* path is called. Override the top-level `name` and `file` to set the plugin's conventions,
|
|
1006
|
+
* and add your own naming helpers, top-level (`typeName`, …) or grouped in namespaces
|
|
1007
|
+
* (`query`, `schema`, …). Every method reaches sibling helpers and the built-in machinery
|
|
1008
|
+
* through `this.name`, `this.file`, and `this.default`.
|
|
1229
1009
|
*
|
|
1230
|
-
* @example
|
|
1010
|
+
* @example Custom identifier and file casing
|
|
1231
1011
|
* ```ts
|
|
1232
|
-
* export const resolverTs =
|
|
1233
|
-
*
|
|
1234
|
-
*
|
|
1235
|
-
* return
|
|
1012
|
+
* export const resolverTs = createResolver<PluginTs>({
|
|
1013
|
+
* pluginName: 'plugin-ts',
|
|
1014
|
+
* name(name) {
|
|
1015
|
+
* return ensureValidVarName(pascalCase(name))
|
|
1236
1016
|
* },
|
|
1237
|
-
*
|
|
1238
|
-
* return this.default(name,
|
|
1017
|
+
* file(params, context) {
|
|
1018
|
+
* return this.default.file({ ...params, resolveName: (name) => toFilePath(name, pascalCase) }, context)
|
|
1239
1019
|
* },
|
|
1240
|
-
* })
|
|
1241
|
-
* ```
|
|
1242
|
-
*
|
|
1243
|
-
* @example Custom output path
|
|
1244
|
-
* ```ts
|
|
1245
|
-
* import path from 'node:path'
|
|
1246
|
-
*
|
|
1247
|
-
* export const resolverTs = defineResolver<PluginTs>(() => ({
|
|
1248
|
-
* name: 'custom',
|
|
1249
|
-
* resolvePath({ baseName }, { root, output }) {
|
|
1250
|
-
* return path.resolve(root, output.path, 'generated', baseName)
|
|
1251
|
-
* },
|
|
1252
|
-
* }))
|
|
1020
|
+
* })
|
|
1253
1021
|
* ```
|
|
1254
1022
|
*/
|
|
1255
|
-
function
|
|
1256
|
-
|
|
1257
|
-
resolver = {
|
|
1258
|
-
default: defaultResolver,
|
|
1259
|
-
resolveOptions: defaultResolveOptions,
|
|
1260
|
-
resolvePath: defaultResolvePath,
|
|
1261
|
-
resolveFile: (params, context) => defaultResolveFile.call(resolver, params, context),
|
|
1262
|
-
resolveBanner: defaultResolveBanner,
|
|
1263
|
-
resolveFooter: defaultResolveFooter,
|
|
1264
|
-
...build()
|
|
1265
|
-
};
|
|
1266
|
-
return resolver;
|
|
1023
|
+
function createResolver(options) {
|
|
1024
|
+
return new Resolver(options);
|
|
1267
1025
|
}
|
|
1268
1026
|
//#endregion
|
|
1269
1027
|
//#region src/Transform.ts
|
|
@@ -1284,12 +1042,6 @@ var Transform = class {
|
|
|
1284
1042
|
#composed = /* @__PURE__ */ new Map();
|
|
1285
1043
|
#memo = /* @__PURE__ */ new Map();
|
|
1286
1044
|
/**
|
|
1287
|
-
* Number of plugins with at least one registered macro.
|
|
1288
|
-
*/
|
|
1289
|
-
get size() {
|
|
1290
|
-
return this.#macros.size;
|
|
1291
|
-
}
|
|
1292
|
-
/**
|
|
1293
1045
|
* Appends `macro` to the plugin's list, after any macros already registered.
|
|
1294
1046
|
*/
|
|
1295
1047
|
add(pluginName, macro) {
|
|
@@ -1306,12 +1058,6 @@ var Transform = class {
|
|
|
1306
1058
|
this.#invalidate(pluginName);
|
|
1307
1059
|
}
|
|
1308
1060
|
/**
|
|
1309
|
-
* Looks up the composed visitor for `pluginName`, or `undefined` when the plugin has no macros.
|
|
1310
|
-
*/
|
|
1311
|
-
get(pluginName) {
|
|
1312
|
-
return this.#visitorFor(pluginName);
|
|
1313
|
-
}
|
|
1314
|
-
/**
|
|
1315
1061
|
* Runs the plugin's macros on `node`. Returns the original node reference when the plugin has no
|
|
1316
1062
|
* macros, so callers can compare by identity to detect a no-op.
|
|
1317
1063
|
*/
|
|
@@ -1355,20 +1101,21 @@ var Transform = class {
|
|
|
1355
1101
|
};
|
|
1356
1102
|
//#endregion
|
|
1357
1103
|
//#region src/KubbDriver.ts
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1104
|
+
const ENFORCE_ORDER = {
|
|
1105
|
+
pre: -1,
|
|
1106
|
+
post: 1
|
|
1107
|
+
};
|
|
1108
|
+
const enforceWeight = (plugin) => plugin.enforce ? ENFORCE_ORDER[plugin.enforce] : 0;
|
|
1361
1109
|
var KubbDriver = class {
|
|
1362
1110
|
config;
|
|
1363
1111
|
options;
|
|
1364
1112
|
/**
|
|
1365
|
-
* The
|
|
1366
|
-
* Parse-only adapters are wrapped automatically.
|
|
1113
|
+
* The `InputNode` produced by the adapter. Set after adapter setup.
|
|
1367
1114
|
*/
|
|
1368
1115
|
inputNode = null;
|
|
1369
1116
|
adapter = null;
|
|
1370
1117
|
/**
|
|
1371
|
-
* Raw adapter source so `adapter.parse()`
|
|
1118
|
+
* Raw adapter source so `adapter.parse()` can run lazily.
|
|
1372
1119
|
* Intentionally outlives the build, cleared by `dispose()`.
|
|
1373
1120
|
*/
|
|
1374
1121
|
#adapterSource = null;
|
|
@@ -1377,20 +1124,20 @@ var KubbDriver = class {
|
|
|
1377
1124
|
* Plugins should use `this.addFile()` / `this.upsertFile()` (via their context) to
|
|
1378
1125
|
* add files. This property gives direct read/write access when needed.
|
|
1379
1126
|
*/
|
|
1380
|
-
fileManager = new
|
|
1127
|
+
fileManager = new require_usingCtx.FileManager();
|
|
1381
1128
|
plugins = /* @__PURE__ */ new Map();
|
|
1382
1129
|
/**
|
|
1383
|
-
* Tracks which plugins have generators registered via `addGenerator()` (
|
|
1384
|
-
* Used by the build loop to decide whether to emit generator
|
|
1130
|
+
* Tracks which plugins have generators registered via `addGenerator()` (hook-based path).
|
|
1131
|
+
* Used by the build loop to decide whether to emit generator hooks for a given plugin.
|
|
1385
1132
|
*/
|
|
1386
|
-
#
|
|
1133
|
+
#hookGeneratorPlugins = /* @__PURE__ */ new Set();
|
|
1387
1134
|
#resolvers = /* @__PURE__ */ new Map();
|
|
1388
1135
|
#defaultResolvers = /* @__PURE__ */ new Map();
|
|
1389
1136
|
/**
|
|
1390
|
-
*
|
|
1391
|
-
* in one pass. External `hooks.
|
|
1137
|
+
* Removers for every listener the driver added (plugin, generator) so `dispose()` can detach
|
|
1138
|
+
* them in one pass. External `hooks.hook(...)` listeners are not tracked.
|
|
1392
1139
|
*/
|
|
1393
|
-
#
|
|
1140
|
+
#unhooks = [];
|
|
1394
1141
|
/**
|
|
1395
1142
|
* Transform registry. Plugins populate it during `kubb:plugin:setup` via `addMacro`/`setMacros`,
|
|
1396
1143
|
* and `#runGenerators` reads it once per `(plugin, node)` pair through `applyTo`.
|
|
@@ -1402,163 +1149,137 @@ var KubbDriver = class {
|
|
|
1402
1149
|
this.adapter = config.adapter ?? null;
|
|
1403
1150
|
}
|
|
1404
1151
|
/**
|
|
1405
|
-
* Attaches a listener to the shared emitter and tracks it so `dispose()` can remove it later.
|
|
1406
|
-
* Listeners attached directly via `hooks.on(...)` are not tracked and survive disposal.
|
|
1407
|
-
*/
|
|
1408
|
-
#trackListener(event, handler) {
|
|
1409
|
-
this.hooks.on(event, handler);
|
|
1410
|
-
this.#listeners.push([event, handler]);
|
|
1411
|
-
}
|
|
1412
|
-
/**
|
|
1413
1152
|
* Normalizes every configured plugin, orders them, and registers their lifecycle handlers.
|
|
1414
1153
|
* A plugin that another lists as a dependency runs first, then `enforce: 'pre'` before
|
|
1415
1154
|
* `'post'`. When the config has an adapter, the adapter source is resolved from the input
|
|
1416
1155
|
* so `run` can parse it later.
|
|
1417
1156
|
*/
|
|
1418
1157
|
async setup() {
|
|
1419
|
-
const normalized = this.config.plugins.map((rawPlugin) =>
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1158
|
+
const normalized = this.#sortPlugins(this.config.plugins.map((rawPlugin) => {
|
|
1159
|
+
return {
|
|
1160
|
+
name: rawPlugin.name,
|
|
1161
|
+
dependencies: rawPlugin.dependencies,
|
|
1162
|
+
enforce: rawPlugin.enforce,
|
|
1163
|
+
hooks: rawPlugin.hooks,
|
|
1164
|
+
options: rawPlugin.options ?? {
|
|
1165
|
+
output: {
|
|
1166
|
+
path: ".",
|
|
1167
|
+
mode: "directory"
|
|
1168
|
+
},
|
|
1169
|
+
exclude: [],
|
|
1170
|
+
override: []
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
}));
|
|
1426
1174
|
for (const plugin of normalized) {
|
|
1427
|
-
if (plugin.apply) plugin.apply(this.config);
|
|
1428
1175
|
this.#registerPlugin(plugin);
|
|
1429
1176
|
this.plugins.set(plugin.name, plugin);
|
|
1430
1177
|
}
|
|
1431
1178
|
if (this.config.adapter) this.#adapterSource = inputToAdapterSource(this.config);
|
|
1432
1179
|
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Orders plugins so every dependency runs before its dependents (Kahn's algorithm), with
|
|
1182
|
+
* `enforce` (`'pre'` before normal before `'post'`) and declaration order as tiebreaks.
|
|
1183
|
+
* A pairwise `Array.sort` comparator cannot do this: dependency relations are not transitive
|
|
1184
|
+
* at the comparator level, so a chain where A depends on B and B depends on C could come out
|
|
1185
|
+
* wrong when A and C are never compared directly. Dependencies on plugins missing from the
|
|
1186
|
+
* config are ignored here and surface later through `requirePlugin`.
|
|
1187
|
+
*/
|
|
1188
|
+
#sortPlugins(plugins) {
|
|
1189
|
+
const queue = [...plugins].sort((a, b) => enforceWeight(a) - enforceWeight(b));
|
|
1190
|
+
const names = new Set(queue.map((plugin) => plugin.name));
|
|
1191
|
+
const blockedBy = new Map(queue.map((plugin) => [plugin.name, new Set(plugin.dependencies?.filter((name) => names.has(name) && name !== plugin.name))]));
|
|
1192
|
+
const sorted = [];
|
|
1193
|
+
for (const _ of plugins) {
|
|
1194
|
+
const index = queue.findIndex((plugin) => blockedBy.get(plugin.name)?.size === 0);
|
|
1195
|
+
if (index === -1) throw new Diagnostics.Error({
|
|
1196
|
+
code: Diagnostics.code.invalidPluginOptions,
|
|
1197
|
+
severity: "error",
|
|
1198
|
+
message: `Plugin dependencies form a cycle: ${queue.map((plugin) => plugin.name).join(" → ")}.`,
|
|
1199
|
+
help: "Remove one of the `dependencies` entries so the plugins can be ordered.",
|
|
1200
|
+
location: { kind: "config" }
|
|
1201
|
+
});
|
|
1202
|
+
const [plugin] = queue.splice(index, 1);
|
|
1203
|
+
if (!plugin) break;
|
|
1204
|
+
sorted.push(plugin);
|
|
1205
|
+
for (const blockers of blockedBy.values()) blockers.delete(plugin.name);
|
|
1206
|
+
}
|
|
1207
|
+
return sorted;
|
|
1208
|
+
}
|
|
1433
1209
|
get hooks() {
|
|
1434
1210
|
return this.options.hooks;
|
|
1435
1211
|
}
|
|
1436
1212
|
/**
|
|
1437
|
-
* Builds a `NormalizedPlugin` from a hook-style plugin, filling in default
|
|
1438
|
-
* options and copying `apply` when present. Registering its lifecycle handlers
|
|
1439
|
-
* on the `AsyncEventEmitter` is done separately by `#registerPlugin`.
|
|
1440
|
-
*/
|
|
1441
|
-
#normalizePlugin(plugin) {
|
|
1442
|
-
const normalized = {
|
|
1443
|
-
name: plugin.name,
|
|
1444
|
-
dependencies: plugin.dependencies,
|
|
1445
|
-
enforce: plugin.enforce,
|
|
1446
|
-
hooks: plugin.hooks,
|
|
1447
|
-
options: plugin.options ?? {
|
|
1448
|
-
output: {
|
|
1449
|
-
path: ".",
|
|
1450
|
-
mode: "directory"
|
|
1451
|
-
},
|
|
1452
|
-
exclude: [],
|
|
1453
|
-
override: []
|
|
1454
|
-
}
|
|
1455
|
-
};
|
|
1456
|
-
if ("apply" in plugin && typeof plugin.apply === "function") normalized.apply = plugin.apply;
|
|
1457
|
-
return normalized;
|
|
1458
|
-
}
|
|
1459
|
-
/**
|
|
1460
1213
|
* Parses the adapter source into `this.inputNode`. Idempotent, so repeated calls from
|
|
1461
|
-
* `run` do not re-parse.
|
|
1462
|
-
* Adapters with only `parse()` are wrapped via `ast.factory.createInput({ stream: true })` so the dispatch loop
|
|
1463
|
-
* stays stream-only.
|
|
1214
|
+
* `run` do not re-parse.
|
|
1464
1215
|
*/
|
|
1465
1216
|
async #parseInput() {
|
|
1466
1217
|
if (this.inputNode || !this.adapter || !this.#adapterSource) return;
|
|
1467
|
-
|
|
1468
|
-
const source = this.#adapterSource;
|
|
1469
|
-
if (adapter.stream) {
|
|
1470
|
-
this.inputNode = await adapter.stream(source);
|
|
1471
|
-
return;
|
|
1472
|
-
}
|
|
1473
|
-
const parsed = await adapter.parse(source);
|
|
1474
|
-
this.inputNode = _kubb_ast.ast.factory.createInput({
|
|
1475
|
-
stream: true,
|
|
1476
|
-
schemas: arrayToAsyncIterable(parsed.schemas),
|
|
1477
|
-
operations: arrayToAsyncIterable(parsed.operations),
|
|
1478
|
-
meta: parsed.meta
|
|
1479
|
-
});
|
|
1218
|
+
this.inputNode = await this.adapter.parse(this.#adapterSource);
|
|
1480
1219
|
}
|
|
1481
1220
|
/**
|
|
1482
|
-
* Registers a
|
|
1483
|
-
*
|
|
1484
|
-
*
|
|
1485
|
-
*
|
|
1486
|
-
* Every other `KubbHooks` event registers as a pass-through listener that external tooling
|
|
1487
|
-
* can observe via `hooks.on(...)`.
|
|
1221
|
+
* Registers a plugin's lifecycle hooks on the shared `Hookable` as pass-through listeners that
|
|
1222
|
+
* external tooling can observe via `hooks.hook(...)`. The returned remover is tracked for
|
|
1223
|
+
* `dispose`. `kubb:plugin:setup` is skipped here; `setupHooks` invokes it directly with a
|
|
1224
|
+
* plugin-scoped context.
|
|
1488
1225
|
*
|
|
1489
1226
|
* @internal
|
|
1490
1227
|
*/
|
|
1491
1228
|
#registerPlugin(plugin) {
|
|
1492
1229
|
const { hooks } = plugin;
|
|
1493
1230
|
if (!hooks) return;
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
const pluginCtx = {
|
|
1497
|
-
...globalCtx,
|
|
1498
|
-
options: plugin.options ?? {},
|
|
1499
|
-
addGenerator: (...generators) => {
|
|
1500
|
-
for (const generator of generators.flat()) this.registerGenerator(plugin.name, generator);
|
|
1501
|
-
},
|
|
1502
|
-
setResolver: (resolver) => {
|
|
1503
|
-
this.setPluginResolver(plugin.name, resolver);
|
|
1504
|
-
},
|
|
1505
|
-
addMacro: (macro) => {
|
|
1506
|
-
this.#transforms.add(plugin.name, macro);
|
|
1507
|
-
},
|
|
1508
|
-
setMacros: (macros) => {
|
|
1509
|
-
this.#transforms.set(plugin.name, macros);
|
|
1510
|
-
},
|
|
1511
|
-
setOptions: (opts) => {
|
|
1512
|
-
plugin.options = {
|
|
1513
|
-
...plugin.options,
|
|
1514
|
-
...opts
|
|
1515
|
-
};
|
|
1516
|
-
if (plugin.options.output) {
|
|
1517
|
-
const group = "group" in plugin.options ? plugin.options.group : void 0;
|
|
1518
|
-
plugin.options.output = normalizeOutput({
|
|
1519
|
-
output: plugin.options.output,
|
|
1520
|
-
group,
|
|
1521
|
-
pluginName: plugin.name
|
|
1522
|
-
});
|
|
1523
|
-
}
|
|
1524
|
-
},
|
|
1525
|
-
injectFile: (userFileNode) => {
|
|
1526
|
-
this.fileManager.add(_kubb_ast.ast.factory.createFile(userFileNode));
|
|
1527
|
-
}
|
|
1528
|
-
};
|
|
1529
|
-
return hooks["kubb:plugin:setup"](pluginCtx);
|
|
1530
|
-
};
|
|
1531
|
-
this.#trackListener("kubb:plugin:setup", setupHandler);
|
|
1532
|
-
}
|
|
1533
|
-
for (const event of Object.keys(hooks)) {
|
|
1534
|
-
if (event === "kubb:plugin:setup") continue;
|
|
1535
|
-
const handler = hooks[event];
|
|
1536
|
-
if (!handler) continue;
|
|
1537
|
-
this.#trackListener(event, handler);
|
|
1538
|
-
}
|
|
1231
|
+
const { "kubb:plugin:setup": _setup, ...configHooks } = hooks;
|
|
1232
|
+
this.#unhooks.push(this.hooks.addHooks(configHooks));
|
|
1539
1233
|
}
|
|
1540
1234
|
/**
|
|
1541
|
-
*
|
|
1542
|
-
*
|
|
1543
|
-
*
|
|
1544
|
-
*
|
|
1235
|
+
* Runs each plugin's `kubb:plugin:setup` handler, in plugin order, with a context scoped to that
|
|
1236
|
+
* plugin so `addGenerator`, `setResolver`, `addMacro`, `setMacros`, and `setOptions` target its
|
|
1237
|
+
* `NormalizedPlugin` entry. Called once from `run` before the plugin execution loop begins, so
|
|
1238
|
+
* plugins can configure generators, resolvers, macros, and options before `buildStart`.
|
|
1545
1239
|
*/
|
|
1546
|
-
async
|
|
1240
|
+
async setupHooks() {
|
|
1547
1241
|
const noop = () => {};
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1242
|
+
for (const plugin of this.plugins.values()) {
|
|
1243
|
+
const setup = plugin.hooks?.["kubb:plugin:setup"];
|
|
1244
|
+
if (!setup) continue;
|
|
1245
|
+
await setup({
|
|
1246
|
+
config: this.config,
|
|
1247
|
+
options: plugin.options ?? {},
|
|
1248
|
+
updateConfig: noop,
|
|
1249
|
+
addGenerator: (...generators) => {
|
|
1250
|
+
for (const generator of generators) this.registerGenerator(plugin.name, generator);
|
|
1251
|
+
},
|
|
1252
|
+
setResolver: (resolver) => {
|
|
1253
|
+
this.setPluginResolver(plugin.name, resolver);
|
|
1254
|
+
},
|
|
1255
|
+
addMacro: (macro) => {
|
|
1256
|
+
this.#transforms.add(plugin.name, macro);
|
|
1257
|
+
},
|
|
1258
|
+
setMacros: (macros) => {
|
|
1259
|
+
this.#transforms.set(plugin.name, macros);
|
|
1260
|
+
},
|
|
1261
|
+
setOptions: (opts) => {
|
|
1262
|
+
plugin.options = {
|
|
1263
|
+
...plugin.options,
|
|
1264
|
+
...opts
|
|
1265
|
+
};
|
|
1266
|
+
if (plugin.options.output) {
|
|
1267
|
+
const group = "group" in plugin.options ? plugin.options.group : void 0;
|
|
1268
|
+
plugin.options.output = normalizeOutput({
|
|
1269
|
+
output: plugin.options.output,
|
|
1270
|
+
group,
|
|
1271
|
+
pluginName: plugin.name
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
},
|
|
1275
|
+
injectFile: (userFileNode) => {
|
|
1276
|
+
this.fileManager.add(_kubb_ast.ast.factory.createFile(userFileNode));
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1559
1280
|
}
|
|
1560
1281
|
/**
|
|
1561
|
-
* Registers a generator for the given plugin on the shared
|
|
1282
|
+
* Registers a generator for the given plugin on the shared hook emitter.
|
|
1562
1283
|
*
|
|
1563
1284
|
* The generator's `schema`, `operation`, and `operations` methods are registered as
|
|
1564
1285
|
* listeners on `kubb:generate:schema`, `kubb:generate:operation`, and `kubb:generate:operations`
|
|
@@ -1571,9 +1292,9 @@ var KubbDriver = class {
|
|
|
1571
1292
|
* Call this method inside `addGenerator()` (in `kubb:plugin:setup`) to wire up a generator.
|
|
1572
1293
|
*/
|
|
1573
1294
|
registerGenerator(pluginName, generator) {
|
|
1574
|
-
const
|
|
1575
|
-
if (!method) return;
|
|
1576
|
-
|
|
1295
|
+
const wrap = (method) => {
|
|
1296
|
+
if (!method) return void 0;
|
|
1297
|
+
return async (node, ctx) => {
|
|
1577
1298
|
if (ctx.plugin.name !== pluginName) return;
|
|
1578
1299
|
const result = await method(node, ctx);
|
|
1579
1300
|
await this.dispatch({
|
|
@@ -1581,22 +1302,23 @@ var KubbDriver = class {
|
|
|
1581
1302
|
renderer: generator.renderer
|
|
1582
1303
|
});
|
|
1583
1304
|
};
|
|
1584
|
-
this.#trackListener(event, handler);
|
|
1585
1305
|
};
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1306
|
+
this.#unhooks.push(this.hooks.addHooks({
|
|
1307
|
+
"kubb:generate:schema": wrap(generator.schema),
|
|
1308
|
+
"kubb:generate:operation": wrap(generator.operation),
|
|
1309
|
+
"kubb:generate:operations": wrap(generator.operations)
|
|
1310
|
+
}));
|
|
1311
|
+
this.#hookGeneratorPlugins.add(pluginName);
|
|
1590
1312
|
}
|
|
1591
1313
|
/**
|
|
1592
1314
|
* Returns `true` when at least one generator was registered for the given plugin
|
|
1593
|
-
* via `addGenerator()` in `kubb:plugin:setup
|
|
1315
|
+
* via `addGenerator()` in `kubb:plugin:setup`.
|
|
1594
1316
|
*
|
|
1595
|
-
* Used by the build loop to decide whether to walk the AST and emit generator
|
|
1596
|
-
* for a plugin
|
|
1317
|
+
* Used by the build loop to decide whether to walk the AST and emit generator hooks
|
|
1318
|
+
* for a plugin.
|
|
1597
1319
|
*/
|
|
1598
|
-
|
|
1599
|
-
return this.#
|
|
1320
|
+
hasHookGenerators(pluginName) {
|
|
1321
|
+
return this.#hookGeneratorPlugins.has(pluginName);
|
|
1600
1322
|
}
|
|
1601
1323
|
/**
|
|
1602
1324
|
* Runs the full plugin pipeline. Returns the diagnostics collected so far even
|
|
@@ -1605,40 +1327,36 @@ var KubbDriver = class {
|
|
|
1605
1327
|
* contributes a `timing` diagnostic for the run summary.
|
|
1606
1328
|
*/
|
|
1607
1329
|
async run({ storage }) {
|
|
1608
|
-
const { hooks, config } = this;
|
|
1330
|
+
const { hooks, config, fileManager } = this;
|
|
1609
1331
|
const diagnostics = [];
|
|
1610
1332
|
const parsersMap = /* @__PURE__ */ new Map();
|
|
1611
1333
|
for (const parser of config.parsers) if (parser.extNames) for (const ext of parser.extNames) parsersMap.set(ext, parser);
|
|
1612
|
-
const
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
extension: config.output.extension
|
|
1616
|
-
});
|
|
1617
|
-
processor.hooks.on("start", async (files) => {
|
|
1618
|
-
await hooks.emit("kubb:files:processing:start", { files });
|
|
1619
|
-
});
|
|
1334
|
+
const onWriteStart = async (files) => {
|
|
1335
|
+
await hooks.callHook("kubb:files:processing:start", { files });
|
|
1336
|
+
};
|
|
1620
1337
|
const updateBuffer = [];
|
|
1621
|
-
|
|
1338
|
+
const onWriteUpdate = (item) => {
|
|
1622
1339
|
updateBuffer.push(item);
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
await hooks.
|
|
1340
|
+
};
|
|
1341
|
+
const onWriteEnd = async (files) => {
|
|
1342
|
+
await hooks.callHook("kubb:files:processing:update", { files: updateBuffer.map((item) => ({
|
|
1626
1343
|
...item,
|
|
1627
1344
|
config
|
|
1628
1345
|
})) });
|
|
1629
1346
|
updateBuffer.length = 0;
|
|
1630
|
-
await hooks.
|
|
1631
|
-
});
|
|
1632
|
-
const onFileUpsert = (file) => {
|
|
1633
|
-
processor.enqueue(file);
|
|
1347
|
+
await hooks.callHook("kubb:files:processing:end", { files });
|
|
1634
1348
|
};
|
|
1635
|
-
|
|
1349
|
+
const unhookWrites = fileManager.hooks.addHooks({
|
|
1350
|
+
start: onWriteStart,
|
|
1351
|
+
update: onWriteUpdate,
|
|
1352
|
+
end: onWriteEnd
|
|
1353
|
+
});
|
|
1636
1354
|
return Diagnostics.scope((diagnostic) => diagnostics.push(diagnostic), async () => {
|
|
1637
1355
|
try {
|
|
1638
1356
|
const outputRoot = (0, node_path.resolve)(config.root, config.output.path);
|
|
1639
1357
|
await this.#parseInput();
|
|
1640
|
-
await this.
|
|
1641
|
-
if (this.adapter && this.inputNode) await hooks.
|
|
1358
|
+
await this.setupHooks();
|
|
1359
|
+
if (this.adapter && this.inputNode) await hooks.callHook("kubb:build:start", Object.assign({
|
|
1642
1360
|
config,
|
|
1643
1361
|
adapter: this.adapter,
|
|
1644
1362
|
meta: this.inputNode.meta,
|
|
@@ -1649,9 +1367,9 @@ var KubbDriver = class {
|
|
|
1649
1367
|
const context = this.getContext(plugin);
|
|
1650
1368
|
const hrStart = process.hrtime();
|
|
1651
1369
|
try {
|
|
1652
|
-
await hooks.
|
|
1370
|
+
await hooks.callHook("kubb:plugin:start", { plugin });
|
|
1653
1371
|
} catch (caughtError) {
|
|
1654
|
-
const error = caughtError;
|
|
1372
|
+
const error = require_usingCtx.toError(caughtError);
|
|
1655
1373
|
const duration = getElapsedMs(hrStart);
|
|
1656
1374
|
await this.#emitPluginEnd({
|
|
1657
1375
|
plugin,
|
|
@@ -1668,7 +1386,7 @@ var KubbDriver = class {
|
|
|
1668
1386
|
}));
|
|
1669
1387
|
continue;
|
|
1670
1388
|
}
|
|
1671
|
-
if (this.
|
|
1389
|
+
if (this.hasHookGenerators(plugin.name)) {
|
|
1672
1390
|
generatorPlugins.push({
|
|
1673
1391
|
plugin,
|
|
1674
1392
|
context,
|
|
@@ -1687,11 +1405,14 @@ var KubbDriver = class {
|
|
|
1687
1405
|
success: true
|
|
1688
1406
|
});
|
|
1689
1407
|
}
|
|
1690
|
-
diagnostics.push(...await this.#runGenerators(generatorPlugins
|
|
1691
|
-
await
|
|
1692
|
-
await
|
|
1693
|
-
|
|
1694
|
-
|
|
1408
|
+
diagnostics.push(...await this.#runGenerators(generatorPlugins));
|
|
1409
|
+
await hooks.callHook("kubb:plugins:end", Object.assign({ config }, this.#filesPayload()));
|
|
1410
|
+
await fileManager.write(fileManager.files, {
|
|
1411
|
+
storage,
|
|
1412
|
+
parsers: parsersMap,
|
|
1413
|
+
extension: config.output.extension
|
|
1414
|
+
});
|
|
1415
|
+
await hooks.callHook("kubb:build:end", {
|
|
1695
1416
|
files: this.fileManager.files,
|
|
1696
1417
|
config,
|
|
1697
1418
|
outputDir: outputRoot
|
|
@@ -1701,7 +1422,7 @@ var KubbDriver = class {
|
|
|
1701
1422
|
diagnostics.push(Diagnostics.from(caughtError));
|
|
1702
1423
|
return { diagnostics: Diagnostics.dedupe(diagnostics) };
|
|
1703
1424
|
} finally {
|
|
1704
|
-
|
|
1425
|
+
unhookWrites();
|
|
1705
1426
|
}
|
|
1706
1427
|
});
|
|
1707
1428
|
}
|
|
@@ -1715,7 +1436,7 @@ var KubbDriver = class {
|
|
|
1715
1436
|
};
|
|
1716
1437
|
}
|
|
1717
1438
|
#emitPluginEnd({ plugin, duration, success, error }) {
|
|
1718
|
-
return this.hooks.
|
|
1439
|
+
return this.hooks.callHook("kubb:plugin:end", Object.assign({
|
|
1719
1440
|
plugin,
|
|
1720
1441
|
duration,
|
|
1721
1442
|
success,
|
|
@@ -1724,22 +1445,21 @@ var KubbDriver = class {
|
|
|
1724
1445
|
}, this.#filesPayload()));
|
|
1725
1446
|
}
|
|
1726
1447
|
/**
|
|
1727
|
-
*
|
|
1448
|
+
* Runs schemas and operations through every plugin's generators. Each node is run
|
|
1728
1449
|
* through the plugin's macros (from `this.#transforms`) before the generator sees it,
|
|
1729
1450
|
* so plugins stay isolated and the hot path stays per-node. Schemas run before operations
|
|
1730
|
-
*
|
|
1451
|
+
* so file output stays deterministic across runs.
|
|
1731
1452
|
* A failing plugin contributes an error diagnostic so the rest of the build continues.
|
|
1732
1453
|
* Every plugin also contributes a `timing` diagnostic.
|
|
1733
1454
|
*
|
|
1734
|
-
* Plugins
|
|
1735
|
-
*
|
|
1736
|
-
*
|
|
1737
|
-
* sit at the initial value until the very end of the run.
|
|
1455
|
+
* Plugins are processed one at a time, in full, so `kubb:plugin:end` fires as each one
|
|
1456
|
+
* completes rather than all at once at the end. That ordering drives the CLI's
|
|
1457
|
+
* `Plugins N/M` counter.
|
|
1738
1458
|
*
|
|
1739
1459
|
* When `this.inputNode` is `null`, every entry still gets a `kubb:plugin:end` so
|
|
1740
1460
|
* post-plugin listeners (the barrel writer and friends) complete.
|
|
1741
1461
|
*/
|
|
1742
|
-
async #runGenerators(entries
|
|
1462
|
+
async #runGenerators(entries) {
|
|
1743
1463
|
const diagnostics = [];
|
|
1744
1464
|
if (entries.length === 0) return diagnostics;
|
|
1745
1465
|
if (!this.inputNode) {
|
|
@@ -1759,160 +1479,104 @@ var KubbDriver = class {
|
|
|
1759
1479
|
}
|
|
1760
1480
|
const transforms = this.#transforms;
|
|
1761
1481
|
const { schemas, operations } = this.inputNode;
|
|
1762
|
-
const states = entries.map(({ plugin, context, hrStart }) => {
|
|
1763
|
-
const { exclude, include, override } = plugin.options;
|
|
1764
|
-
const hasExclude = Array.isArray(exclude) && exclude.length > 0;
|
|
1765
|
-
const hasInclude = Array.isArray(include) && include.length > 0;
|
|
1766
|
-
const hasOverride = Array.isArray(override) && override.length > 0;
|
|
1767
|
-
return {
|
|
1768
|
-
plugin,
|
|
1769
|
-
generatorContext: {
|
|
1770
|
-
...context,
|
|
1771
|
-
resolver: this.getResolver(plugin.name)
|
|
1772
|
-
},
|
|
1773
|
-
generators: plugin.generators ?? [],
|
|
1774
|
-
hrStart,
|
|
1775
|
-
failed: false,
|
|
1776
|
-
error: null,
|
|
1777
|
-
optionsAreStatic: !hasExclude && !hasInclude && !hasOverride,
|
|
1778
|
-
allowedSchemaNames: null
|
|
1779
|
-
};
|
|
1780
|
-
});
|
|
1781
1482
|
const emitsSchemaHook = this.hooks.listenerCount("kubb:generate:schema") > 0;
|
|
1782
1483
|
const emitsOperationHook = this.hooks.listenerCount("kubb:generate:operation") > 0;
|
|
1783
1484
|
const emitsOperationsHook = this.hooks.listenerCount("kubb:generate:operations") > 0;
|
|
1784
|
-
const
|
|
1785
|
-
const
|
|
1786
|
-
const pruningStates = states.filter(({ plugin }) => {
|
|
1787
|
-
const { include } = plugin.options;
|
|
1788
|
-
return (include?.some(({ type }) => require_memoryStorage.OPERATION_FILTER_TYPES.has(type)) ?? false) && !(include?.some(({ type }) => type === "schemaName") ?? false);
|
|
1789
|
-
});
|
|
1790
|
-
if (pruningStates.length > 0) {
|
|
1791
|
-
const includedOpsByState = new Map(pruningStates.map((state) => [state, []]));
|
|
1792
|
-
for (const operation of operationsBuffer) for (const state of pruningStates) {
|
|
1793
|
-
const { exclude, include, override } = state.plugin.options;
|
|
1794
|
-
if (state.generatorContext.resolver.resolveOptions(operation, {
|
|
1795
|
-
options: state.plugin.options,
|
|
1796
|
-
exclude,
|
|
1797
|
-
include,
|
|
1798
|
-
override
|
|
1799
|
-
}) !== null) includedOpsByState.get(state)?.push(operation);
|
|
1800
|
-
}
|
|
1801
|
-
for (const state of pruningStates) {
|
|
1802
|
-
state.allowedSchemaNames = (0, _kubb_ast.collectUsedSchemaNames)(includedOpsByState.get(state) ?? [], schemasBuffer);
|
|
1803
|
-
includedOpsByState.delete(state);
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
const resolveForPlugin = (state, node) => {
|
|
1807
|
-
const { plugin, generatorContext } = state;
|
|
1808
|
-
const transformedNode = transforms.applyTo(plugin.name, node);
|
|
1809
|
-
if (state.optionsAreStatic) return {
|
|
1810
|
-
transformedNode,
|
|
1811
|
-
options: plugin.options
|
|
1812
|
-
};
|
|
1485
|
+
const allowedSchemaNamesByPlugin = /* @__PURE__ */ new Map();
|
|
1486
|
+
for (const { plugin } of entries) {
|
|
1813
1487
|
const { exclude, include, override } = plugin.options;
|
|
1814
|
-
|
|
1488
|
+
if (!((include?.some(({ type }) => OPERATION_FILTER_TYPES.has(type)) ?? false) && !(include?.some(({ type }) => type === "schemaName") ?? false))) continue;
|
|
1489
|
+
const resolver = this.getResolver(plugin.name);
|
|
1490
|
+
const includedOps = operations.filter((operation) => resolver.default.options(operation, {
|
|
1815
1491
|
options: plugin.options,
|
|
1816
1492
|
exclude,
|
|
1817
1493
|
include,
|
|
1818
1494
|
override
|
|
1819
|
-
});
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1495
|
+
}) !== null);
|
|
1496
|
+
allowedSchemaNamesByPlugin.set(plugin.name, (0, _kubb_ast.collectUsedSchemaNames)(includedOps, schemas));
|
|
1497
|
+
}
|
|
1498
|
+
for (const { plugin, context, hrStart } of entries) {
|
|
1499
|
+
const generatorContext = {
|
|
1500
|
+
...context,
|
|
1501
|
+
resolver: this.getResolver(plugin.name)
|
|
1824
1502
|
};
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1503
|
+
const { exclude, include, override } = plugin.options;
|
|
1504
|
+
const optionsAreStatic = !exclude?.length && !include?.length && !override?.length;
|
|
1505
|
+
const allowedSchemaNames = allowedSchemaNamesByPlugin.get(plugin.name) ?? null;
|
|
1506
|
+
let error = null;
|
|
1507
|
+
const resolveForPlugin = (node) => {
|
|
1508
|
+
const transformedNode = transforms.applyTo(plugin.name, node);
|
|
1509
|
+
if (optionsAreStatic) return {
|
|
1510
|
+
transformedNode,
|
|
1511
|
+
options: plugin.options
|
|
1512
|
+
};
|
|
1513
|
+
const options = generatorContext.resolver.default.options(transformedNode, {
|
|
1514
|
+
options: plugin.options,
|
|
1515
|
+
exclude,
|
|
1516
|
+
include,
|
|
1517
|
+
override
|
|
1518
|
+
});
|
|
1519
|
+
if (options === null) return null;
|
|
1520
|
+
return {
|
|
1521
|
+
transformedNode,
|
|
1835
1522
|
options
|
|
1836
1523
|
};
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
const
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1524
|
+
};
|
|
1525
|
+
if (emitsSchemaHook) for (const node of schemas) {
|
|
1526
|
+
if (error) break;
|
|
1527
|
+
try {
|
|
1528
|
+
const resolved = resolveForPlugin(node);
|
|
1529
|
+
if (!resolved) continue;
|
|
1530
|
+
const { transformedNode, options } = resolved;
|
|
1531
|
+
if (allowedSchemaNames !== null && transformedNode.name && !allowedSchemaNames.has(transformedNode.name)) continue;
|
|
1532
|
+
await this.hooks.callHook("kubb:generate:schema", transformedNode, {
|
|
1533
|
+
...generatorContext,
|
|
1534
|
+
options
|
|
1845
1535
|
});
|
|
1846
|
-
|
|
1536
|
+
} catch (caughtError) {
|
|
1537
|
+
error = require_usingCtx.toError(caughtError);
|
|
1847
1538
|
}
|
|
1848
|
-
if (dispatch.emit) await dispatch.emit(transformedNode, ctx);
|
|
1849
|
-
} catch (caughtError) {
|
|
1850
|
-
state.failed = true;
|
|
1851
|
-
state.error = caughtError;
|
|
1852
1539
|
}
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
const dispatchPass = async (state, nodes, dispatch) => {
|
|
1865
|
-
let sinceFlush = 0;
|
|
1866
|
-
for (const node of nodes) {
|
|
1867
|
-
await dispatchNode(state, node, dispatch);
|
|
1868
|
-
if (++sinceFlush >= 8) {
|
|
1869
|
-
sinceFlush = 0;
|
|
1870
|
-
await flushPending();
|
|
1540
|
+
if (emitsOperationHook) for (const node of operations) {
|
|
1541
|
+
if (error) break;
|
|
1542
|
+
try {
|
|
1543
|
+
const resolved = resolveForPlugin(node);
|
|
1544
|
+
if (!resolved) continue;
|
|
1545
|
+
await this.hooks.callHook("kubb:generate:operation", resolved.transformedNode, {
|
|
1546
|
+
...generatorContext,
|
|
1547
|
+
options: resolved.options
|
|
1548
|
+
});
|
|
1549
|
+
} catch (caughtError) {
|
|
1550
|
+
error = require_usingCtx.toError(caughtError);
|
|
1871
1551
|
}
|
|
1872
1552
|
}
|
|
1873
|
-
if (
|
|
1874
|
-
};
|
|
1875
|
-
for (const state of states) {
|
|
1876
|
-
const needsOperationsAggregate = emitsOperationsHook || state.generators.some((gen) => !!gen.operations);
|
|
1877
|
-
await dispatchPass(state, schemasBuffer, schemaDispatch);
|
|
1878
|
-
await dispatchPass(state, operationsBuffer, operationDispatch);
|
|
1879
|
-
if (!state.failed && needsOperationsAggregate) try {
|
|
1880
|
-
const { plugin, generatorContext, generators } = state;
|
|
1553
|
+
if (!error && emitsOperationsHook) try {
|
|
1881
1554
|
const ctx = {
|
|
1882
1555
|
...generatorContext,
|
|
1883
1556
|
options: plugin.options
|
|
1884
1557
|
};
|
|
1885
|
-
const pluginOperations =
|
|
1886
|
-
const resolved = resolveForPlugin(
|
|
1558
|
+
const pluginOperations = operations.reduce((acc, node) => {
|
|
1559
|
+
const resolved = resolveForPlugin(node);
|
|
1887
1560
|
if (resolved) acc.push(resolved.transformedNode);
|
|
1888
1561
|
return acc;
|
|
1889
1562
|
}, []);
|
|
1890
|
-
|
|
1891
|
-
if (!gen.operations) continue;
|
|
1892
|
-
const result = await gen.operations(pluginOperations, ctx);
|
|
1893
|
-
await this.dispatch({
|
|
1894
|
-
result,
|
|
1895
|
-
renderer: gen.renderer
|
|
1896
|
-
});
|
|
1897
|
-
}
|
|
1898
|
-
await this.hooks.emit("kubb:generate:operations", pluginOperations, ctx);
|
|
1563
|
+
await this.hooks.callHook("kubb:generate:operations", pluginOperations, ctx);
|
|
1899
1564
|
} catch (caughtError) {
|
|
1900
|
-
|
|
1901
|
-
state.error = caughtError;
|
|
1565
|
+
error = require_usingCtx.toError(caughtError);
|
|
1902
1566
|
}
|
|
1903
|
-
const duration = getElapsedMs(
|
|
1567
|
+
const duration = getElapsedMs(hrStart);
|
|
1904
1568
|
await this.#emitPluginEnd({
|
|
1905
|
-
plugin
|
|
1569
|
+
plugin,
|
|
1906
1570
|
duration,
|
|
1907
|
-
success: !
|
|
1908
|
-
error:
|
|
1571
|
+
success: !error,
|
|
1572
|
+
error: error ?? void 0
|
|
1909
1573
|
});
|
|
1910
|
-
if (
|
|
1911
|
-
...Diagnostics.from(
|
|
1912
|
-
plugin:
|
|
1574
|
+
if (error) diagnostics.push({
|
|
1575
|
+
...Diagnostics.from(error),
|
|
1576
|
+
plugin: plugin.name
|
|
1913
1577
|
});
|
|
1914
1578
|
diagnostics.push(Diagnostics.performance({
|
|
1915
|
-
plugin:
|
|
1579
|
+
plugin: plugin.name,
|
|
1916
1580
|
duration
|
|
1917
1581
|
}));
|
|
1918
1582
|
}
|
|
@@ -1932,7 +1596,7 @@ var KubbDriver = class {
|
|
|
1932
1596
|
*/
|
|
1933
1597
|
async dispatch({ result, renderer }) {
|
|
1934
1598
|
try {
|
|
1935
|
-
var _usingCtx$2 =
|
|
1599
|
+
var _usingCtx$2 = require_usingCtx._usingCtx();
|
|
1936
1600
|
if (!result) return;
|
|
1937
1601
|
if (Array.isArray(result)) {
|
|
1938
1602
|
this.fileManager.upsert(...result);
|
|
@@ -1940,10 +1604,6 @@ var KubbDriver = class {
|
|
|
1940
1604
|
}
|
|
1941
1605
|
if (!renderer) return;
|
|
1942
1606
|
const instance = _usingCtx$2.u(renderer());
|
|
1943
|
-
if (instance.stream) {
|
|
1944
|
-
for (const file of instance.stream(result)) this.fileManager.upsert(file);
|
|
1945
|
-
return;
|
|
1946
|
-
}
|
|
1947
1607
|
await instance.render(result);
|
|
1948
1608
|
this.fileManager.upsert(...instance.files);
|
|
1949
1609
|
} catch (_) {
|
|
@@ -1959,9 +1619,9 @@ var KubbDriver = class {
|
|
|
1959
1619
|
* @internal
|
|
1960
1620
|
*/
|
|
1961
1621
|
dispose() {
|
|
1962
|
-
for (const
|
|
1963
|
-
this.#
|
|
1964
|
-
this.#
|
|
1622
|
+
for (const unhook of this.#unhooks) unhook();
|
|
1623
|
+
this.#unhooks.length = 0;
|
|
1624
|
+
this.#hookGeneratorPlugins.clear();
|
|
1965
1625
|
this.#transforms.dispose();
|
|
1966
1626
|
this.#resolvers.clear();
|
|
1967
1627
|
this.#defaultResolvers.clear();
|
|
@@ -1972,26 +1632,21 @@ var KubbDriver = class {
|
|
|
1972
1632
|
[Symbol.dispose]() {
|
|
1973
1633
|
this.dispose();
|
|
1974
1634
|
}
|
|
1975
|
-
#getDefaultResolver = memoize(this.#defaultResolvers, (pluginName) =>
|
|
1976
|
-
name: "default",
|
|
1977
|
-
pluginName
|
|
1978
|
-
})));
|
|
1635
|
+
#getDefaultResolver = memoize(this.#defaultResolvers, (pluginName) => createResolver({ pluginName }));
|
|
1979
1636
|
/**
|
|
1980
1637
|
* Merges `partial` with the plugin's default resolver and stores the result.
|
|
1981
1638
|
* Also mirrors it onto `plugin.resolver` so callers using `getPlugin(name).resolver`
|
|
1982
1639
|
* get the up-to-date resolver without going through `getResolver()`.
|
|
1983
1640
|
*/
|
|
1984
1641
|
setPluginResolver(pluginName, partial) {
|
|
1985
|
-
const
|
|
1986
|
-
|
|
1987
|
-
...partial
|
|
1988
|
-
};
|
|
1642
|
+
const defaultResolver = this.#getDefaultResolver(pluginName);
|
|
1643
|
+
const merged = Resolver.merge(defaultResolver, partial);
|
|
1989
1644
|
this.#resolvers.set(pluginName, merged);
|
|
1990
1645
|
const plugin = this.plugins.get(pluginName);
|
|
1991
1646
|
if (plugin) plugin.resolver = merged;
|
|
1992
1647
|
}
|
|
1993
1648
|
getResolver(pluginName) {
|
|
1994
|
-
return this.#resolvers.get(pluginName) ?? this
|
|
1649
|
+
return this.#resolvers.get(pluginName) ?? this.#getDefaultResolver(pluginName);
|
|
1995
1650
|
}
|
|
1996
1651
|
getContext(plugin) {
|
|
1997
1652
|
const driver = this;
|
|
@@ -2059,18 +1714,18 @@ var KubbDriver = class {
|
|
|
2059
1714
|
return this.plugins.get(pluginName);
|
|
2060
1715
|
}
|
|
2061
1716
|
requirePlugin(pluginName, context) {
|
|
2062
|
-
const plugin = this.
|
|
2063
|
-
if (
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
|
|
1717
|
+
const plugin = this.getPlugin(pluginName);
|
|
1718
|
+
if (plugin) return plugin;
|
|
1719
|
+
const requiredBy = context?.requiredBy;
|
|
1720
|
+
const by = requiredBy ? ` by "${requiredBy}"` : "";
|
|
1721
|
+
const help = requiredBy ? ` (required by "${requiredBy}")` : "";
|
|
1722
|
+
throw new Diagnostics.Error({
|
|
1723
|
+
code: Diagnostics.code.pluginNotFound,
|
|
1724
|
+
severity: "error",
|
|
1725
|
+
message: `Plugin "${pluginName}" is required${by} but not found. Make sure it is included in your Kubb config.`,
|
|
1726
|
+
help: `Add "${pluginName}" to the \`plugins\` array in kubb.config.ts${help}, or remove the dependency on it.`,
|
|
1727
|
+
location: { kind: "config" }
|
|
1728
|
+
});
|
|
2074
1729
|
}
|
|
2075
1730
|
};
|
|
2076
1731
|
function inputToAdapterSource(config) {
|
|
@@ -2096,7 +1751,73 @@ function inputToAdapterSource(config) {
|
|
|
2096
1751
|
};
|
|
2097
1752
|
}
|
|
2098
1753
|
//#endregion
|
|
1754
|
+
//#region src/createStorage.ts
|
|
1755
|
+
/**
|
|
1756
|
+
* Defines a custom storage backend. The builder receives user options and
|
|
1757
|
+
* returns a `Storage` implementation. Kubb ships with filesystem and in-memory
|
|
1758
|
+
* storages. A custom backend writes generated files elsewhere, such as cloud
|
|
1759
|
+
* storage or a database.
|
|
1760
|
+
*
|
|
1761
|
+
* @example In-memory storage (the built-in implementation)
|
|
1762
|
+
* ```ts
|
|
1763
|
+
* import { createStorage } from '@kubb/core'
|
|
1764
|
+
*
|
|
1765
|
+
* export const memoryStorage = createStorage(() => {
|
|
1766
|
+
* const store = new Map<string, string>()
|
|
1767
|
+
*
|
|
1768
|
+
* return {
|
|
1769
|
+
* name: 'memory',
|
|
1770
|
+
* async hasItem(key) {
|
|
1771
|
+
* return store.has(key)
|
|
1772
|
+
* },
|
|
1773
|
+
* async getItem(key) {
|
|
1774
|
+
* return store.get(key) ?? null
|
|
1775
|
+
* },
|
|
1776
|
+
* async setItem(key, value) {
|
|
1777
|
+
* store.set(key, value)
|
|
1778
|
+
* },
|
|
1779
|
+
* async removeItem(key) {
|
|
1780
|
+
* store.delete(key)
|
|
1781
|
+
* },
|
|
1782
|
+
* async getKeys(base) {
|
|
1783
|
+
* const keys = [...store.keys()]
|
|
1784
|
+
* return base ? keys.filter((k) => k.startsWith(base)) : keys
|
|
1785
|
+
* },
|
|
1786
|
+
* async clear(base) {
|
|
1787
|
+
* if (!base) store.clear()
|
|
1788
|
+
* },
|
|
1789
|
+
* }
|
|
1790
|
+
* })
|
|
1791
|
+
* ```
|
|
1792
|
+
*/
|
|
1793
|
+
function createStorage(build) {
|
|
1794
|
+
return (options) => build(options ?? {});
|
|
1795
|
+
}
|
|
1796
|
+
//#endregion
|
|
2099
1797
|
//#region src/storages/fsStorage.ts
|
|
1798
|
+
const WRITE_CONCURRENCY = 50;
|
|
1799
|
+
function createLimiter(concurrency) {
|
|
1800
|
+
let active = 0;
|
|
1801
|
+
const queue = [];
|
|
1802
|
+
function next() {
|
|
1803
|
+
if (active >= concurrency) return;
|
|
1804
|
+
const run = queue.shift();
|
|
1805
|
+
if (!run) return;
|
|
1806
|
+
active++;
|
|
1807
|
+
run();
|
|
1808
|
+
}
|
|
1809
|
+
return function limit(task) {
|
|
1810
|
+
return new Promise((resolve, reject) => {
|
|
1811
|
+
queue.push(() => {
|
|
1812
|
+
task().then(resolve, reject).finally(() => {
|
|
1813
|
+
active--;
|
|
1814
|
+
next();
|
|
1815
|
+
});
|
|
1816
|
+
});
|
|
1817
|
+
next();
|
|
1818
|
+
});
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
2100
1821
|
/**
|
|
2101
1822
|
* Built-in filesystem storage driver.
|
|
2102
1823
|
*
|
|
@@ -2109,6 +1830,8 @@ function inputToAdapterSource(config) {
|
|
|
2109
1830
|
* - the write is skipped when the file content is already identical
|
|
2110
1831
|
* - missing parent directories are created automatically
|
|
2111
1832
|
* - Bun's native file API is used when running under Bun
|
|
1833
|
+
* - concurrent `setItem` calls are capped at {@link WRITE_CONCURRENCY} in flight, so a caller
|
|
1834
|
+
* can fire every file's write without pacing itself
|
|
2112
1835
|
*
|
|
2113
1836
|
* @example
|
|
2114
1837
|
* ```ts
|
|
@@ -2122,91 +1845,50 @@ function inputToAdapterSource(config) {
|
|
|
2122
1845
|
* })
|
|
2123
1846
|
* ```
|
|
2124
1847
|
*/
|
|
2125
|
-
const fsStorage =
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
await (0, node_fs_promises.access)((0, node_path.resolve)(key));
|
|
2130
|
-
return true;
|
|
2131
|
-
} catch (_error) {
|
|
2132
|
-
return false;
|
|
2133
|
-
}
|
|
2134
|
-
},
|
|
2135
|
-
async getItem(key) {
|
|
2136
|
-
try {
|
|
2137
|
-
return await (0, node_fs_promises.readFile)((0, node_path.resolve)(key), "utf8");
|
|
2138
|
-
} catch (_error) {
|
|
2139
|
-
return null;
|
|
2140
|
-
}
|
|
2141
|
-
},
|
|
2142
|
-
async setItem(key, value) {
|
|
2143
|
-
await require_memoryStorage.write((0, node_path.resolve)(key), value, { sanity: false });
|
|
2144
|
-
},
|
|
2145
|
-
async removeItem(key) {
|
|
2146
|
-
await (0, node_fs_promises.rm)((0, node_path.resolve)(key), { force: true });
|
|
2147
|
-
},
|
|
2148
|
-
async getKeys(base) {
|
|
2149
|
-
const resolvedBase = (0, node_path.resolve)(base ?? process.cwd());
|
|
2150
|
-
if (require_memoryStorage.runtime.isBun) {
|
|
2151
|
-
const bunGlob = new Bun.Glob("**/*");
|
|
2152
|
-
return Array.fromAsync(bunGlob.scan({
|
|
2153
|
-
cwd: resolvedBase,
|
|
2154
|
-
onlyFiles: true,
|
|
2155
|
-
dot: true
|
|
2156
|
-
}));
|
|
2157
|
-
}
|
|
2158
|
-
const keys = [];
|
|
2159
|
-
try {
|
|
2160
|
-
for await (const entry of (0, node_fs_promises.glob)("**/*", {
|
|
2161
|
-
cwd: resolvedBase,
|
|
2162
|
-
withFileTypes: true
|
|
2163
|
-
})) if (entry.isFile()) keys.push(require_memoryStorage.toPosixPath((0, node_path.relative)(resolvedBase, (0, node_path.join)(entry.parentPath, entry.name))));
|
|
2164
|
-
} catch (_error) {}
|
|
2165
|
-
return keys;
|
|
2166
|
-
},
|
|
2167
|
-
async clear(base) {
|
|
2168
|
-
if (!base) return;
|
|
2169
|
-
await require_memoryStorage.clean((0, node_path.resolve)(base));
|
|
2170
|
-
}
|
|
2171
|
-
}));
|
|
2172
|
-
//#endregion
|
|
2173
|
-
//#region src/createKubb.ts
|
|
2174
|
-
/**
|
|
2175
|
-
* Builds a `Storage` view scoped to the file paths produced by the current build.
|
|
2176
|
-
* Reads delegate to the underlying `storage` so source bytes stay where they were
|
|
2177
|
-
* written. Writes register the key so subsequent reads and `getKeys` are scoped
|
|
2178
|
-
* to this build's output.
|
|
2179
|
-
*/
|
|
2180
|
-
function createSourcesView(storage) {
|
|
2181
|
-
const paths = /* @__PURE__ */ new Set();
|
|
2182
|
-
return require_memoryStorage.createStorage(() => ({
|
|
2183
|
-
name: `${storage.name}:sources`,
|
|
1848
|
+
const fsStorage = createStorage(() => {
|
|
1849
|
+
const limit = createLimiter(WRITE_CONCURRENCY);
|
|
1850
|
+
return {
|
|
1851
|
+
name: "fs",
|
|
2184
1852
|
async hasItem(key) {
|
|
2185
|
-
|
|
1853
|
+
try {
|
|
1854
|
+
await (0, node_fs_promises.access)((0, node_path.resolve)(key));
|
|
1855
|
+
return true;
|
|
1856
|
+
} catch (_error) {
|
|
1857
|
+
return false;
|
|
1858
|
+
}
|
|
2186
1859
|
},
|
|
2187
1860
|
async getItem(key) {
|
|
2188
|
-
|
|
1861
|
+
try {
|
|
1862
|
+
return await (0, node_fs_promises.readFile)((0, node_path.resolve)(key), "utf8");
|
|
1863
|
+
} catch (_error) {
|
|
1864
|
+
return null;
|
|
1865
|
+
}
|
|
2189
1866
|
},
|
|
2190
1867
|
async setItem(key, value) {
|
|
2191
|
-
|
|
2192
|
-
await storage.setItem(key, value);
|
|
1868
|
+
await limit(() => require_usingCtx.write((0, node_path.resolve)(key), value, { sanity: false }));
|
|
2193
1869
|
},
|
|
2194
1870
|
async removeItem(key) {
|
|
2195
|
-
|
|
2196
|
-
await storage.removeItem(key);
|
|
1871
|
+
await (0, node_fs_promises.rm)((0, node_path.resolve)(key), { force: true });
|
|
2197
1872
|
},
|
|
2198
1873
|
async getKeys(base) {
|
|
2199
|
-
|
|
2200
|
-
const
|
|
2201
|
-
|
|
2202
|
-
|
|
1874
|
+
const resolvedBase = (0, node_path.resolve)(base ?? process.cwd());
|
|
1875
|
+
const keys = [];
|
|
1876
|
+
try {
|
|
1877
|
+
for await (const entry of (0, node_fs_promises.glob)("**/*", {
|
|
1878
|
+
cwd: resolvedBase,
|
|
1879
|
+
withFileTypes: true
|
|
1880
|
+
})) if (entry.isFile()) keys.push(require_usingCtx.toPosixPath((0, node_path.relative)(resolvedBase, (0, node_path.join)(entry.parentPath, entry.name))));
|
|
1881
|
+
} catch (_error) {}
|
|
1882
|
+
return keys;
|
|
2203
1883
|
},
|
|
2204
|
-
async clear() {
|
|
2205
|
-
|
|
2206
|
-
await
|
|
1884
|
+
async clear(base) {
|
|
1885
|
+
if (!base) return;
|
|
1886
|
+
await require_usingCtx.clean((0, node_path.resolve)(base));
|
|
2207
1887
|
}
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
1888
|
+
};
|
|
1889
|
+
});
|
|
1890
|
+
//#endregion
|
|
1891
|
+
//#region src/createKubb.ts
|
|
2210
1892
|
function resolveConfig(userConfig) {
|
|
2211
1893
|
return {
|
|
2212
1894
|
...userConfig,
|
|
@@ -2232,12 +1914,12 @@ function resolveConfig(userConfig) {
|
|
|
2232
1914
|
* `createKubb` takes a plain config object (the shape `defineConfig` produces),
|
|
2233
1915
|
* not a fluent builder.
|
|
2234
1916
|
*
|
|
2235
|
-
* Attach
|
|
1917
|
+
* Attach hook listeners to `.hooks` before calling `setup()` or `build()`.
|
|
2236
1918
|
*
|
|
2237
1919
|
* @example
|
|
2238
1920
|
* ```ts
|
|
2239
1921
|
* const kubb = createKubb(userConfig)
|
|
2240
|
-
* kubb.hooks.
|
|
1922
|
+
* kubb.hooks.hook('kubb:plugin:end', ({ plugin, duration }) => console.log(plugin.name, duration))
|
|
2241
1923
|
* const { files, diagnostics } = await kubb.safeBuild()
|
|
2242
1924
|
* ```
|
|
2243
1925
|
*/
|
|
@@ -2248,7 +1930,7 @@ var Kubb = class {
|
|
|
2248
1930
|
#storage = null;
|
|
2249
1931
|
constructor(userConfig, options = {}) {
|
|
2250
1932
|
this.config = resolveConfig(userConfig);
|
|
2251
|
-
this.hooks = options.hooks ?? new
|
|
1933
|
+
this.hooks = options.hooks ?? new require_usingCtx.Hookable();
|
|
2252
1934
|
}
|
|
2253
1935
|
get storage() {
|
|
2254
1936
|
if (!this.#storage) throw new Error("[kubb] setup() must be called before accessing storage");
|
|
@@ -2264,12 +1946,11 @@ var Kubb = class {
|
|
|
2264
1946
|
async setup() {
|
|
2265
1947
|
const config = this.config;
|
|
2266
1948
|
const driver = new KubbDriver(config, { hooks: this.hooks });
|
|
2267
|
-
const storage = createSourcesView(config.storage);
|
|
2268
1949
|
this.hooks.setMaxListeners(Math.max(10, config.plugins.length * 4));
|
|
2269
1950
|
if (config.output.clean) await config.storage.clear((0, node_path.resolve)(config.root, config.output.path));
|
|
2270
1951
|
await driver.setup();
|
|
2271
1952
|
this.#driver = driver;
|
|
2272
|
-
this.#storage = storage;
|
|
1953
|
+
this.#storage = config.storage;
|
|
2273
1954
|
}
|
|
2274
1955
|
/**
|
|
2275
1956
|
* Runs the full pipeline and throws on any plugin error.
|
|
@@ -2279,7 +1960,7 @@ var Kubb = class {
|
|
|
2279
1960
|
const out = await this.safeBuild();
|
|
2280
1961
|
if (Diagnostics.hasError(out.diagnostics)) {
|
|
2281
1962
|
const errors = out.diagnostics.filter(Diagnostics.isProblem).filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.cause ?? new Diagnostics.Error(diagnostic));
|
|
2282
|
-
throw new
|
|
1963
|
+
throw new require_usingCtx.BuildError(`Build failed with ${errors.length} ${errors.length === 1 ? "error" : "errors"}`, { errors });
|
|
2283
1964
|
}
|
|
2284
1965
|
return out;
|
|
2285
1966
|
}
|
|
@@ -2290,11 +1971,11 @@ var Kubb = class {
|
|
|
2290
1971
|
*/
|
|
2291
1972
|
async safeBuild() {
|
|
2292
1973
|
try {
|
|
2293
|
-
var _usingCtx$1 =
|
|
1974
|
+
var _usingCtx$1 = require_usingCtx._usingCtx();
|
|
2294
1975
|
if (!this.#driver) await this.setup();
|
|
2295
|
-
const
|
|
2296
|
-
const driver =
|
|
2297
|
-
const storage =
|
|
1976
|
+
const self = _usingCtx$1.u(this);
|
|
1977
|
+
const driver = self.driver;
|
|
1978
|
+
const storage = self.storage;
|
|
2298
1979
|
const { diagnostics } = await driver.run({ storage });
|
|
2299
1980
|
return {
|
|
2300
1981
|
diagnostics,
|
|
@@ -2355,7 +2036,7 @@ const logLevel = {
|
|
|
2355
2036
|
/**
|
|
2356
2037
|
* Defines a reporter. When the definition has a `drain`, the returned reporter buffers each value
|
|
2357
2038
|
* `report` returns and hands the array to `drain` once, then clears it. Without a `drain`, nothing
|
|
2358
|
-
* is buffered. Wiring the reporter onto the run's
|
|
2039
|
+
* is buffered. Wiring the reporter onto the run's hooks is the host's job, so the reporter only
|
|
2359
2040
|
* ever deals with a {@link GenerationResult}.
|
|
2360
2041
|
*
|
|
2361
2042
|
* @example
|
|
@@ -2374,22 +2055,19 @@ const logLevel = {
|
|
|
2374
2055
|
* ```
|
|
2375
2056
|
*/
|
|
2376
2057
|
function createReporter(reporter) {
|
|
2377
|
-
const
|
|
2378
|
-
if (!drain) return {
|
|
2379
|
-
name: reporter.name,
|
|
2380
|
-
async report(result, context) {
|
|
2381
|
-
await reporter.report(result, context);
|
|
2382
|
-
}
|
|
2383
|
-
};
|
|
2384
|
-
const reports = [];
|
|
2058
|
+
const reports = /* @__PURE__ */ new Set();
|
|
2385
2059
|
return {
|
|
2386
2060
|
name: reporter.name,
|
|
2387
2061
|
async report(result, context) {
|
|
2388
|
-
|
|
2062
|
+
const report = await reporter.report(result, context);
|
|
2063
|
+
reports.add(report);
|
|
2389
2064
|
},
|
|
2390
2065
|
async drain(context) {
|
|
2391
|
-
await drain(context, reports);
|
|
2392
|
-
reports.
|
|
2066
|
+
await reporter.drain?.(context, Array.from(reports));
|
|
2067
|
+
reports.clear();
|
|
2068
|
+
},
|
|
2069
|
+
[Symbol.dispose]() {
|
|
2070
|
+
reports.clear();
|
|
2393
2071
|
}
|
|
2394
2072
|
};
|
|
2395
2073
|
}
|
|
@@ -2542,7 +2220,7 @@ function buildTimingSection(report) {
|
|
|
2542
2220
|
* `## Timings` section. Selected with `--reporter file` (or `reporters: ['file']`).
|
|
2543
2221
|
*
|
|
2544
2222
|
* @note It captures the collected diagnostics once a config finishes, not the live
|
|
2545
|
-
* `kubb:info`/`kubb:plugin`
|
|
2223
|
+
* `kubb:info`/`kubb:plugin` hook stream. Color is stripped so the file stays plain text even when
|
|
2546
2224
|
* the run is attached to a TTY.
|
|
2547
2225
|
*/
|
|
2548
2226
|
const fileReporter = createReporter({
|
|
@@ -2562,7 +2240,7 @@ const fileReporter = createReporter({
|
|
|
2562
2240
|
Date.now()
|
|
2563
2241
|
].filter(Boolean).join("-")}.log`;
|
|
2564
2242
|
const pathName = (0, node_path.resolve)(node_process.default.cwd(), ".kubb", baseName);
|
|
2565
|
-
await
|
|
2243
|
+
await require_usingCtx.write(pathName, `${content}\n`);
|
|
2566
2244
|
console.error(`Debug log written to ${(0, node_path.relative)(node_process.default.cwd(), pathName)}`);
|
|
2567
2245
|
}
|
|
2568
2246
|
});
|
|
@@ -2680,24 +2358,74 @@ function defineParser(parser) {
|
|
|
2680
2358
|
return parser;
|
|
2681
2359
|
}
|
|
2682
2360
|
//#endregion
|
|
2683
|
-
|
|
2361
|
+
//#region src/storages/memoryStorage.ts
|
|
2362
|
+
/**
|
|
2363
|
+
* In-memory storage driver. Useful for testing and dry-run scenarios where
|
|
2364
|
+
* generated output should be captured without touching the filesystem.
|
|
2365
|
+
*
|
|
2366
|
+
* All data lives in a `Map` scoped to the storage instance and is discarded
|
|
2367
|
+
* when the instance is garbage-collected.
|
|
2368
|
+
*
|
|
2369
|
+
* @example
|
|
2370
|
+
* ```ts
|
|
2371
|
+
* import { memoryStorage } from '@kubb/core'
|
|
2372
|
+
* import { defineConfig } from 'kubb'
|
|
2373
|
+
*
|
|
2374
|
+
* export default defineConfig({
|
|
2375
|
+
* input: { path: './petStore.yaml' },
|
|
2376
|
+
* output: { path: './src/gen' },
|
|
2377
|
+
* storage: memoryStorage(),
|
|
2378
|
+
* })
|
|
2379
|
+
* ```
|
|
2380
|
+
*/
|
|
2381
|
+
const memoryStorage = createStorage(() => {
|
|
2382
|
+
const store = /* @__PURE__ */ new Map();
|
|
2383
|
+
return {
|
|
2384
|
+
name: "memory",
|
|
2385
|
+
async hasItem(key) {
|
|
2386
|
+
return store.has(key);
|
|
2387
|
+
},
|
|
2388
|
+
async getItem(key) {
|
|
2389
|
+
return store.get(key) ?? null;
|
|
2390
|
+
},
|
|
2391
|
+
async setItem(key, value) {
|
|
2392
|
+
store.set(key, value);
|
|
2393
|
+
},
|
|
2394
|
+
async removeItem(key) {
|
|
2395
|
+
store.delete(key);
|
|
2396
|
+
},
|
|
2397
|
+
async getKeys(base) {
|
|
2398
|
+
const keys = [...store.keys()];
|
|
2399
|
+
return base ? keys.filter((k) => k.startsWith(base)) : keys;
|
|
2400
|
+
},
|
|
2401
|
+
async clear(base) {
|
|
2402
|
+
if (!base) {
|
|
2403
|
+
store.clear();
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
for (const key of store.keys()) if (key.startsWith(base)) store.delete(key);
|
|
2407
|
+
}
|
|
2408
|
+
};
|
|
2409
|
+
});
|
|
2410
|
+
//#endregion
|
|
2684
2411
|
exports.Diagnostics = Diagnostics;
|
|
2412
|
+
exports.Hookable = require_usingCtx.Hookable;
|
|
2685
2413
|
exports.KubbDriver = KubbDriver;
|
|
2686
|
-
exports.
|
|
2414
|
+
exports.Resolver = Resolver;
|
|
2687
2415
|
exports.cliReporter = cliReporter;
|
|
2688
2416
|
exports.createAdapter = createAdapter;
|
|
2689
2417
|
exports.createKubb = createKubb;
|
|
2690
2418
|
exports.createRenderer = createRenderer;
|
|
2691
2419
|
exports.createReporter = createReporter;
|
|
2692
|
-
exports.
|
|
2420
|
+
exports.createResolver = createResolver;
|
|
2421
|
+
exports.createStorage = createStorage;
|
|
2693
2422
|
exports.defineGenerator = defineGenerator;
|
|
2694
2423
|
exports.defineParser = defineParser;
|
|
2695
2424
|
exports.definePlugin = definePlugin;
|
|
2696
|
-
exports.defineResolver = defineResolver;
|
|
2697
2425
|
exports.fileReporter = fileReporter;
|
|
2698
2426
|
exports.fsStorage = fsStorage;
|
|
2699
2427
|
exports.jsonReporter = jsonReporter;
|
|
2700
2428
|
exports.logLevel = logLevel;
|
|
2701
|
-
exports.memoryStorage =
|
|
2429
|
+
exports.memoryStorage = memoryStorage;
|
|
2702
2430
|
|
|
2703
2431
|
//# sourceMappingURL=index.cjs.map
|