@kubb/core 5.10.3 → 5.11.0
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/{Hookable-BsWZ8pHO.js → Hookable-COETyXWW.js} +12 -6
- package/dist/Hookable-COETyXWW.js.map +1 -0
- package/dist/{createAdapter-B5kKMsWV.d.cts → createAdapter-D6osJ_0O.d.ts} +12 -3
- package/dist/index.cjs +2924 -114
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3157 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mocks.d.ts +1 -1
- package/dist/mocks.js +1 -1
- package/package.json +5 -11
- package/dist/Hookable-BsWZ8pHO.js.map +0 -1
- package/dist/Hookable-C4S0gBso.cjs +0 -3030
- package/dist/Hookable-C4S0gBso.cjs.map +0 -1
- package/dist/Hookable-pvEb-nN9.cjs +0 -3044
- package/dist/Hookable-pvEb-nN9.cjs.map +0 -1
- package/dist/createAdapter-B5kKMsWV.d.ts +0 -3147
- package/dist/mocks.cjs +0 -209
- package/dist/mocks.cjs.map +0 -1
- package/dist/mocks.d.cts +0 -107
package/dist/index.cjs
CHANGED
|
@@ -1,22 +1,118 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
3
24
|
let verkit = require("verkit");
|
|
25
|
+
let node_async_hooks = require("node:async_hooks");
|
|
4
26
|
let node_process = require("node:process");
|
|
5
|
-
node_process =
|
|
27
|
+
node_process = __toESM(node_process, 1);
|
|
6
28
|
let tinyexec = require("tinyexec");
|
|
7
29
|
let node_fs_promises = require("node:fs/promises");
|
|
8
30
|
let node_path = require("node:path");
|
|
9
|
-
node_path =
|
|
31
|
+
node_path = __toESM(node_path, 1);
|
|
10
32
|
let node_util = require("node:util");
|
|
33
|
+
let _kubb_ast = require("@kubb/ast");
|
|
11
34
|
let node_crypto = require("node:crypto");
|
|
12
35
|
let node_fs = require("node:fs");
|
|
13
36
|
let node_os = require("node:os");
|
|
14
37
|
let node_readline = require("node:readline");
|
|
15
38
|
let unstorage = require("unstorage");
|
|
16
39
|
let unstorage_drivers_fs = require("unstorage/drivers/fs");
|
|
17
|
-
unstorage_drivers_fs =
|
|
40
|
+
unstorage_drivers_fs = __toESM(unstorage_drivers_fs, 1);
|
|
18
41
|
let node_url = require("node:url");
|
|
19
42
|
let node_module = require("node:module");
|
|
43
|
+
//#region package.json
|
|
44
|
+
var version = "5.11.0";
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region ../../internals/utils/src/casing.ts
|
|
47
|
+
/**
|
|
48
|
+
* Shared implementation for camelCase and PascalCase conversion.
|
|
49
|
+
* Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
|
|
50
|
+
* and capitalizes each word according to `pascal`.
|
|
51
|
+
*
|
|
52
|
+
* When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
|
|
53
|
+
*/
|
|
54
|
+
function toCamelOrPascal(text, pascal) {
|
|
55
|
+
return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
|
|
56
|
+
if (word.length > 1 && word === word.toUpperCase()) return word;
|
|
57
|
+
return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
|
|
58
|
+
}).join("").replace(/[^a-zA-Z0-9]/g, "");
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Converts `text` to camelCase.
|
|
62
|
+
*
|
|
63
|
+
* @example Word boundaries
|
|
64
|
+
* `camelCase('hello-world') // 'helloWorld'`
|
|
65
|
+
*
|
|
66
|
+
* @example With a prefix
|
|
67
|
+
* `camelCase('tag', { prefix: 'create' }) // 'createTag'`
|
|
68
|
+
*/
|
|
69
|
+
function camelCase(text, { prefix = "", suffix = "" } = {}) {
|
|
70
|
+
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region ../../internals/utils/src/errors.ts
|
|
74
|
+
/**
|
|
75
|
+
* Coerces an unknown thrown value to an `Error` instance.
|
|
76
|
+
* Returns the value as-is when it is already an `Error`; otherwise wraps it with `String(value)`.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* try { ... } catch(err) {
|
|
81
|
+
* throw new Error('Build failed', { cause: toError(err) })
|
|
82
|
+
* }
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
function toError(value) {
|
|
86
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Extracts a human-readable message from any thrown value.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* getErrorMessage(new Error('oops')) // 'oops'
|
|
94
|
+
* getErrorMessage('plain string') // 'plain string'
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
function getErrorMessage(value) {
|
|
98
|
+
return value instanceof Error ? value.message : String(value);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Returns `error` followed by every error in its `cause` chain, outermost first. Stops at a
|
|
102
|
+
* non-`Error` cause or a cycle.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* getErrorChain(new Error('Config failed loading', { cause: new Error('boom') })).map((error) => error.message)
|
|
107
|
+
* // ['Config failed loading', 'boom']
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
function getErrorChain(error) {
|
|
111
|
+
const chain = [];
|
|
112
|
+
for (let current = error; current instanceof Error && !chain.includes(current); current = current.cause) chain.push(current);
|
|
113
|
+
return chain;
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
20
116
|
//#region ../../internals/utils/src/isCIEnvironment.ts
|
|
21
117
|
/**
|
|
22
118
|
* Returns `true` when a known CI provider has set its environment variable.
|
|
@@ -27,6 +123,20 @@ function isCIEnvironment() {
|
|
|
27
123
|
return !!(ci && ci !== "false" && ci !== "0" || node_process.default.env.GITHUB_ACTIONS || node_process.default.env.GITLAB_CI || node_process.default.env.BITBUCKET_BUILD_NUMBER || node_process.default.env.JENKINS_URL || node_process.default.env.CIRCLECI || node_process.default.env.TRAVIS || node_process.default.env.TEAMCITY_VERSION || node_process.default.env.BUILDKITE || node_process.default.env.TF_BUILD);
|
|
28
124
|
}
|
|
29
125
|
//#endregion
|
|
126
|
+
//#region ../../internals/utils/src/jsonPointer.ts
|
|
127
|
+
/**
|
|
128
|
+
* Escapes a single JSON pointer reference token per RFC 6901 (`~` → `~0`, `/` → `~1`), so a
|
|
129
|
+
* name holding those characters maps to a distinct pointer.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* escapePointerToken('/pets/{id}') // '~1pets~1{id}'
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
function escapePointerToken(token) {
|
|
137
|
+
return token.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
30
140
|
//#region ../../internals/utils/src/openInBrowser.ts
|
|
31
141
|
const ignore = () => {};
|
|
32
142
|
/**
|
|
@@ -46,6 +156,172 @@ function openInBrowser(target) {
|
|
|
46
156
|
(0, tinyexec.x)(node_process.default.platform === "darwin" ? "open" : "xdg-open", [target]).then(ignore, ignore);
|
|
47
157
|
}
|
|
48
158
|
//#endregion
|
|
159
|
+
//#region ../../internals/utils/src/runtime.ts
|
|
160
|
+
const isBun = typeof Bun !== "undefined";
|
|
161
|
+
const isDeno = "Deno" in globalThis;
|
|
162
|
+
/**
|
|
163
|
+
* The JavaScript runtime executing the current process: its name, version and whether it is Bun.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```ts
|
|
167
|
+
* runtime.name // 'bun' when run with `bun kubb`, 'node' otherwise
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
const runtime = {
|
|
171
|
+
isBun,
|
|
172
|
+
...detect()
|
|
173
|
+
};
|
|
174
|
+
function detect() {
|
|
175
|
+
if (isBun) return {
|
|
176
|
+
name: "bun",
|
|
177
|
+
version: process.versions.bun ?? ""
|
|
178
|
+
};
|
|
179
|
+
if (isDeno) return {
|
|
180
|
+
name: "deno",
|
|
181
|
+
version: globalThis.Deno?.version?.deno ?? ""
|
|
182
|
+
};
|
|
183
|
+
return {
|
|
184
|
+
name: "node",
|
|
185
|
+
version: process.versions?.node ?? ""
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region ../../internals/utils/src/fs.ts
|
|
190
|
+
/**
|
|
191
|
+
* Reads the file at `path` as a UTF-8 string.
|
|
192
|
+
* Uses `Bun.file().text()` when running under Bun, `fs.readFile` otherwise.
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```ts
|
|
196
|
+
* const source = await read('./src/Pet.ts')
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
async function read(path) {
|
|
200
|
+
if (runtime.isBun) return Bun.file(path).text();
|
|
201
|
+
return (0, node_fs_promises.readFile)(path, { encoding: "utf8" });
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Whether `stored` already holds `source`, comparing on the trimmed text rather than the exact
|
|
205
|
+
* bytes. Surrounding whitespace is what a formatter adds and what editors strip, and neither is a
|
|
206
|
+
* reason to rewrite the file.
|
|
207
|
+
*
|
|
208
|
+
* Both sides are trimmed, so a storage that keeps bytes verbatim settles on the same answer as one
|
|
209
|
+
* that normalizes what it stores. Trimming only `stored` would leave a source with leading
|
|
210
|
+
* whitespace rewritten on every build, since the stored copy keeps the whitespace the comparison
|
|
211
|
+
* has already dropped.
|
|
212
|
+
*/
|
|
213
|
+
function matchesStored({ stored, source }) {
|
|
214
|
+
return stored.trim() === source.trim();
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Writes `data` to `path`, trimming surrounding whitespace and ending the file with a single newline
|
|
218
|
+
* the way prettier, biome, and oxfmt all do.
|
|
219
|
+
* Skips the write when the trimmed content is empty, or when the file already holds that content.
|
|
220
|
+
* Creates any missing parent directories automatically.
|
|
221
|
+
* When `sanity` is `true`, re-reads the file after writing and throws if the content does not match.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```ts
|
|
225
|
+
* await write('./src/Pet.ts', source) // writes and returns the trimmed content plus a newline
|
|
226
|
+
* await write('./src/Pet.ts', source) // null — file unchanged
|
|
227
|
+
* await write('./src/Pet.ts', ' ') // null — empty content skipped
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
async function write(path, data, options = {}) {
|
|
231
|
+
const trimmed = data.trim();
|
|
232
|
+
if (trimmed === "") return null;
|
|
233
|
+
const content = `${trimmed}\n`;
|
|
234
|
+
const resolved = (0, node_path.resolve)(path);
|
|
235
|
+
let stored = options.stored;
|
|
236
|
+
if (stored === void 0) {
|
|
237
|
+
if (runtime.isBun) {
|
|
238
|
+
const file = Bun.file(resolved);
|
|
239
|
+
stored = await file.exists() ? await file.text() : null;
|
|
240
|
+
} else try {
|
|
241
|
+
stored = await (0, node_fs_promises.readFile)(resolved, { encoding: "utf-8" });
|
|
242
|
+
} catch {
|
|
243
|
+
stored = null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (matchesStored({
|
|
247
|
+
stored: stored ?? "",
|
|
248
|
+
source: trimmed
|
|
249
|
+
})) return null;
|
|
250
|
+
if (runtime.isBun) {
|
|
251
|
+
await Bun.write(resolved, content);
|
|
252
|
+
return content;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
await (0, node_fs_promises.writeFile)(resolved, content, { encoding: "utf-8" });
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (error.code !== "ENOENT") throw error;
|
|
258
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(resolved), { recursive: true });
|
|
259
|
+
await (0, node_fs_promises.writeFile)(resolved, content, { encoding: "utf-8" });
|
|
260
|
+
}
|
|
261
|
+
if (options.sanity) {
|
|
262
|
+
const savedData = await (0, node_fs_promises.readFile)(resolved, { encoding: "utf-8" });
|
|
263
|
+
if (savedData !== content) throw new Error(`Sanity check failed for ${path}\n\nData[${data.length}]:\n${data}\n\nSaved[${savedData.length}]:\n${savedData}\n`);
|
|
264
|
+
return savedData;
|
|
265
|
+
}
|
|
266
|
+
return content;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Converts a filesystem path to use POSIX (`/`) separators.
|
|
270
|
+
*
|
|
271
|
+
* Most of the codebase compares and composes paths as strings (prefix matching, joining for
|
|
272
|
+
* import specifiers, splitting on `/`). On POSIX `path.resolve` already returns `/`-separated
|
|
273
|
+
* paths, but on Windows it returns `\`-separated paths, which breaks every such comparison.
|
|
274
|
+
*
|
|
275
|
+
* Routing every path that crosses a module boundary through `toPosixPath` keeps the rest of the
|
|
276
|
+
* code platform-agnostic. The conversion runs unconditionally so Windows-specific behavior is
|
|
277
|
+
* exercisable from POSIX CI.
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* toPosixPath('C:\\repo\\src\\pet.ts') // 'C:/repo/src/pet.ts'
|
|
281
|
+
*/
|
|
282
|
+
function toPosixPath(filePath) {
|
|
283
|
+
return filePath.replaceAll("\\", "/");
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Resolves to `true` when `path` is `parent` itself or nested inside it. Both sides are resolved
|
|
287
|
+
* to absolute paths first, so relative and `..`-containing inputs compare correctly.
|
|
288
|
+
*
|
|
289
|
+
* Guards a destructive or an out-of-tree operation: before wiping an output directory, check that
|
|
290
|
+
* it does not contain the project root, and before loading a path a caller supplied, check that it
|
|
291
|
+
* did not escape the directory it is allowed to read from.
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* isPathInside('./src/gen', '.') // true — nested inside the root
|
|
295
|
+
* isPathInside('.', '.') // true — the same directory counts as inside
|
|
296
|
+
* isPathInside('.', './src/gen') // false — the root is not inside its own output
|
|
297
|
+
* isPathInside('../other', '.') // false — escapes the root
|
|
298
|
+
*/
|
|
299
|
+
function isPathInside(path, parent) {
|
|
300
|
+
const resolvedPath = (0, node_path.resolve)(path);
|
|
301
|
+
const resolvedParent = (0, node_path.resolve)(parent);
|
|
302
|
+
if (resolvedPath === resolvedParent) return true;
|
|
303
|
+
const rel = (0, node_path.relative)(resolvedParent, resolvedPath);
|
|
304
|
+
return rel !== "" && !rel.startsWith("..") && !(0, node_path.isAbsolute)(rel);
|
|
305
|
+
}
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region ../../internals/utils/src/promise.ts
|
|
308
|
+
/**
|
|
309
|
+
* Runs `run` over every item with at most `limit` in flight. Workers share one iterator, so each
|
|
310
|
+
* takes the next item the moment it frees up instead of waiting for a batch to drain.
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* ```ts
|
|
314
|
+
* await inParallel({ items: files, limit: 50, run: (file) => storage.writeItem(file.path, file.source) })
|
|
315
|
+
* ```
|
|
316
|
+
*/
|
|
317
|
+
async function inParallel({ items, limit, run }) {
|
|
318
|
+
const queue = items.entries();
|
|
319
|
+
const worker = async () => {
|
|
320
|
+
for (const [index, item] of queue) await run(item, index);
|
|
321
|
+
};
|
|
322
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
|
|
323
|
+
}
|
|
324
|
+
//#endregion
|
|
49
325
|
//#region ../../internals/utils/src/tools.ts
|
|
50
326
|
/**
|
|
51
327
|
* CLI command descriptors for each supported code formatter.
|
|
@@ -104,72 +380,2499 @@ const linters = {
|
|
|
104
380
|
}
|
|
105
381
|
};
|
|
106
382
|
/**
|
|
107
|
-
* Preference order for `format: 'auto'`, most-preferred first. Spelled out rather than taken from
|
|
108
|
-
* the table's key order, which is arbitrary and would silently change what `auto` picks.
|
|
383
|
+
* Preference order for `format: 'auto'`, most-preferred first. Spelled out rather than taken from
|
|
384
|
+
* the table's key order, which is arbitrary and would silently change what `auto` picks.
|
|
385
|
+
*/
|
|
386
|
+
const FORMATTER_PREFERENCE = [
|
|
387
|
+
"oxfmt",
|
|
388
|
+
"biome",
|
|
389
|
+
"prettier"
|
|
390
|
+
];
|
|
391
|
+
/**
|
|
392
|
+
* Preference order for `lint: 'auto'`, most-preferred first.
|
|
393
|
+
*/
|
|
394
|
+
const LINTER_PREFERENCE = [
|
|
395
|
+
"oxlint",
|
|
396
|
+
"biome",
|
|
397
|
+
"eslint"
|
|
398
|
+
];
|
|
399
|
+
/**
|
|
400
|
+
* Whether `name` is on PATH or in a `node_modules/.bin` above `cwd` and answers `--version` with a zero exit.
|
|
401
|
+
*/
|
|
402
|
+
async function isToolAvailable(name, cwd) {
|
|
403
|
+
return (0, tinyexec.x)(name, ["--version"], {
|
|
404
|
+
throwOnError: false,
|
|
405
|
+
nodeOptions: {
|
|
406
|
+
cwd,
|
|
407
|
+
stdio: "ignore"
|
|
408
|
+
}
|
|
409
|
+
}).then(({ exitCode }) => exitCode === 0, () => false);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Returns the first installed executable from `candidates`, or `null` when none are found.
|
|
413
|
+
*
|
|
414
|
+
* Not memoized: a long-running host that probes repeatedly should cache the result itself, and a
|
|
415
|
+
* `--watch` build should keep noticing a tool installed mid-session.
|
|
416
|
+
*/
|
|
417
|
+
async function detectTool(candidates, cwd) {
|
|
418
|
+
for (const candidate of candidates) if (await isToolAvailable(candidate, cwd)) return candidate;
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Tokenizes a shell command string, respecting single and double quotes.
|
|
423
|
+
*
|
|
424
|
+
* @example
|
|
425
|
+
* ```ts
|
|
426
|
+
* tokenize('git commit -m "initial commit"')
|
|
427
|
+
* // → ['git', 'commit', '-m', 'initial commit']
|
|
428
|
+
* ```
|
|
429
|
+
*/
|
|
430
|
+
function tokenize(command) {
|
|
431
|
+
return (command.match(/[^\s"']+|"([^"]*)"|'([^']*)'/g) ?? []).map((token) => token.replace(/^["']|["']$/g, ""));
|
|
432
|
+
}
|
|
433
|
+
//#endregion
|
|
434
|
+
//#region ../../internals/utils/src/time.ts
|
|
435
|
+
/**
|
|
436
|
+
* Calculates elapsed time in milliseconds from a high-resolution `process.hrtime` start time.
|
|
437
|
+
* Rounds to 2 decimal places for sub-millisecond precision without noise.
|
|
438
|
+
*
|
|
439
|
+
* @example
|
|
440
|
+
* ```ts
|
|
441
|
+
* const start = process.hrtime()
|
|
442
|
+
* doWork()
|
|
443
|
+
* getElapsedMs(start) // 42.35
|
|
444
|
+
* ```
|
|
445
|
+
*/
|
|
446
|
+
function getElapsedMs(hrStart) {
|
|
447
|
+
const [seconds, nanoseconds] = process.hrtime(hrStart);
|
|
448
|
+
const ms = seconds * 1e3 + nanoseconds / 1e6;
|
|
449
|
+
return Math.round(ms * 100) / 100;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Converts a millisecond duration into a human-readable string (`ms`, `s`, or `m s`).
|
|
453
|
+
*
|
|
454
|
+
* @example
|
|
455
|
+
* ```ts
|
|
456
|
+
* formatMs(250) // '250ms'
|
|
457
|
+
* formatMs(1500) // '1.50s'
|
|
458
|
+
* formatMs(90000) // '1m 30.0s'
|
|
459
|
+
* ```
|
|
460
|
+
*/
|
|
461
|
+
function formatMs(ms) {
|
|
462
|
+
if (ms >= 6e4) return `${Math.floor(ms / 6e4)}m ${(ms % 6e4 / 1e3).toFixed(1)}s`;
|
|
463
|
+
if (ms >= 1e3) return `${(ms / 1e3).toFixed(2)}s`;
|
|
464
|
+
return `${Math.round(ms)}ms`;
|
|
465
|
+
}
|
|
466
|
+
//#endregion
|
|
467
|
+
//#region src/diagnosticCatalog.ts
|
|
468
|
+
/**
|
|
469
|
+
* Every diagnostic code with its docs entry: a short title, what triggers it, and how to resolve
|
|
470
|
+
* it. The source of truth for {@link diagnosticCode}, and the kubb.dev `/reference/diagnostics`
|
|
471
|
+
* sections mirror it.
|
|
472
|
+
*/
|
|
473
|
+
const diagnosticCatalog = {
|
|
474
|
+
unknown: {
|
|
475
|
+
code: "KUBB_UNKNOWN",
|
|
476
|
+
title: "Unknown error",
|
|
477
|
+
cause: "An error was thrown without a stable Kubb code, so it is reported as-is.",
|
|
478
|
+
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."
|
|
479
|
+
},
|
|
480
|
+
inputNotFound: {
|
|
481
|
+
code: "KUBB_INPUT_NOT_FOUND",
|
|
482
|
+
title: "Input not found",
|
|
483
|
+
cause: "The file set as `input` (or passed as `kubb generate PATH`) could not be read. A URL reports `KUBB_INPUT_REQUEST_FAILED` or `KUBB_INPUT_UNREACHABLE` instead.",
|
|
484
|
+
fix: "Check that the path exists and is readable, then set it as `input` or pass it on the CLI."
|
|
485
|
+
},
|
|
486
|
+
inputRequestFailed: {
|
|
487
|
+
code: "KUBB_INPUT_REQUEST_FAILED",
|
|
488
|
+
title: "Input request failed",
|
|
489
|
+
cause: "A URL set as `input` (or reached through a `$ref`) answered with a 4xx or 5xx status instead of the document.",
|
|
490
|
+
fix: "Open the URL to see what the server returns. A 401 or 403 needs credentials Kubb does not send, so download the document and point `input` at the local file. A 404 means the path is wrong, and a 5xx means the server itself failed."
|
|
491
|
+
},
|
|
492
|
+
inputUnreachable: {
|
|
493
|
+
code: "KUBB_INPUT_UNREACHABLE",
|
|
494
|
+
title: "Input unreachable",
|
|
495
|
+
cause: "A URL set as `input` (or reached through a `$ref`) never answered, so the request failed before a status came back. A refused connection, an unknown host, an expired certificate, and a timeout all land here.",
|
|
496
|
+
fix: "Check that the host is running and reachable from this machine. For a local server, start it and confirm the port matches the one in `input`."
|
|
497
|
+
},
|
|
498
|
+
inputRequired: {
|
|
499
|
+
code: "KUBB_INPUT_REQUIRED",
|
|
500
|
+
title: "Input required",
|
|
501
|
+
cause: "An adapter is configured but no `input` was provided.",
|
|
502
|
+
fix: "Set `input` to a file path, a URL, an inline spec (JSON/YAML string), or a parsed object in your Kubb config."
|
|
503
|
+
},
|
|
504
|
+
legacyInput: {
|
|
505
|
+
code: "KUBB_LEGACY_INPUT",
|
|
506
|
+
title: "Legacy input shape",
|
|
507
|
+
cause: "`input` is a `{ path }` or `{ data }` wrapper, which v4 used to point at a document and v5 reads as the document itself.",
|
|
508
|
+
fix: "Unwrap it: `input: { path: \"./petStore.yaml\" }` becomes `input: \"./petStore.yaml\"`, and `input: { data: spec }` becomes `input: spec`."
|
|
509
|
+
},
|
|
510
|
+
invalidDocument: {
|
|
511
|
+
code: "KUBB_INVALID_DOCUMENT",
|
|
512
|
+
title: "Invalid document",
|
|
513
|
+
cause: "The `input` cannot be read as an OpenAPI or Swagger document: its JSON or YAML has a syntax error, it declares no `openapi` or `swagger` version field, or its version cannot be upgraded to OpenAPI 3.1.",
|
|
514
|
+
fix: "Fix the syntax at the reported line, and check that `input` points at the spec and not an HTML page. Point `input` at a Swagger 2.0, OpenAPI 3.0, or OpenAPI 3.1 document, and check that a passed object is the spec itself rather than a wrapper around it."
|
|
515
|
+
},
|
|
516
|
+
invalidSpec: {
|
|
517
|
+
code: "KUBB_INVALID_SPEC",
|
|
518
|
+
title: "Invalid spec",
|
|
519
|
+
cause: "The document does not pass OpenAPI schema validation. Kubb still generates from it, but the output can be incomplete or wrong.",
|
|
520
|
+
fix: "Fix the reported problem in the spec. Run `kubb validate` to list every error, or set `validate: false` on the adapter to skip the check."
|
|
521
|
+
},
|
|
522
|
+
refNotFound: {
|
|
523
|
+
code: "KUBB_REF_NOT_FOUND",
|
|
524
|
+
title: "Reference not found",
|
|
525
|
+
cause: "A `$ref` could not be resolved in the source document.",
|
|
526
|
+
fix: "Add the missing definition (for example under `components.schemas`) or fix the `$ref`. Run `kubb validate` to check the spec."
|
|
527
|
+
},
|
|
528
|
+
invalidServerVariable: {
|
|
529
|
+
code: "KUBB_INVALID_SERVER_VARIABLE",
|
|
530
|
+
title: "Invalid server variable",
|
|
531
|
+
cause: "A server variable value is not allowed by its `enum`.",
|
|
532
|
+
fix: "Use one of the values listed in the server variable `enum`, or update the spec."
|
|
533
|
+
},
|
|
534
|
+
pluginNotFound: {
|
|
535
|
+
code: "KUBB_PLUGIN_NOT_FOUND",
|
|
536
|
+
title: "Plugin not found",
|
|
537
|
+
cause: "A plugin that another plugin depends on is missing from the config.",
|
|
538
|
+
fix: "Add the required plugin to the `plugins` array in kubb.config.ts, or remove the dependency on it."
|
|
539
|
+
},
|
|
540
|
+
incompatibleExtension: {
|
|
541
|
+
code: "KUBB_INCOMPATIBLE_EXTENSION",
|
|
542
|
+
title: "Incompatible extension",
|
|
543
|
+
cause: "A plugin sets `meta.compatibility.kubb` or `meta.compatibility.node` to a range the running Kubb or Node.js version does not satisfy.",
|
|
544
|
+
fix: "Upgrade Kubb or Node.js to a version in the range, or install a version of the plugin that supports your current Kubb and Node.js."
|
|
545
|
+
},
|
|
546
|
+
missingDependency: {
|
|
547
|
+
code: "KUBB_MISSING_DEPENDENCY",
|
|
548
|
+
title: "Missing dependency",
|
|
549
|
+
cause: "A plugin lists another plugin in its `dependencies` record without `optional: true`, and the config does not include it.",
|
|
550
|
+
fix: "Add the missing plugin to the `plugins` array in kubb.config.ts."
|
|
551
|
+
},
|
|
552
|
+
incompatibleDependency: {
|
|
553
|
+
code: "KUBB_INCOMPATIBLE_DEPENDENCY",
|
|
554
|
+
title: "Incompatible dependency",
|
|
555
|
+
cause: "A plugin's `dependencies` record sets a `version` range, and the configured plugin's `meta.version` falls outside it.",
|
|
556
|
+
fix: "Install a version of the dependency inside the range, or a version of the dependent plugin that supports the installed dependency."
|
|
557
|
+
},
|
|
558
|
+
pluginFailed: {
|
|
559
|
+
code: "KUBB_PLUGIN_FAILED",
|
|
560
|
+
title: "Plugin failed",
|
|
561
|
+
cause: "A plugin threw while generating, or reported an error through `ctx.error`.",
|
|
562
|
+
fix: "Check the plugin options and the node at `at:`. If it is a bug, report it with the stack at `report:`."
|
|
563
|
+
},
|
|
564
|
+
adapterFailed: {
|
|
565
|
+
code: "KUBB_ADAPTER_FAILED",
|
|
566
|
+
title: "Adapter failed",
|
|
567
|
+
cause: "The adapter threw an unexpected error while parsing the input.",
|
|
568
|
+
fix: "Check the input and the adapter options. If it is a bug, report it with the stack at `report:`."
|
|
569
|
+
},
|
|
570
|
+
parserFailed: {
|
|
571
|
+
code: "KUBB_PARSER_FAILED",
|
|
572
|
+
title: "Parser failed",
|
|
573
|
+
cause: "A parser threw an unexpected error while printing a generated file.",
|
|
574
|
+
fix: "Check the parser options. If it is a bug, report it with the stack at `report:`."
|
|
575
|
+
},
|
|
576
|
+
pluginWarning: {
|
|
577
|
+
code: "KUBB_PLUGIN_WARNING",
|
|
578
|
+
title: "Plugin warning",
|
|
579
|
+
cause: "A plugin reported a non-fatal warning through `ctx.warn`.",
|
|
580
|
+
fix: "Review the message. It does not fail the build; adjust the plugin options or input if the warning is unwanted."
|
|
581
|
+
},
|
|
582
|
+
pluginInfo: {
|
|
583
|
+
code: "KUBB_PLUGIN_INFO",
|
|
584
|
+
title: "Plugin info",
|
|
585
|
+
cause: "A plugin reported an informational message through `ctx.info`.",
|
|
586
|
+
fix: "Informational only. No action is required."
|
|
587
|
+
},
|
|
588
|
+
noContext: {
|
|
589
|
+
code: "KUBB_NO_CONTEXT",
|
|
590
|
+
title: "No plugin context",
|
|
591
|
+
cause: "A `kubb/kit` composable such as `useKubb()` or `addGenerator()` ran outside the `setup` and `kubb:plugin:setup` of a plugin, so there is no plugin to apply it to.",
|
|
592
|
+
fix: "Call the composable inside `setup` or `kubb:plugin:setup`, including in code that setup awaits."
|
|
593
|
+
},
|
|
594
|
+
unsupportedFormat: {
|
|
595
|
+
code: "KUBB_UNSUPPORTED_FORMAT",
|
|
596
|
+
title: "Unsupported format",
|
|
597
|
+
cause: "A schema uses a `format` Kubb does not map to a specific type, so it falls back to the base type.",
|
|
598
|
+
fix: "Use a format Kubb supports, or handle the custom format with a parser or plugin."
|
|
599
|
+
},
|
|
600
|
+
deprecated: {
|
|
601
|
+
code: "KUBB_DEPRECATED",
|
|
602
|
+
title: "Deprecated",
|
|
603
|
+
cause: "A referenced schema or operation is marked `deprecated`.",
|
|
604
|
+
fix: "Migrate off the deprecated definition if the warning is unwanted."
|
|
605
|
+
},
|
|
606
|
+
adapterRequired: {
|
|
607
|
+
code: "KUBB_ADAPTER_REQUIRED",
|
|
608
|
+
title: "Adapter required",
|
|
609
|
+
cause: "An action needs an adapter but none is configured.",
|
|
610
|
+
fix: "Set `adapter` in kubb.config.ts, for example `adapterOas()`."
|
|
611
|
+
},
|
|
612
|
+
pathTraversal: {
|
|
613
|
+
code: "KUBB_PATH_TRAVERSAL",
|
|
614
|
+
title: "Path traversal",
|
|
615
|
+
cause: "A resolved output path escaped the output directory, which can stem from a path traversal in the spec or a misconfigured `group.name`.",
|
|
616
|
+
fix: "Keep generated paths within the output directory. Review the `group.name` function and the names coming from the spec."
|
|
617
|
+
},
|
|
618
|
+
cleanRoot: {
|
|
619
|
+
code: "KUBB_CLEAN_ROOT",
|
|
620
|
+
title: "Clean targets the project root",
|
|
621
|
+
cause: "`output.clean` is enabled and `output.path` resolves to the project root or a parent of it, so cleaning would delete `kubb.config` and every source file.",
|
|
622
|
+
fix: "Point `output.path` at a subdirectory such as `./src/gen` so clean only removes generated code, or disable `output.clean`."
|
|
623
|
+
},
|
|
624
|
+
invalidPluginOptions: {
|
|
625
|
+
code: "KUBB_INVALID_PLUGIN_OPTIONS",
|
|
626
|
+
title: "Invalid plugin options",
|
|
627
|
+
cause: "A plugin was configured with options that cannot be honored, for example `output.mode: 'file'` paired with a `group` option.",
|
|
628
|
+
fix: "Fix the plugin options. A single-file output has nothing to group, so remove the `group` option or use `output.mode: 'directory'`."
|
|
629
|
+
},
|
|
630
|
+
postGenerateFailed: {
|
|
631
|
+
code: "KUBB_POST_GENERATE_FAILED",
|
|
632
|
+
title: "Post-generate command failed",
|
|
633
|
+
cause: "A post-generate command (`output.postGenerate`) exited with a non-zero status.",
|
|
634
|
+
fix: "Check the command is installed and correct, and run it manually to see the error."
|
|
635
|
+
},
|
|
636
|
+
formatFailed: {
|
|
637
|
+
code: "KUBB_FORMAT_FAILED",
|
|
638
|
+
title: "Format failed",
|
|
639
|
+
cause: "The formatter pass over the generated files failed.",
|
|
640
|
+
fix: "Check the formatter (oxfmt, biome, or prettier) is installed and its config is valid, then run it manually on the output."
|
|
641
|
+
},
|
|
642
|
+
lintFailed: {
|
|
643
|
+
code: "KUBB_LINT_FAILED",
|
|
644
|
+
title: "Lint failed",
|
|
645
|
+
cause: "The linter pass over the generated files failed.",
|
|
646
|
+
fix: "Check the linter (oxlint, biome, or eslint) is installed and its config is valid, then run it manually on the output."
|
|
647
|
+
},
|
|
648
|
+
barrelDuplicateExport: {
|
|
649
|
+
code: "KUBB_BARREL_DUPLICATE_EXPORT",
|
|
650
|
+
title: "Duplicate barrel export",
|
|
651
|
+
cause: "Two generated modules export the same name, so the barrel cannot re-export both.",
|
|
652
|
+
fix: "Rename one of the colliding declarations, or configure the plugin resolver to produce distinct names."
|
|
653
|
+
},
|
|
654
|
+
internalError: {
|
|
655
|
+
code: "KUBB_INTERNAL_ERROR",
|
|
656
|
+
title: "Internal error",
|
|
657
|
+
cause: "Kubb hit a state it does not expect. This is a bug in Kubb, not in your config.",
|
|
658
|
+
fix: "Rerun with `--reporter json` or `--verbose` to get the stack. Then report it at https://github.com/kubb-labs/kubb/issues with the spec and config that trigger it."
|
|
659
|
+
},
|
|
660
|
+
performance: {
|
|
661
|
+
code: "KUBB_PERFORMANCE",
|
|
662
|
+
title: "Performance",
|
|
663
|
+
cause: "Not a failure. Records a plugin’s elapsed time, summed into the run total.",
|
|
664
|
+
fix: "No action. This is an informational metric."
|
|
665
|
+
},
|
|
666
|
+
updateAvailable: {
|
|
667
|
+
code: "KUBB_UPDATE_AVAILABLE",
|
|
668
|
+
title: "Update available",
|
|
669
|
+
cause: "A newer Kubb version is published on npm than the one running.",
|
|
670
|
+
fix: "Update the `@kubb/*` packages, for example `npm install -g @kubb/cli`, to get the latest fixes."
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
/**
|
|
674
|
+
* Stable codes Kubb attaches to a `Diagnostic`, derived from {@link diagnosticCatalog}. Reference
|
|
675
|
+
* these instead of inlining the string at a throw site.
|
|
676
|
+
*/
|
|
677
|
+
const diagnosticCode = Object.fromEntries(Object.entries(diagnosticCatalog).map(([key, { code }]) => [key, code]));
|
|
678
|
+
//#endregion
|
|
679
|
+
//#region src/formatDiagnostic.ts
|
|
680
|
+
/** A problem's `help`, or the catalog fix. `info` gets none: its catalog fix says no action is needed. */
|
|
681
|
+
function getHelp(problem) {
|
|
682
|
+
if (problem.help || problem.severity === "info") return problem.help;
|
|
683
|
+
return Diagnostics.explain(problem.code).fix;
|
|
684
|
+
}
|
|
685
|
+
/** The `cause` chain's messages, outermost first, minus those `message` already contains. */
|
|
686
|
+
function getCauses(problem) {
|
|
687
|
+
return getErrorChain(problem.cause).filter((error) => !problem.message.includes(error.message)).map((error) => error.name === "Error" ? error.message : `${error.name}: ${error.message}`);
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Accent color per severity, which paints the icon in the `human` layout (red error, yellow
|
|
691
|
+
* warning, blue info). The code itself stays dim.
|
|
692
|
+
*/
|
|
693
|
+
const severityStyle = {
|
|
694
|
+
error: "red",
|
|
695
|
+
warning: "yellow",
|
|
696
|
+
info: "blue"
|
|
697
|
+
};
|
|
698
|
+
const severityIcon = {
|
|
699
|
+
error: "✖",
|
|
700
|
+
warning: "⚠",
|
|
701
|
+
info: "ℹ"
|
|
702
|
+
};
|
|
703
|
+
/**
|
|
704
|
+
* Codes for a crash, which carry the root cause's stack.
|
|
705
|
+
*/
|
|
706
|
+
const crashCodes = /* @__PURE__ */ new Set([
|
|
707
|
+
diagnosticCode.unknown,
|
|
708
|
+
diagnosticCode.internalError,
|
|
709
|
+
diagnosticCode.pluginFailed,
|
|
710
|
+
diagnosticCode.adapterFailed,
|
|
711
|
+
diagnosticCode.parserFailed
|
|
712
|
+
]);
|
|
713
|
+
/**
|
|
714
|
+
* Reduces a diagnostic to its JSON-safe fields plus a `docsUrl`, for machine-readable
|
|
715
|
+
* consumers, with the catalog `title`, `cause`, and fix, and the `cause` error as `causes` and
|
|
716
|
+
* `stack`. Absent optional fields are omitted.
|
|
717
|
+
*/
|
|
718
|
+
function serialize(diagnostic) {
|
|
719
|
+
const problem = Diagnostics.isProblem(diagnostic) ? diagnostic : void 0;
|
|
720
|
+
const doc = problem ? Diagnostics.explain(problem.code) : void 0;
|
|
721
|
+
const help = problem ? getHelp(problem) : void 0;
|
|
722
|
+
const causes = problem ? getCauses(problem) : [];
|
|
723
|
+
const stack = problem && crashCodes.has(problem.code) ? getErrorChain(problem.cause).at(-1)?.stack : void 0;
|
|
724
|
+
return {
|
|
725
|
+
code: diagnostic.code,
|
|
726
|
+
severity: diagnostic.severity,
|
|
727
|
+
message: diagnostic.message,
|
|
728
|
+
...doc ? {
|
|
729
|
+
title: doc.title,
|
|
730
|
+
cause: doc.cause
|
|
731
|
+
} : {},
|
|
732
|
+
...problem?.location ? { location: problem.location } : {},
|
|
733
|
+
...help ? { help } : {},
|
|
734
|
+
...problem?.plugin ? { plugin: problem.plugin } : {},
|
|
735
|
+
...problem?.issues ? { issues: problem.issues } : {},
|
|
736
|
+
...causes.length ? { causes } : {},
|
|
737
|
+
...stack ? { stack } : {},
|
|
738
|
+
...diagnostic.code === diagnosticCode.unknown ? {} : { docsUrl: Diagnostics.docsUrl(diagnostic.code) }
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Renders a {@link Diagnostic} as lines: a headline, then indented detail rows. `agent` (the default)
|
|
743
|
+
* prints uncolored labeled rows (`at:`, `why:`, `fix:`, `caused by:`, `stack:` for a crash,
|
|
744
|
+
* `docs:`, `report:`). `human` prints an icon, the bold message, and the code aligned to `width`,
|
|
745
|
+
* with `at`, `fix`, the first cause, and `Learn more` for an error, and one row for a warning.
|
|
746
|
+
* `verbose` adds the full stack.
|
|
747
|
+
*/
|
|
748
|
+
function format(diagnostic, { audience = "agent", verbose = false, width = 0 } = {}) {
|
|
749
|
+
const { code, severity, message } = diagnostic;
|
|
750
|
+
const problem = Diagnostics.isProblem(diagnostic) ? diagnostic : void 0;
|
|
751
|
+
const human = audience === "human";
|
|
752
|
+
const paint = human ? node_util.styleText : (_format, text) => text;
|
|
753
|
+
const [firstLine = "", ...rest] = message.split("\n");
|
|
754
|
+
const pointer = problem?.location && "pointer" in problem.location ? problem.location.pointer : void 0;
|
|
755
|
+
const help = problem ? getHelp(problem) : void 0;
|
|
756
|
+
const causes = problem ? getCauses(problem) : [];
|
|
757
|
+
const crash = problem ? crashCodes.has(problem.code) : false;
|
|
758
|
+
const frames = (problem && (verbose || !human && crash) ? getErrorChain(problem.cause).at(-1)?.stack : void 0)?.split("\n").slice(1, verbose ? void 0 : 4).map((frame) => ` ${paint("dim", frame.trim())}`);
|
|
759
|
+
if (!human) {
|
|
760
|
+
const details = rest.filter((line) => line.trim()).map((line) => ` ${line}`);
|
|
761
|
+
const row = (label, value) => {
|
|
762
|
+
if (value) details.push(` ${label}: ${value}`);
|
|
763
|
+
};
|
|
764
|
+
row("at", pointer);
|
|
765
|
+
if (problem && severity !== "info") row("why", Diagnostics.explain(problem.code).cause);
|
|
766
|
+
row("fix", help);
|
|
767
|
+
for (const cause of causes) row("caused by", cause);
|
|
768
|
+
if (frames?.length) details.push(" stack:", ...frames);
|
|
769
|
+
if (code !== diagnosticCode.unknown) row("docs", Diagnostics.docsUrl(code));
|
|
770
|
+
row("report", problem?.issues);
|
|
771
|
+
return [`${severity} ${code}${problem?.plugin ? ` [${problem.plugin}]` : ""}: ${firstLine}`, ...details];
|
|
772
|
+
}
|
|
773
|
+
const left = `${paint(severityStyle[severity], severityIcon[severity])} ${paint("bold", firstLine)}`;
|
|
774
|
+
const gap = Math.max(2, width - (0, node_util.stripVTControlCharacters)(left).length - code.length);
|
|
775
|
+
const headline = `${left}${" ".repeat(gap)}${paint("dim", code)}`;
|
|
776
|
+
const frame = rest.filter((line) => line.trim()).map((line) => ` ${paint("dim", line)}`);
|
|
777
|
+
const label = (name) => paint("dim", name.length < 4 ? name.padEnd(4) : `${name} `);
|
|
778
|
+
const rows = [["at", pointer && paint("cyan", pointer)], ["fix", help]];
|
|
779
|
+
if (severity !== "error") {
|
|
780
|
+
const first = rows.find(([, value]) => value);
|
|
781
|
+
return [
|
|
782
|
+
headline,
|
|
783
|
+
...frame,
|
|
784
|
+
...first ? [` ${label(first[0])} ${first[1]}`] : []
|
|
785
|
+
];
|
|
786
|
+
}
|
|
787
|
+
rows.push(["caused by", causes[0]], ["report", problem?.issues && paint("cyan", problem.issues)]);
|
|
788
|
+
const details = [...frame, ...rows.flatMap(([name, value]) => value ? [` ${label(name)} ${value}`] : [])];
|
|
789
|
+
if (frames?.length) details.push(` ${paint("dim", "stack")}`, ...frames);
|
|
790
|
+
if (code !== diagnosticCode.unknown) details.push(` ${paint("dim", "↳ Learn more")} ${paint("cyan", Diagnostics.docsUrl(code).replace(/^https:\/\//, ""))}`);
|
|
791
|
+
return [headline, ...details];
|
|
792
|
+
}
|
|
793
|
+
//#endregion
|
|
794
|
+
//#region src/Diagnostics.ts
|
|
795
|
+
/**
|
|
796
|
+
* Docs major version, derived from the package version so the link tracks the published major.
|
|
797
|
+
*/
|
|
798
|
+
const docsMajor = version.split(".")[0] ?? "5";
|
|
799
|
+
/**
|
|
800
|
+
* Builds a type guard that narrows a {@link Diagnostic} to the variant for `kind`. A diagnostic
|
|
801
|
+
* with no `kind` is treated as a `problem`.
|
|
802
|
+
*/
|
|
803
|
+
function isKind(kind) {
|
|
804
|
+
return (diagnostic) => (diagnostic.kind ?? "problem") === kind;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Returns `true` when the diagnostic is a build {@link ProblemDiagnostic}.
|
|
808
|
+
*
|
|
809
|
+
* @example
|
|
810
|
+
* ```ts
|
|
811
|
+
* if (isProblem(diagnostic)) {
|
|
812
|
+
* console.log(diagnostic.location)
|
|
813
|
+
* }
|
|
814
|
+
* ```
|
|
815
|
+
*/
|
|
816
|
+
const isProblem = isKind("problem");
|
|
817
|
+
/**
|
|
818
|
+
* Returns `true` when the diagnostic is a per-plugin {@link PerformanceDiagnostic}.
|
|
819
|
+
*
|
|
820
|
+
* @example
|
|
821
|
+
* ```ts
|
|
822
|
+
* const timings = diagnostics.filter(isPerformance)
|
|
823
|
+
* ```
|
|
824
|
+
*/
|
|
825
|
+
const isPerformance = isKind("performance");
|
|
826
|
+
/**
|
|
827
|
+
* Returns `true` when the diagnostic is a version-update {@link UpdateDiagnostic}.
|
|
828
|
+
*
|
|
829
|
+
* @example
|
|
830
|
+
* ```ts
|
|
831
|
+
* if (isUpdate(diagnostic)) {
|
|
832
|
+
* console.log(diagnostic.latestVersion)
|
|
833
|
+
* }
|
|
834
|
+
* ```
|
|
835
|
+
*/
|
|
836
|
+
const isUpdate = isKind("update");
|
|
837
|
+
const failedCode = {
|
|
838
|
+
plugin: diagnosticCode.pluginFailed,
|
|
839
|
+
adapter: diagnosticCode.adapterFailed,
|
|
840
|
+
parser: diagnosticCode.parserFailed
|
|
841
|
+
};
|
|
842
|
+
const docsByCode = new Map(Object.values(diagnosticCatalog).map((doc) => [doc.code, doc]));
|
|
843
|
+
const countKey = {
|
|
844
|
+
error: "errors",
|
|
845
|
+
warning: "warnings",
|
|
846
|
+
info: "infos"
|
|
847
|
+
};
|
|
848
|
+
/**
|
|
849
|
+
* Static helpers for working with {@link Diagnostic}s, plus the run-scoped sink
|
|
850
|
+
* that lets deep code report a diagnostic without threading a callback.
|
|
851
|
+
*
|
|
852
|
+
* The sink lives in a single `AsyncLocalStorage` in the `@kubb/core` bundle.
|
|
853
|
+
* `Diagnostics.scope` activates it for a run, so anything inside that run (the
|
|
854
|
+
* adapter parse, a generator) reports through `Diagnostics.report` and lands
|
|
855
|
+
* in the same run.
|
|
856
|
+
*/
|
|
857
|
+
var Diagnostics = class Diagnostics {
|
|
858
|
+
static #reporterStorage = new node_async_hooks.AsyncLocalStorage();
|
|
859
|
+
/**
|
|
860
|
+
* The diagnostic code catalog, exposed as `Diagnostics.code` (e.g. `Diagnostics.code.refNotFound`).
|
|
861
|
+
*/
|
|
862
|
+
static code = diagnosticCode;
|
|
863
|
+
/**
|
|
864
|
+
* Type guard for a build {@link ProblemDiagnostic}.
|
|
865
|
+
*/
|
|
866
|
+
static isProblem = isProblem;
|
|
867
|
+
/**
|
|
868
|
+
* Type guard for a version-update {@link UpdateDiagnostic}.
|
|
869
|
+
*/
|
|
870
|
+
static isUpdate = isUpdate;
|
|
871
|
+
/**
|
|
872
|
+
* Type guard for a per-plugin {@link PerformanceDiagnostic}.
|
|
873
|
+
*/
|
|
874
|
+
static isPerformance = isPerformance;
|
|
875
|
+
/**
|
|
876
|
+
* An `Error` that carries a {@link Diagnostic}, so structured problems can flow
|
|
877
|
+
* through the existing throw/catch paths while keeping their code and location.
|
|
878
|
+
*
|
|
879
|
+
* @example
|
|
880
|
+
* ```ts
|
|
881
|
+
* throw new Diagnostics.Error({ code: diagnosticCode.refNotFound, severity: 'error', message: `Could not find ${ref}`, location: { kind: 'schema', pointer: ref, ref } })
|
|
882
|
+
* ```
|
|
883
|
+
*/
|
|
884
|
+
static Error = class DiagnosticError extends Error {
|
|
885
|
+
diagnostic;
|
|
886
|
+
constructor(diagnostic) {
|
|
887
|
+
super(diagnostic.message, { cause: diagnostic.cause });
|
|
888
|
+
this.name = "DiagnosticError";
|
|
889
|
+
this.diagnostic = diagnostic;
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
/**
|
|
893
|
+
* Structural check for a {@link Diagnostics.Error}, including one thrown from a duplicated
|
|
894
|
+
* `@kubb/core` copy where `instanceof` fails. Matches on the `name` and a `diagnostic`
|
|
895
|
+
* that carries a `code`.
|
|
896
|
+
*/
|
|
897
|
+
static isError(error) {
|
|
898
|
+
if (error instanceof Diagnostics.Error) return true;
|
|
899
|
+
if (!(error instanceof Error) || error.name !== "DiagnosticError") return false;
|
|
900
|
+
const { diagnostic } = error;
|
|
901
|
+
return typeof diagnostic === "object" && diagnostic !== null && typeof diagnostic.code === "string";
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Runs `fn` with `sink` as the active diagnostic sink for the whole async
|
|
905
|
+
* subtree, so {@link Diagnostics.report} reaches it from anywhere inside.
|
|
906
|
+
*/
|
|
907
|
+
static scope(sink, fn) {
|
|
908
|
+
return Diagnostics.#reporterStorage.run(sink, fn);
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Collects a diagnostic into the active build via the run-scoped sink, without throwing.
|
|
912
|
+
* Returns `true` when a run consumed it, `false` when called outside a {@link Diagnostics.scope}
|
|
913
|
+
* (so callers can fall back to throwing). Use a `warning`/`info` severity for non-fatal issues.
|
|
914
|
+
* For rendering a diagnostic live on the hook bus, use {@link Diagnostics.emit} instead.
|
|
915
|
+
*/
|
|
916
|
+
static report(diagnostic) {
|
|
917
|
+
const sink = Diagnostics.#reporterStorage.getStore();
|
|
918
|
+
if (!sink) return false;
|
|
919
|
+
sink(diagnostic);
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Emits a diagnostic, or any thrown value, on the run's hooks so the loggers render it live.
|
|
924
|
+
* A coded problem or an update goes to `kubb:diagnostic`. An uncoded one goes to `kubb:error`
|
|
925
|
+
* whole, so every wrapper's message, its help, and the stack survive. To collect a diagnostic into
|
|
926
|
+
* the build result from deep in a run, use {@link Diagnostics.report} instead.
|
|
927
|
+
*/
|
|
928
|
+
static async emit(hooks, value) {
|
|
929
|
+
const thrown = value instanceof Error || typeof value?.code !== "string";
|
|
930
|
+
const diagnostic = thrown ? Diagnostics.from(value) : value;
|
|
931
|
+
if (!isProblem(diagnostic) || diagnostic.code !== diagnosticCode.unknown) {
|
|
932
|
+
await hooks.callHook("kubb:diagnostic", { diagnostic });
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
const { cause, help, message } = diagnostic;
|
|
936
|
+
const bare = cause && !help && cause.message === message;
|
|
937
|
+
await hooks.callHook("kubb:error", { error: thrown ? toError(value) : bare ? cause : new Diagnostics.Error(diagnostic) });
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Coerces any thrown value into a {@link ProblemDiagnostic}. A {@link Diagnostics.Error}
|
|
941
|
+
* keeps its structured data, and anything else becomes a `KUBB_UNKNOWN` error.
|
|
942
|
+
*/
|
|
943
|
+
static from(error) {
|
|
944
|
+
const chain = getErrorChain(error);
|
|
945
|
+
const coded = chain.findLast((current) => Diagnostics.isError(current));
|
|
946
|
+
if (coded) {
|
|
947
|
+
const { diagnostic } = coded;
|
|
948
|
+
return diagnostic.cause || !crashCodes.has(diagnostic.code) ? diagnostic : {
|
|
949
|
+
...diagnostic,
|
|
950
|
+
cause: coded
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
const root = chain.at(-1);
|
|
954
|
+
return {
|
|
955
|
+
code: diagnosticCode.unknown,
|
|
956
|
+
severity: "error",
|
|
957
|
+
message: root ? root.message : getErrorMessage(error),
|
|
958
|
+
cause: root
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Coerces an error thrown by a plugin, adapter, or parser into a diagnostic attributed to it.
|
|
963
|
+
* A structured {@link Diagnostics.Error} keeps its code. Any other error is a crash: it gets the
|
|
964
|
+
* kind's `*_FAILED` code and the extension's `meta.issues` link, so the user reports it to the
|
|
965
|
+
* extension's maintainers instead of to Kubb.
|
|
966
|
+
*/
|
|
967
|
+
static fromExtension(error, { kind, extension }) {
|
|
968
|
+
const diagnostic = Diagnostics.from(error);
|
|
969
|
+
const name = extension.meta?.name ?? extension.name;
|
|
970
|
+
const owner = kind === "plugin" ? { plugin: diagnostic.plugin ?? name } : {};
|
|
971
|
+
const crashed = diagnostic.code === diagnosticCode.unknown && diagnostic.cause?.name !== "AbortError";
|
|
972
|
+
const failed = Object.values(failedCode).includes(diagnostic.code);
|
|
973
|
+
if (!crashed && !failed) return {
|
|
974
|
+
...diagnostic,
|
|
975
|
+
...owner
|
|
976
|
+
};
|
|
977
|
+
const issues = extension.meta?.issues;
|
|
978
|
+
return {
|
|
979
|
+
...diagnostic,
|
|
980
|
+
...owner,
|
|
981
|
+
code: crashed ? failedCode[kind] : diagnostic.code,
|
|
982
|
+
message: crashed && kind !== "plugin" ? `${kind} "${name}" crashed: ${diagnostic.message}` : diagnostic.message,
|
|
983
|
+
...issues ? { issues } : {}
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Builds a per-plugin performance record. Reporters sum these into the run total.
|
|
988
|
+
*/
|
|
989
|
+
static performance({ plugin, duration }) {
|
|
990
|
+
return {
|
|
991
|
+
kind: "performance",
|
|
992
|
+
code: diagnosticCode.performance,
|
|
993
|
+
severity: "info",
|
|
994
|
+
message: `${plugin} generated in ${Math.round(duration)}ms`,
|
|
995
|
+
plugin,
|
|
996
|
+
duration
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Builds the version-update notice shown when a newer Kubb is published on npm.
|
|
1001
|
+
*/
|
|
1002
|
+
static update({ currentVersion, latestVersion }) {
|
|
1003
|
+
return {
|
|
1004
|
+
kind: "update",
|
|
1005
|
+
code: diagnosticCode.updateAvailable,
|
|
1006
|
+
severity: "info",
|
|
1007
|
+
message: `Update available: v${currentVersion} → v${latestVersion}. Run \`npm install -g @kubb/cli\` to update.`,
|
|
1008
|
+
currentVersion,
|
|
1009
|
+
latestVersion
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* True when any diagnostic is an error, the severity that fails a build. Non-error
|
|
1014
|
+
* diagnostics are ignored.
|
|
1015
|
+
*/
|
|
1016
|
+
static hasError(diagnostics) {
|
|
1017
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Names of the plugins that failed, deduped, derived from the error diagnostics
|
|
1021
|
+
* that carry a `plugin`.
|
|
1022
|
+
*/
|
|
1023
|
+
static failedPlugins(diagnostics) {
|
|
1024
|
+
return [...new Set(diagnostics.flatMap((diagnostic) => diagnostic.severity === "error" && diagnostic.plugin ? [diagnostic.plugin] : []))];
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Counts `problem` diagnostics by severity for the run summary. `performance` and
|
|
1028
|
+
* `update` diagnostics are ignored.
|
|
1029
|
+
*/
|
|
1030
|
+
static count(diagnostics) {
|
|
1031
|
+
const counts = {
|
|
1032
|
+
errors: 0,
|
|
1033
|
+
warnings: 0,
|
|
1034
|
+
infos: 0
|
|
1035
|
+
};
|
|
1036
|
+
for (const diagnostic of diagnostics.filter(isProblem)) counts[countKey[diagnostic.severity]] += 1;
|
|
1037
|
+
return counts;
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Drops duplicate `problem` diagnostics that share a code, location pointer, and
|
|
1041
|
+
* plugin, so the same issue reported across several passes is shown once. Non-problem
|
|
1042
|
+
* diagnostics are always kept.
|
|
1043
|
+
*/
|
|
1044
|
+
static dedupe(diagnostics) {
|
|
1045
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1046
|
+
const result = [];
|
|
1047
|
+
for (const diagnostic of diagnostics) {
|
|
1048
|
+
if (!isProblem(diagnostic)) {
|
|
1049
|
+
result.push(diagnostic);
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
const pointer = diagnostic.location && "pointer" in diagnostic.location ? diagnostic.location.pointer : "";
|
|
1053
|
+
const key = `${diagnostic.code} ${pointer} ${diagnostic.plugin ?? ""}`;
|
|
1054
|
+
if (seen.has(key)) continue;
|
|
1055
|
+
seen.add(key);
|
|
1056
|
+
result.push(diagnostic);
|
|
1057
|
+
}
|
|
1058
|
+
return result;
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Builds the kubb.dev docs URL for a diagnostic code, e.g.
|
|
1062
|
+
* `KUBB_REF_NOT_FOUND` → `https://kubb.dev/docs/5.x/reference/diagnostics#kubb-ref-not-found`.
|
|
1063
|
+
*/
|
|
1064
|
+
static docsUrl(code) {
|
|
1065
|
+
const slug = code.toLowerCase().replaceAll("_", "-");
|
|
1066
|
+
return `https://kubb.dev/docs/${docsMajor}.x/reference/diagnostics#${slug}`;
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* The catalog entry for a code: its title, cause, and fix. Mirrors the kubb.dev
|
|
1070
|
+
* `/reference/diagnostics#<slug>` section.
|
|
1071
|
+
*/
|
|
1072
|
+
static explain(code) {
|
|
1073
|
+
return docsByCode.get(code);
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Reduces a diagnostic to its JSON-safe fields plus a `docsUrl`, for machine-readable
|
|
1077
|
+
* consumers, with the catalog `title`, `cause`, and fix, and the `cause` error as `causes` and
|
|
1078
|
+
* `stack`. Absent optional fields are omitted.
|
|
1079
|
+
*/
|
|
1080
|
+
static serialize = serialize;
|
|
1081
|
+
/**
|
|
1082
|
+
* Renders a diagnostic as lines for an `agent` (the default) or a `human`. See {@link format}.
|
|
1083
|
+
*/
|
|
1084
|
+
static format = format;
|
|
1085
|
+
};
|
|
1086
|
+
//#endregion
|
|
1087
|
+
//#region src/defineExtension.ts
|
|
1088
|
+
/**
|
|
1089
|
+
* Returns `meta`, or `{ name }` for an extension that only sets the deprecated `name`. Throws `KUBB_INVALID_PLUGIN_OPTIONS` when it has neither.
|
|
1090
|
+
*/
|
|
1091
|
+
function resolveMeta(extension) {
|
|
1092
|
+
const { name, meta } = extension;
|
|
1093
|
+
if (meta) return meta;
|
|
1094
|
+
if (name) return { name };
|
|
1095
|
+
const keys = Object.keys(extension);
|
|
1096
|
+
throw new Diagnostics.Error({
|
|
1097
|
+
code: Diagnostics.code.invalidPluginOptions,
|
|
1098
|
+
severity: "error",
|
|
1099
|
+
message: `An extension has neither \`meta.name\` nor \`name\`. Received keys: ${keys.length ? keys.join(", ") : "none"}.`,
|
|
1100
|
+
help: "Set `meta: { name }` on the plugin or parser.",
|
|
1101
|
+
location: { kind: "config" }
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Throws `KUBB_INCOMPATIBLE_EXTENSION` when the running Kubb or Node.js version is outside `meta.compatibility`.
|
|
1106
|
+
*/
|
|
1107
|
+
function checkCompatibility(meta) {
|
|
1108
|
+
const checks = [{
|
|
1109
|
+
target: "Kubb",
|
|
1110
|
+
range: meta.compatibility?.kubb,
|
|
1111
|
+
version
|
|
1112
|
+
}, {
|
|
1113
|
+
target: "Node.js",
|
|
1114
|
+
range: meta.compatibility?.node,
|
|
1115
|
+
version: process.versions.node
|
|
1116
|
+
}];
|
|
1117
|
+
for (const { target, range, version } of checks) {
|
|
1118
|
+
if (!range || (0, verkit.satisfies)(version, range, { includePrerelease: true })) continue;
|
|
1119
|
+
throw new Diagnostics.Error({
|
|
1120
|
+
code: Diagnostics.code.incompatibleExtension,
|
|
1121
|
+
severity: "error",
|
|
1122
|
+
message: `"${meta.name}" requires ${target} ${range}, but ${version} is running.`,
|
|
1123
|
+
help: `Use a ${target} version that satisfies ${range}, or a version of "${meta.name}" that supports ${target} ${version}.`,
|
|
1124
|
+
location: { kind: "config" },
|
|
1125
|
+
plugin: meta.name
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Throws `KUBB_MISSING_DEPENDENCY` for a missing required entry in a `dependencies` record, and
|
|
1131
|
+
* `KUBB_INCOMPATIBLE_DEPENDENCY` when an entry's `meta.version` is out of range. Arrays are not checked.
|
|
1132
|
+
*/
|
|
1133
|
+
function checkDependencies({ name, dependencies, plugins }) {
|
|
1134
|
+
if (!dependencies || Array.isArray(dependencies)) return;
|
|
1135
|
+
for (const [dependencyName, dependency] of Object.entries(dependencies)) {
|
|
1136
|
+
const target = plugins.get(dependencyName);
|
|
1137
|
+
if (!target) {
|
|
1138
|
+
if (dependency?.optional) continue;
|
|
1139
|
+
throw new Diagnostics.Error({
|
|
1140
|
+
code: Diagnostics.code.missingDependency,
|
|
1141
|
+
severity: "error",
|
|
1142
|
+
message: `Plugin "${dependencyName}" is required by "${name}" but not found.`,
|
|
1143
|
+
help: `Add "${dependencyName}" to the \`plugins\` array in kubb.config.ts.`,
|
|
1144
|
+
location: { kind: "config" },
|
|
1145
|
+
plugin: name
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
const range = dependency?.version;
|
|
1149
|
+
const version = target.meta?.version;
|
|
1150
|
+
if (!range || !version || (0, verkit.satisfies)(version, range, { includePrerelease: true })) continue;
|
|
1151
|
+
throw new Diagnostics.Error({
|
|
1152
|
+
code: Diagnostics.code.incompatibleDependency,
|
|
1153
|
+
severity: "error",
|
|
1154
|
+
message: `"${name}" requires "${dependencyName}" ${range}, but ${version} is configured.`,
|
|
1155
|
+
help: `Install a version of "${dependencyName}" that satisfies ${range}.`,
|
|
1156
|
+
location: { kind: "config" },
|
|
1157
|
+
plugin: name
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Builds the factory behind `defineExtension`. It passes `{}` when called without options, and carries `kind` and
|
|
1163
|
+
* `meta` so tools can read them without calling it.
|
|
1164
|
+
*
|
|
1165
|
+
* @internal
|
|
1166
|
+
*/
|
|
1167
|
+
function createExtensionFactory({ kind, meta, build }) {
|
|
1168
|
+
return Object.assign((options = {}) => build(options), {
|
|
1169
|
+
kind,
|
|
1170
|
+
meta
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
function defineExtension(definition) {
|
|
1174
|
+
switch (definition.kind) {
|
|
1175
|
+
case "plugin": {
|
|
1176
|
+
const { meta, dependencies, enforce, hooks = {}, setup } = definition;
|
|
1177
|
+
return createExtensionFactory({
|
|
1178
|
+
kind: "plugin",
|
|
1179
|
+
meta,
|
|
1180
|
+
build: (options) => ({
|
|
1181
|
+
name: meta.name,
|
|
1182
|
+
meta,
|
|
1183
|
+
options,
|
|
1184
|
+
dependencies,
|
|
1185
|
+
enforce,
|
|
1186
|
+
hooks,
|
|
1187
|
+
setup
|
|
1188
|
+
})
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
case "adapter": {
|
|
1192
|
+
const { meta, setup, build } = definition;
|
|
1193
|
+
return createExtensionFactory({
|
|
1194
|
+
kind: "adapter",
|
|
1195
|
+
meta,
|
|
1196
|
+
build: (raw) => {
|
|
1197
|
+
const options = setup ? setup(raw) : raw;
|
|
1198
|
+
return Object.assign(build(options), {
|
|
1199
|
+
name: meta.name,
|
|
1200
|
+
meta,
|
|
1201
|
+
options
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
case "parser": {
|
|
1207
|
+
const { meta, build } = definition;
|
|
1208
|
+
return createExtensionFactory({
|
|
1209
|
+
kind: "parser",
|
|
1210
|
+
meta,
|
|
1211
|
+
build: (options) => ({
|
|
1212
|
+
...build(options),
|
|
1213
|
+
name: meta.name,
|
|
1214
|
+
meta,
|
|
1215
|
+
options
|
|
1216
|
+
})
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
//#endregion
|
|
1222
|
+
//#region src/createAdapter.ts
|
|
1223
|
+
/**
|
|
1224
|
+
* Defines a custom adapter that translates a spec format into Kubb's universal
|
|
1225
|
+
* AST, for example GraphQL, gRPC, or AsyncAPI. The built-in `@kubb/adapter-oas`
|
|
1226
|
+
* handles OpenAPI/Swagger documents.
|
|
1227
|
+
*
|
|
1228
|
+
* Adapters must return an `InputNode` from `parse`. That node is what every
|
|
1229
|
+
* plugin in the build consumes.
|
|
1230
|
+
*
|
|
1231
|
+
* @deprecated Use `defineExtension` with `kind: 'adapter'` instead.
|
|
1232
|
+
*/
|
|
1233
|
+
function createAdapter(definition) {
|
|
1234
|
+
return defineExtension({
|
|
1235
|
+
kind: "adapter",
|
|
1236
|
+
...definition
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
//#endregion
|
|
1240
|
+
//#region src/composables.ts
|
|
1241
|
+
const kubbContextKey = Symbol.for("kubb.context");
|
|
1242
|
+
const host = globalThis;
|
|
1243
|
+
/**
|
|
1244
|
+
* Holds the setup context of the plugin whose `setup` or `kubb:plugin:setup` is running. `KubbDriver` sets it for each plugin.
|
|
1245
|
+
*
|
|
1246
|
+
* The store lives on `globalThis`, so the ESM and CommonJS builds of `@kubb/core` share one. A plugin that a
|
|
1247
|
+
* CommonJS config loads would otherwise read an empty store and fail with `KUBB_NO_CONTEXT`.
|
|
1248
|
+
*/
|
|
1249
|
+
const kubbContext = host[kubbContextKey] ??= new node_async_hooks.AsyncLocalStorage();
|
|
1250
|
+
function requireContext(caller) {
|
|
1251
|
+
const ctx = kubbContext.getStore();
|
|
1252
|
+
if (ctx) return ctx;
|
|
1253
|
+
throw new Diagnostics.Error({
|
|
1254
|
+
code: Diagnostics.code.noContext,
|
|
1255
|
+
severity: "error",
|
|
1256
|
+
message: `\`${caller}()\` was called outside a plugin setup.`,
|
|
1257
|
+
help: `Call \`${caller}()\` inside \`setup\` or \`kubb:plugin:setup\`, including in code that setup awaits.`
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Returns the context of the plugin whose `kubb:plugin:setup` is running. It stays available after an `await`.
|
|
1262
|
+
* Throws `KUBB_NO_CONTEXT` outside setup.
|
|
1263
|
+
*/
|
|
1264
|
+
function useKubb() {
|
|
1265
|
+
return requireContext("useKubb");
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Returns the same context as {@link useKubb}, or `undefined` outside `setup` and `kubb:plugin:setup`.
|
|
1269
|
+
*/
|
|
1270
|
+
function tryUseKubb() {
|
|
1271
|
+
return kubbContext.getStore();
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Registers generators on the plugin being set up, like `ctx.addGenerator`.
|
|
1275
|
+
*
|
|
1276
|
+
* @example
|
|
1277
|
+
* `addGenerator(typeGenerator, enumGenerator)`
|
|
1278
|
+
*/
|
|
1279
|
+
function addGenerator(...generators) {
|
|
1280
|
+
requireContext("addGenerator").addGenerator(...generators);
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Sets the resolver of the plugin being set up, like `ctx.setResolver`.
|
|
1284
|
+
*/
|
|
1285
|
+
function setResolver(resolver) {
|
|
1286
|
+
requireContext("setResolver").setResolver(resolver);
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Adds a macro to the plugin being set up, like `ctx.addMacro`.
|
|
1290
|
+
*/
|
|
1291
|
+
function addMacro(macro) {
|
|
1292
|
+
requireContext("addMacro").addMacro(macro);
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Replaces the macros of the plugin being set up, like `ctx.setMacros`.
|
|
1296
|
+
*/
|
|
1297
|
+
function setMacros(macros) {
|
|
1298
|
+
requireContext("setMacros").setMacros(macros);
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Adds a raw file to the build output, like `ctx.injectFile`.
|
|
1302
|
+
*/
|
|
1303
|
+
function injectFile(file) {
|
|
1304
|
+
requireContext("injectFile").injectFile(file);
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Returns the options the user passed to the plugin being set up, like `ctx.options`.
|
|
1308
|
+
*/
|
|
1309
|
+
function useOptions() {
|
|
1310
|
+
return requireContext("useOptions").options;
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Sets the resolved options of the plugin being set up, like `ctx.setOptions`.
|
|
1314
|
+
*/
|
|
1315
|
+
function setOptions(options) {
|
|
1316
|
+
requireContext("setOptions").setOptions(options);
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Returns the resolved build configuration, like `ctx.config`.
|
|
1320
|
+
*/
|
|
1321
|
+
function useConfig() {
|
|
1322
|
+
return requireContext("useConfig").config;
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Returns another plugin from the config, typed through `Kubb.PluginRegistry`, like `ctx.requirePlugin`.
|
|
1326
|
+
*/
|
|
1327
|
+
function requirePlugin(name) {
|
|
1328
|
+
return requireContext("requirePlugin").requirePlugin(name);
|
|
1329
|
+
}
|
|
1330
|
+
//#endregion
|
|
1331
|
+
//#region src/definePlugin.ts
|
|
1332
|
+
/**
|
|
1333
|
+
* Merges the `output.mode` default into the output config and validates the combination.
|
|
1334
|
+
* Throws `KUBB_INVALID_PLUGIN_OPTIONS` when `mode: 'file'` is paired with a `group` option,
|
|
1335
|
+
* since a single-file output has nothing to group.
|
|
1336
|
+
*
|
|
1337
|
+
* An omitted `mode` follows the shape of `path`: an extension means a single file, anything
|
|
1338
|
+
* else is a directory. Plugin defaults such as `path: 'types'` name a directory, so they
|
|
1339
|
+
* generate without the caller spelling out `mode`.
|
|
1340
|
+
*/
|
|
1341
|
+
function normalizeOutput({ output, group, pluginName }) {
|
|
1342
|
+
const mode = output.mode ?? (node_path.default.extname(output.path) ? "file" : "directory");
|
|
1343
|
+
if (mode === "file" && group) throw new Diagnostics.Error({
|
|
1344
|
+
code: Diagnostics.code.invalidPluginOptions,
|
|
1345
|
+
severity: "error",
|
|
1346
|
+
message: `Plugin "${pluginName}" resolves \`output.mode\` to 'file' but also configures a \`group\` option.`,
|
|
1347
|
+
help: "A single-file output has nothing to group. Remove the `group` option, give `output.path` an extensionless directory name, or set `output.mode: 'directory'` explicitly.",
|
|
1348
|
+
location: { kind: "config" },
|
|
1349
|
+
plugin: pluginName
|
|
1350
|
+
});
|
|
1351
|
+
return {
|
|
1352
|
+
...output,
|
|
1353
|
+
mode
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
/** Throws `KUBB_INVALID_PLUGIN_OPTIONS`, listing the allowed values, when `value` is not one of `allowed`. */
|
|
1357
|
+
function assertPluginOption({ plugin, option, value, allowed }) {
|
|
1358
|
+
if (allowed.includes(value)) return;
|
|
1359
|
+
const format = (item) => typeof item === "string" ? JSON.stringify(item) : String(item);
|
|
1360
|
+
const expected = allowed.map(format).join(", ");
|
|
1361
|
+
throw new Diagnostics.Error({
|
|
1362
|
+
code: Diagnostics.code.invalidPluginOptions,
|
|
1363
|
+
severity: "error",
|
|
1364
|
+
message: `Plugin "${plugin}" option \`${option}\` is ${format(value)}, but it must be one of ${expected}.`,
|
|
1365
|
+
help: `Set \`${option}\` to one of ${expected}.`,
|
|
1366
|
+
location: { kind: "config" },
|
|
1367
|
+
plugin
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Defines a plugin from one object and returns a factory that takes the user's options.
|
|
1372
|
+
*
|
|
1373
|
+
* @deprecated Use `defineExtension` with `kind: 'plugin'` instead.
|
|
1374
|
+
*/
|
|
1375
|
+
function definePlugin(definition) {
|
|
1376
|
+
return defineExtension({
|
|
1377
|
+
kind: "plugin",
|
|
1378
|
+
...definition
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
//#endregion
|
|
1382
|
+
//#region src/input.ts
|
|
1383
|
+
/**
|
|
1384
|
+
* Classifies an `input` value so callers branch on it once instead of repeating the checks.
|
|
1385
|
+
*
|
|
1386
|
+
* A non-string is a parsed spec (`object`). A string is `inline` when it holds OpenAPI content,
|
|
1387
|
+
* meaning it starts with `{` or `[`, spans multiple lines, or opens with a YAML `openapi:` or
|
|
1388
|
+
* `swagger:` key. Otherwise a string is a `url` when it parses as one, or a `file` path.
|
|
1389
|
+
*/
|
|
1390
|
+
function getInputKind(input) {
|
|
1391
|
+
if (typeof input !== "string") return "object";
|
|
1392
|
+
const trimmed = input.trimStart();
|
|
1393
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[") || input.includes("\n") || /^(openapi|swagger)\s*:/i.test(trimmed)) return "inline";
|
|
1394
|
+
if (URL.canParse(input)) return "url";
|
|
1395
|
+
return "file";
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* The v4 `input` wrapper keys. v4 typed `input` as `{ path }` or `{ data }`; v5 takes the value
|
|
1399
|
+
* directly, so the wrapper now matches the "already-parsed document" branch and silently yields
|
|
1400
|
+
* an empty build.
|
|
1401
|
+
*/
|
|
1402
|
+
const legacyInputKeys = ["path", "data"];
|
|
1403
|
+
/**
|
|
1404
|
+
* Detects the v4 `{ path }` / `{ data }` wrapper so it fails loudly instead of being read as a
|
|
1405
|
+
* document. A real spec always carries more than these keys, so an object whose keys are drawn
|
|
1406
|
+
* only from them is the old shape rather than a document that happens to have a `path` property.
|
|
1407
|
+
*/
|
|
1408
|
+
function isLegacyInput(input) {
|
|
1409
|
+
if (Array.isArray(input)) return input.some(isLegacyInput);
|
|
1410
|
+
if (typeof input !== "object" || input === null) return false;
|
|
1411
|
+
const keys = Object.keys(input);
|
|
1412
|
+
return keys.length > 0 && keys.every((key) => legacyInputKeys.includes(key));
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Normalizes `config.input` into an `AdapterSource` the adapter can parse.
|
|
1416
|
+
*
|
|
1417
|
+
* A parsed object and inline content become `{ type: 'data' }`; a URL is kept verbatim and a
|
|
1418
|
+
* local path is resolved against `config.root`, both as `{ type: 'path' }`.
|
|
1419
|
+
*/
|
|
1420
|
+
function inputToAdapterSource(config) {
|
|
1421
|
+
const input = config.input;
|
|
1422
|
+
if (input && isLegacyInput(input)) throw new Diagnostics.Error({
|
|
1423
|
+
code: Diagnostics.code.legacyInput,
|
|
1424
|
+
severity: "error",
|
|
1425
|
+
message: "The `input` option uses the v4 `{ path }` / `{ data }` wrapper.",
|
|
1426
|
+
help: "Unwrap it: `input: { path: \"./petStore.yaml\" }` becomes `input: \"./petStore.yaml\"`, and `input: { data: spec }` becomes `input: spec`.",
|
|
1427
|
+
location: { kind: "config" }
|
|
1428
|
+
});
|
|
1429
|
+
if (!input) throw new Diagnostics.Error({
|
|
1430
|
+
code: Diagnostics.code.inputRequired,
|
|
1431
|
+
severity: "error",
|
|
1432
|
+
message: "An adapter is configured without an input.",
|
|
1433
|
+
help: "Set `input` to a file path, a URL, an inline spec (JSON/YAML string), or a parsed object in your Kubb config.",
|
|
1434
|
+
location: { kind: "config" }
|
|
1435
|
+
});
|
|
1436
|
+
if (typeof input !== "string") return {
|
|
1437
|
+
type: "data",
|
|
1438
|
+
data: input
|
|
1439
|
+
};
|
|
1440
|
+
const kind = getInputKind(input);
|
|
1441
|
+
if (kind === "inline") return {
|
|
1442
|
+
type: "data",
|
|
1443
|
+
data: input
|
|
1444
|
+
};
|
|
1445
|
+
if (kind === "url") return {
|
|
1446
|
+
type: "path",
|
|
1447
|
+
path: input
|
|
1448
|
+
};
|
|
1449
|
+
return {
|
|
1450
|
+
type: "path",
|
|
1451
|
+
path: (0, node_path.resolve)(config.root, input)
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
//#endregion
|
|
1455
|
+
//#region src/Resolver.ts
|
|
1456
|
+
function isNamespace(value) {
|
|
1457
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Shared brand for reaching a resolver's build options. `Resolver.merge` reads this instead of
|
|
1461
|
+
* relying on `instanceof`, which fails when a CommonJS config and the ESM CLI each load their own
|
|
1462
|
+
* copy of `@kubb/core`. `Symbol.for` resolves to one key across those copies, so the options stay
|
|
1463
|
+
* reachable and a `file` override is never dropped.
|
|
1464
|
+
*/
|
|
1465
|
+
const resolverOptions = Symbol.for("@kubb/core/resolver/options");
|
|
1466
|
+
/**
|
|
1467
|
+
* Builds a nested file path from a dotted name. Splits on dots that precede a letter
|
|
1468
|
+
* (so version numbers embedded in operationIds like `v2025.0` stay intact), camelCases
|
|
1469
|
+
* every segment, and joins with `/`.
|
|
1470
|
+
*
|
|
1471
|
+
* Empty segments are dropped before joining. They arise when the name starts with a dot
|
|
1472
|
+
* followed by a letter (e.g. `..Schema` splits into `['..', 'Schema']` and `'..'` cases to
|
|
1473
|
+
* an empty string). Without this a leading `/` would form, which `path.resolve` reads as an
|
|
1474
|
+
* absolute path, letting generated files escape the configured output directory.
|
|
1475
|
+
*
|
|
1476
|
+
* @example Nested path from a dotted name
|
|
1477
|
+
* `toFilePath('pet.petId') // 'pet/petId'`
|
|
1478
|
+
*/
|
|
1479
|
+
function toFilePath(name) {
|
|
1480
|
+
return name.split(/\.(?=[a-zA-Z])/).map((part) => camelCase(part)).filter(Boolean).join("/");
|
|
1481
|
+
}
|
|
1482
|
+
/** Throws `KUBB_PATH_TRAVERSAL` when `target` lies outside `parent`; `target === parent` stays allowed. */
|
|
1483
|
+
function assertInside({ target, parent, message, help }) {
|
|
1484
|
+
const parentWithSep = parent.endsWith(node_path.default.sep) ? parent : `${parent}${node_path.default.sep}`;
|
|
1485
|
+
if (target === parent || target.startsWith(parentWithSep)) return;
|
|
1486
|
+
throw new Diagnostics.Error({
|
|
1487
|
+
code: Diagnostics.code.pathTraversal,
|
|
1488
|
+
severity: "error",
|
|
1489
|
+
message,
|
|
1490
|
+
help,
|
|
1491
|
+
location: { kind: "config" }
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Built-in `file.baseName`: casts the identifier with `toFilePath` and appends the extension.
|
|
1496
|
+
*/
|
|
1497
|
+
function toBaseName({ name, extname }) {
|
|
1498
|
+
return `${toFilePath(name)}${extname}`;
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Base constraint for all plugin resolver objects.
|
|
1502
|
+
*
|
|
1503
|
+
* The built-in machinery lives under `default`. Generators call the top-level `name`, `file`,
|
|
1504
|
+
* and `imports`, and a plugin overrides `name` and `file` to set its conventions. Extend with
|
|
1505
|
+
* top-level helpers (`typeName`, …) and/or grouped namespaces (`query`, `schema`, …).
|
|
1506
|
+
*
|
|
1507
|
+
* @example Top-level helper
|
|
1508
|
+
* ```ts
|
|
1509
|
+
* type MyResolver = Resolver & {
|
|
1510
|
+
* typeName(name: string): string
|
|
1511
|
+
* }
|
|
1512
|
+
* ```
|
|
1513
|
+
*
|
|
1514
|
+
* @example Grouped namespace
|
|
1515
|
+
* ```ts
|
|
1516
|
+
* type MyResolver = Resolver & {
|
|
1517
|
+
* query: {
|
|
1518
|
+
* name(node: OperationNode): string
|
|
1519
|
+
* keyName(node: OperationNode): string
|
|
1520
|
+
* }
|
|
1521
|
+
* }
|
|
1522
|
+
* ```
|
|
1523
|
+
*/
|
|
1524
|
+
var Resolver = class Resolver {
|
|
1525
|
+
static #patternCache = /* @__PURE__ */ new Map();
|
|
1526
|
+
static #optionsCache = /* @__PURE__ */ new WeakMap();
|
|
1527
|
+
pluginName;
|
|
1528
|
+
/** The built-in machinery, untouched by `name`/`file` overrides and built once since the driver reads it per node. */
|
|
1529
|
+
default;
|
|
1530
|
+
#options;
|
|
1531
|
+
#baseName;
|
|
1532
|
+
#filePath;
|
|
1533
|
+
constructor(options) {
|
|
1534
|
+
this.pluginName = options.pluginName;
|
|
1535
|
+
this.#options = options;
|
|
1536
|
+
this.#baseName = options.file?.baseName ? options.file.baseName.bind(this) : toBaseName;
|
|
1537
|
+
this.#filePath = options.file?.path ? options.file.path.bind(this) : void 0;
|
|
1538
|
+
this.default = {
|
|
1539
|
+
name: camelCase,
|
|
1540
|
+
options: this.#resolveOptions.bind(this),
|
|
1541
|
+
path: this.#resolvePath.bind(this),
|
|
1542
|
+
file: this.#resolveFile.bind(this),
|
|
1543
|
+
banner: this.#resolveBanner.bind(this),
|
|
1544
|
+
footer: this.#resolveFooter.bind(this)
|
|
1545
|
+
};
|
|
1546
|
+
this.#apply(options);
|
|
1547
|
+
}
|
|
1548
|
+
/** Exposes the raw build options so `Resolver.merge` can read them across `@kubb/core` copies. */
|
|
1549
|
+
get [resolverOptions]() {
|
|
1550
|
+
return this.#options;
|
|
1551
|
+
}
|
|
1552
|
+
name(name) {
|
|
1553
|
+
return this.default.name(name);
|
|
1554
|
+
}
|
|
1555
|
+
file(options) {
|
|
1556
|
+
return this.#resolveFile(options);
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Builds one `ImportNode` per unique schema referenced in the tree, in first-occurrence
|
|
1560
|
+
* order. Each ref's target resolves through `resolveRefName`, so collision- or macro-renamed
|
|
1561
|
+
* schemas (`targetName`) import the emitted name. Names and paths go through the top-level
|
|
1562
|
+
* `name` and `file`, so import entries follow the plugin's conventions, and a per-call
|
|
1563
|
+
* `name` override wins over both.
|
|
1564
|
+
*
|
|
1565
|
+
* The subtree scan runs through `collectImportedRefNames`, which memoizes by node identity, so a
|
|
1566
|
+
* schema shared across the ts, zod, and faker plugins is walked once and every plugin's resolver
|
|
1567
|
+
* reads the same ref set instead of re-scanning it per plugin.
|
|
1568
|
+
*/
|
|
1569
|
+
imports(options) {
|
|
1570
|
+
const { node, root, output, group, extname = ".ts", name } = options;
|
|
1571
|
+
const resolveName = name ?? ((schemaName) => this.name(schemaName));
|
|
1572
|
+
return (0, _kubb_ast.collectImportedRefNames)(node).map((schemaName) => _kubb_ast.ast.factory.createImport({
|
|
1573
|
+
name: [resolveName(schemaName)],
|
|
1574
|
+
path: this.file({
|
|
1575
|
+
name: schemaName,
|
|
1576
|
+
extname,
|
|
1577
|
+
root,
|
|
1578
|
+
output,
|
|
1579
|
+
group
|
|
1580
|
+
}).path
|
|
1581
|
+
}));
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* Folds each `override` over `base`, left to right, and returns a new resolver with helpers
|
|
1585
|
+
* re-bound. Top-level keys replace, and a namespace (or `file`) merges per method, so overriding
|
|
1586
|
+
* `query.name` keeps the base `query.keyName`. The last override wins per key. Used when applying
|
|
1587
|
+
* `setResolver` partial overrides, and to compose shared resolver fragments without spreading each
|
|
1588
|
+
* namespace by hand. Reads a resolver's options through the shared brand rather than `instanceof`,
|
|
1589
|
+
* so a `file` override survives even when `base` and `override` come from different `@kubb/core`
|
|
1590
|
+
* copies.
|
|
1591
|
+
*
|
|
1592
|
+
* @example Fold several partial overrides onto a resolver
|
|
1593
|
+
* ```ts
|
|
1594
|
+
* const resolver = Resolver.merge(defaultResolver, sharedNamingPatch, { name: (name) => name.toUpperCase() })
|
|
1595
|
+
* ```
|
|
1596
|
+
*/
|
|
1597
|
+
static merge(base, ...overrides) {
|
|
1598
|
+
const merged = overrides.reduce((acc, override) => {
|
|
1599
|
+
const patch = resolverOptions in override ? override[resolverOptions] : override;
|
|
1600
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
1601
|
+
if (value === void 0) continue;
|
|
1602
|
+
const current = acc[key];
|
|
1603
|
+
acc[key] = isNamespace(value) && isNamespace(current) ? {
|
|
1604
|
+
...current,
|
|
1605
|
+
...value
|
|
1606
|
+
} : value;
|
|
1607
|
+
}
|
|
1608
|
+
return acc;
|
|
1609
|
+
}, { ...base[resolverOptions] });
|
|
1610
|
+
return new Resolver(merged);
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Binds each entry of `options` onto the resolver, so `this.name`, `this.default`, and
|
|
1614
|
+
* `this.file` resolve there for top-level helpers and namespace methods alike. `default`
|
|
1615
|
+
* is skipped so it can't be shadowed.
|
|
1616
|
+
*/
|
|
1617
|
+
#apply(options) {
|
|
1618
|
+
const root = this;
|
|
1619
|
+
const bind = (value) => typeof value === "function" ? value.bind(root) : value;
|
|
1620
|
+
for (const [key, value] of Object.entries(options)) {
|
|
1621
|
+
if (key === "pluginName" || key === "default" || key === "file" || value === void 0) continue;
|
|
1622
|
+
root[key] = isNamespace(value) ? Object.fromEntries(Object.entries(value).map(([method, member]) => [method, bind(member)])) : bind(value);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
static #testPattern(value, pattern) {
|
|
1626
|
+
if (typeof pattern === "string") {
|
|
1627
|
+
let regex = Resolver.#patternCache.get(pattern);
|
|
1628
|
+
regex ??= new RegExp(pattern);
|
|
1629
|
+
Resolver.#patternCache.set(pattern, regex);
|
|
1630
|
+
return regex.test(value);
|
|
1631
|
+
}
|
|
1632
|
+
return value.match(pattern) !== null;
|
|
1633
|
+
}
|
|
1634
|
+
static #matchesOperation(node, filter) {
|
|
1635
|
+
if (filter.type === "extension") {
|
|
1636
|
+
const { extensions } = node;
|
|
1637
|
+
return extensions !== void 0 && Object.hasOwn(extensions, filter.key) && (filter.value === void 0 || extensions[filter.key] === filter.value);
|
|
1638
|
+
}
|
|
1639
|
+
const { type, pattern } = filter;
|
|
1640
|
+
if (type === "tag") return node.tags.some((tag) => Resolver.#testPattern(tag, pattern));
|
|
1641
|
+
if (type === "operationId") return Resolver.#testPattern(node.operationId, pattern);
|
|
1642
|
+
if (type === "path") return node.path !== void 0 && Resolver.#testPattern(node.path, pattern);
|
|
1643
|
+
if (type === "method") return node.method !== void 0 && Resolver.#testPattern(node.method.toLowerCase(), pattern);
|
|
1644
|
+
if (type === "contentType") return node.requestBody?.content?.some((c) => Resolver.#testPattern(c.contentType, pattern)) ?? false;
|
|
1645
|
+
return false;
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Returns `null` when the filter type doesn't apply to schemas, so include rules built
|
|
1649
|
+
* from operation filters (e.g. `tag`) don't exclude every schema.
|
|
1650
|
+
*/
|
|
1651
|
+
static #matchesSchema(node, filter) {
|
|
1652
|
+
if (filter.type === "schemaName") return node.name ? Resolver.#testPattern(node.name, filter.pattern) : false;
|
|
1653
|
+
return null;
|
|
1654
|
+
}
|
|
1655
|
+
static #computeOptions(node, { options, exclude = [], include, override = [] }) {
|
|
1656
|
+
if (node.kind === "Operation") {
|
|
1657
|
+
if (exclude.some((filter) => Resolver.#matchesOperation(node, filter))) return null;
|
|
1658
|
+
if (include && !include.some((filter) => Resolver.#matchesOperation(node, filter))) return null;
|
|
1659
|
+
return {
|
|
1660
|
+
...options,
|
|
1661
|
+
...override.find((filter) => Resolver.#matchesOperation(node, filter))?.options
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
if (node.kind === "Schema") {
|
|
1665
|
+
if (exclude.some((filter) => Resolver.#matchesSchema(node, filter) === true)) return null;
|
|
1666
|
+
if (include) {
|
|
1667
|
+
const applicable = include.map((filter) => Resolver.#matchesSchema(node, filter)).filter((result) => result !== null);
|
|
1668
|
+
if (applicable.length > 0 && !applicable.includes(true)) return null;
|
|
1669
|
+
}
|
|
1670
|
+
return {
|
|
1671
|
+
...options,
|
|
1672
|
+
...override.find((filter) => Resolver.#matchesSchema(node, filter) === true)?.options
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
return options;
|
|
1676
|
+
}
|
|
1677
|
+
/**
|
|
1678
|
+
* Applies include/exclude filters and merges matching override options, caching the result
|
|
1679
|
+
* per `(options, node)` pair. Returns `null` when the node is filtered out.
|
|
1680
|
+
*/
|
|
1681
|
+
#resolveOptions(node, context) {
|
|
1682
|
+
const { options } = context;
|
|
1683
|
+
if (typeof options !== "object" || options === null) return Resolver.#computeOptions(node, context);
|
|
1684
|
+
let byOptions = Resolver.#optionsCache.get(options);
|
|
1685
|
+
if (!byOptions) {
|
|
1686
|
+
byOptions = /* @__PURE__ */ new WeakMap();
|
|
1687
|
+
Resolver.#optionsCache.set(options, byOptions);
|
|
1688
|
+
}
|
|
1689
|
+
const cached = byOptions.get(node);
|
|
1690
|
+
if (cached) return cached.value;
|
|
1691
|
+
const result = Resolver.#computeOptions(node, context);
|
|
1692
|
+
byOptions.set(node, { value: result });
|
|
1693
|
+
return result;
|
|
1694
|
+
}
|
|
1695
|
+
/**
|
|
1696
|
+
* A custom `group.name` wins; otherwise `tag` groups use the camelCased tag and `path`
|
|
1697
|
+
* groups use the first non-traversal segment (`''` when none remain, placing the file in
|
|
1698
|
+
* the output root, kept safe by the caller's boundary check).
|
|
1699
|
+
*/
|
|
1700
|
+
static #resolveGroupDir(group, groupValue) {
|
|
1701
|
+
if (group.name) return group.name({ group: groupValue });
|
|
1702
|
+
if (group.type === "tag") return camelCase(groupValue);
|
|
1703
|
+
const segment = groupValue.split("/").filter((part) => part !== "" && part !== "." && part !== "..")[0];
|
|
1704
|
+
return segment ? camelCase(segment) : "";
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* `mode: 'file'` (default) resolves directly to `output.path`. `mode: 'directory'` resolves
|
|
1708
|
+
* to `output.path/{baseName}`, or into a subdirectory when `group` and a `tag`/`path` value
|
|
1709
|
+
* are provided.
|
|
1710
|
+
*/
|
|
1711
|
+
#resolvePath({ baseName, tag, path: groupPath, root, output, group }) {
|
|
1712
|
+
if (output.mode !== "directory") return node_path.default.resolve(root, output.path);
|
|
1713
|
+
const outputDir = node_path.default.resolve(root, output.path);
|
|
1714
|
+
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);
|
|
1715
|
+
assertInside({
|
|
1716
|
+
target: result,
|
|
1717
|
+
parent: outputDir,
|
|
1718
|
+
message: `Resolved path "${result}" is outside the output directory "${outputDir}".`,
|
|
1719
|
+
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."
|
|
1720
|
+
});
|
|
1721
|
+
return result;
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Resolves a resolver-supplied full path (`file.path`) against `root`, bypassing `output.path`
|
|
1725
|
+
* and `group`. The path may not escape `root`, which keeps a `file.path` that interpolates
|
|
1726
|
+
* spec-derived values from writing outside the project.
|
|
1727
|
+
*/
|
|
1728
|
+
#resolveOverridePath(filePath, root) {
|
|
1729
|
+
const resolved = node_path.default.resolve(root, filePath);
|
|
1730
|
+
assertInside({
|
|
1731
|
+
target: resolved,
|
|
1732
|
+
parent: root,
|
|
1733
|
+
message: `Resolved path "${resolved}" is outside the project root "${root}".`,
|
|
1734
|
+
help: "A resolver `file.path` must return a path inside the project root."
|
|
1735
|
+
});
|
|
1736
|
+
return resolved;
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Builds a `FileNode`. When `#filePath` (the resolver's `file.path`) is set it owns the whole
|
|
1740
|
+
* path; otherwise the base name (from `#baseName`, the resolver's `file.baseName` or the
|
|
1741
|
+
* built-in `toBaseName`) is placed by the `output.path`/`group` layout. The resolved file starts
|
|
1742
|
+
* with empty `sources`, `imports`, and `exports`, which consumers populate separately.
|
|
1743
|
+
*/
|
|
1744
|
+
#resolveFile(options) {
|
|
1745
|
+
const { name, extname, tag, path: groupPath, root, output, group } = options;
|
|
1746
|
+
const baseName = this.#baseName({
|
|
1747
|
+
name,
|
|
1748
|
+
extname
|
|
1749
|
+
});
|
|
1750
|
+
const filePath = this.#filePath ? this.#resolveOverridePath(this.#filePath({
|
|
1751
|
+
baseName,
|
|
1752
|
+
output
|
|
1753
|
+
}), root) : this.#resolvePath({
|
|
1754
|
+
baseName,
|
|
1755
|
+
tag,
|
|
1756
|
+
path: groupPath,
|
|
1757
|
+
root,
|
|
1758
|
+
output,
|
|
1759
|
+
group
|
|
1760
|
+
});
|
|
1761
|
+
return _kubb_ast.ast.factory.createFile({
|
|
1762
|
+
path: filePath,
|
|
1763
|
+
baseName: node_path.default.basename(filePath),
|
|
1764
|
+
meta: { pluginName: this.pluginName },
|
|
1765
|
+
sources: [],
|
|
1766
|
+
imports: [],
|
|
1767
|
+
exports: []
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Missing fields default to empty/`false` so the `BannerMeta` shape stays stable even when
|
|
1772
|
+
* a caller (e.g. the barrel plugin) has no document metadata.
|
|
1773
|
+
*/
|
|
1774
|
+
static #buildBannerMeta(meta, file) {
|
|
1775
|
+
return {
|
|
1776
|
+
title: meta?.title,
|
|
1777
|
+
description: meta?.description,
|
|
1778
|
+
version: meta?.version,
|
|
1779
|
+
baseURL: meta?.baseURL,
|
|
1780
|
+
circularNames: meta?.circularNames ?? [],
|
|
1781
|
+
enumNames: meta?.enumNames ?? [],
|
|
1782
|
+
filePath: file?.path ?? "",
|
|
1783
|
+
baseName: file?.baseName ?? "",
|
|
1784
|
+
isBarrel: file?.isBarrel ?? false,
|
|
1785
|
+
isAggregation: file?.isAggregation ?? false
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Resolves a user-configured banner/footer value. `undefined` means not configured.
|
|
1790
|
+
*/
|
|
1791
|
+
static #resolveUserText(value, meta, file) {
|
|
1792
|
+
if (typeof value === "function") return value(Resolver.#buildBannerMeta(meta, file));
|
|
1793
|
+
if (typeof value === "string") return value;
|
|
1794
|
+
}
|
|
1795
|
+
static #buildDefaultBanner({ title, version, config }) {
|
|
1796
|
+
const lines = [
|
|
1797
|
+
"/**",
|
|
1798
|
+
"* Generated by Kubb (https://kubb.dev/).",
|
|
1799
|
+
"* Do not edit manually."
|
|
1800
|
+
];
|
|
1801
|
+
if (config.output.defaultBanner !== "simple") {
|
|
1802
|
+
const input = config.input;
|
|
1803
|
+
const source = typeof input === "string" ? getInputKind(input) === "inline" ? "text content" : node_path.default.basename(input) : input ? "text content" : "";
|
|
1804
|
+
if (source) lines.push(`* Source: ${source}`);
|
|
1805
|
+
if (title) lines.push(`* Title: ${title}`);
|
|
1806
|
+
if (version) lines.push(`* OpenAPI spec version: ${version}`);
|
|
1807
|
+
}
|
|
1808
|
+
return `${lines.join("\n")}\n*/\n`;
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* A user-supplied `output.banner` overrides the default Kubb notice. When
|
|
1812
|
+
* `config.output.defaultBanner` is `false` and no user banner is set, returns `null`.
|
|
1813
|
+
*/
|
|
1814
|
+
#resolveBanner(meta, { output, config, file }) {
|
|
1815
|
+
const userBanner = Resolver.#resolveUserText(output?.banner, meta, file);
|
|
1816
|
+
if (userBanner !== void 0) return userBanner;
|
|
1817
|
+
if (config.output.defaultBanner === false) return null;
|
|
1818
|
+
return Resolver.#buildDefaultBanner({
|
|
1819
|
+
title: meta?.title,
|
|
1820
|
+
version: meta?.version,
|
|
1821
|
+
config
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
#resolveFooter(meta, { output, file }) {
|
|
1825
|
+
return Resolver.#resolveUserText(output?.footer, meta, file) ?? null;
|
|
1826
|
+
}
|
|
1827
|
+
};
|
|
1828
|
+
//#endregion
|
|
1829
|
+
//#region src/createResolver.ts
|
|
1830
|
+
/**
|
|
1831
|
+
* Defines a plugin resolver, the object that decides what every generated symbol and file
|
|
1832
|
+
* path is called. Override the top-level `name` and `file` to set the plugin's conventions,
|
|
1833
|
+
* and add your own naming helpers, top-level (`typeName`, …) or grouped in namespaces
|
|
1834
|
+
* (`query`, `schema`, …). Every method reaches sibling helpers and the built-in machinery
|
|
1835
|
+
* through `this.name`, `this.file`, and `this.default`.
|
|
1836
|
+
*
|
|
1837
|
+
* @example Custom identifier casing
|
|
1838
|
+
* ```ts
|
|
1839
|
+
* export const resolverTs = createResolver<PluginTs>({
|
|
1840
|
+
* pluginName: 'plugin-ts',
|
|
1841
|
+
* name(name) {
|
|
1842
|
+
* return ensureValidVarName(pascalCase(name))
|
|
1843
|
+
* },
|
|
1844
|
+
* })
|
|
1845
|
+
* ```
|
|
1846
|
+
*
|
|
1847
|
+
* @example Rename generated files with `file.baseName`
|
|
1848
|
+
* ```ts
|
|
1849
|
+
* export const resolverFaker = createResolver<PluginFaker>({
|
|
1850
|
+
* pluginName: 'plugin-faker',
|
|
1851
|
+
* name(name) {
|
|
1852
|
+
* return camelCase(name, { prefix: 'create' })
|
|
1853
|
+
* },
|
|
1854
|
+
* file: {
|
|
1855
|
+
* baseName({ name, extname }) {
|
|
1856
|
+
* return `${camelCase(name, { prefix: 'create' })}${extname}`
|
|
1857
|
+
* },
|
|
1858
|
+
* },
|
|
1859
|
+
* })
|
|
1860
|
+
* ```
|
|
1861
|
+
*
|
|
1862
|
+
* @example Own the full path with `file.path`
|
|
1863
|
+
* ```ts
|
|
1864
|
+
* export const resolverFaker = createResolver<PluginFaker>({
|
|
1865
|
+
* pluginName: 'plugin-faker',
|
|
1866
|
+
* file: {
|
|
1867
|
+
* path({ baseName, output }) {
|
|
1868
|
+
* return `${output.path}/mocks/${baseName}`
|
|
1869
|
+
* },
|
|
1870
|
+
* },
|
|
1871
|
+
* })
|
|
1872
|
+
* ```
|
|
1873
|
+
*/
|
|
1874
|
+
function createResolver(options) {
|
|
1875
|
+
return new Resolver(options);
|
|
1876
|
+
}
|
|
1877
|
+
//#endregion
|
|
1878
|
+
//#region \0@oxc-project+runtime@0.153.0/helpers/esm/usingCtx.js
|
|
1879
|
+
function _usingCtx() {
|
|
1880
|
+
var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
|
|
1881
|
+
var n = Error();
|
|
1882
|
+
return n.name = "SuppressedError", n.error = r, n.suppressed = e, n;
|
|
1883
|
+
};
|
|
1884
|
+
var e = {};
|
|
1885
|
+
var n = [];
|
|
1886
|
+
function using(r, e) {
|
|
1887
|
+
if (null != e) {
|
|
1888
|
+
if (Object(e) !== e) throw new TypeError("using declarations can only be used with objects, functions, null, or undefined.");
|
|
1889
|
+
if (r) var o = e[Symbol.asyncDispose || Symbol["for"]("Symbol.asyncDispose")];
|
|
1890
|
+
if (void 0 === o && (o = e[Symbol.dispose || Symbol["for"]("Symbol.dispose")], r)) var t = o;
|
|
1891
|
+
if ("function" != typeof o) throw new TypeError("Object is not disposable.");
|
|
1892
|
+
t && (o = function o() {
|
|
1893
|
+
try {
|
|
1894
|
+
t.call(e);
|
|
1895
|
+
} catch (r) {
|
|
1896
|
+
return Promise.reject(r);
|
|
1897
|
+
}
|
|
1898
|
+
}), n.push({
|
|
1899
|
+
v: e,
|
|
1900
|
+
d: o,
|
|
1901
|
+
a: r
|
|
1902
|
+
});
|
|
1903
|
+
} else r && n.push({
|
|
1904
|
+
d: e,
|
|
1905
|
+
a: r
|
|
1906
|
+
});
|
|
1907
|
+
return e;
|
|
1908
|
+
}
|
|
1909
|
+
return {
|
|
1910
|
+
e,
|
|
1911
|
+
u: using.bind(null, !1),
|
|
1912
|
+
a: using.bind(null, !0),
|
|
1913
|
+
d: function d() {
|
|
1914
|
+
var o;
|
|
1915
|
+
var t = this.e;
|
|
1916
|
+
var s = 0;
|
|
1917
|
+
function next() {
|
|
1918
|
+
for (; o = n.pop();) try {
|
|
1919
|
+
if (!o.a && 1 === s) return s = 0, n.push(o), Promise.resolve().then(next);
|
|
1920
|
+
if (o.d) {
|
|
1921
|
+
var r = o.d.call(o.v);
|
|
1922
|
+
if (o.a) return s |= 2, Promise.resolve(r).then(next, err);
|
|
1923
|
+
} else s |= 1;
|
|
1924
|
+
} catch (r) {
|
|
1925
|
+
return err(r);
|
|
1926
|
+
}
|
|
1927
|
+
if (1 === s) return t !== e ? Promise.reject(t) : Promise.resolve();
|
|
1928
|
+
if (t !== e) throw t;
|
|
1929
|
+
}
|
|
1930
|
+
function err(n) {
|
|
1931
|
+
return t = t !== e ? new r(n, t) : n, next();
|
|
1932
|
+
}
|
|
1933
|
+
return next();
|
|
1934
|
+
}
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
function joinSources(file) {
|
|
1938
|
+
return file.sources.map((source) => (0, _kubb_ast.extractStringsFromNodes)(source.nodes)).filter(Boolean).join("\n\n");
|
|
1939
|
+
}
|
|
1940
|
+
/** Runs a parser call and turns a crash into a `KUBB_PARSER_FAILED` diagnostic that names the parser. */
|
|
1941
|
+
function runParser(parser, print) {
|
|
1942
|
+
try {
|
|
1943
|
+
return print();
|
|
1944
|
+
} catch (error) {
|
|
1945
|
+
throw new Diagnostics.Error(Diagnostics.fromExtension(error, {
|
|
1946
|
+
kind: "parser",
|
|
1947
|
+
extension: parser
|
|
1948
|
+
}));
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Reads a `copy` file and prints it through the parser's `copy` hook, or verbatim with `banner`/`footer` when there is none.
|
|
1953
|
+
*/
|
|
1954
|
+
async function parseCopy(file, parser) {
|
|
1955
|
+
let content;
|
|
1956
|
+
try {
|
|
1957
|
+
content = await (0, node_fs_promises.readFile)(file.copy, "utf8");
|
|
1958
|
+
} catch (err) {
|
|
1959
|
+
throw new Diagnostics.Error({
|
|
1960
|
+
code: Diagnostics.code.pluginFailed,
|
|
1961
|
+
severity: "error",
|
|
1962
|
+
message: `Could not copy file into output: ${file.copy}`,
|
|
1963
|
+
help: "Check that the file a plugin copies into the output exists and is readable.",
|
|
1964
|
+
cause: toError(err)
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
if (parser?.copy) return runParser(parser, () => parser.parse(_kubb_ast.ast.factory.createFile(parser.copy(file, content))));
|
|
1968
|
+
return [
|
|
1969
|
+
file.banner,
|
|
1970
|
+
content,
|
|
1971
|
+
file.footer
|
|
1972
|
+
].filter((segment) => Boolean(segment)).map((segment) => segment.trimEnd()).join("\n");
|
|
1973
|
+
}
|
|
1974
|
+
function mergeFile(a, b) {
|
|
1975
|
+
return {
|
|
1976
|
+
...a,
|
|
1977
|
+
banner: b.banner,
|
|
1978
|
+
footer: b.footer,
|
|
1979
|
+
copy: b.copy ?? a.copy,
|
|
1980
|
+
sources: a.sources.length ? b.sources.length ? [...a.sources, ...b.sources] : a.sources : b.sources,
|
|
1981
|
+
imports: a.imports.length ? b.imports.length ? [...a.imports, ...b.imports] : a.imports : b.imports,
|
|
1982
|
+
exports: a.exports.length ? b.exports.length ? [...a.exports, ...b.exports] : a.exports : b.exports
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
function isIndexPath(path) {
|
|
1986
|
+
return path.endsWith("/index.ts") || path === "index.ts";
|
|
1987
|
+
}
|
|
1988
|
+
function compareFiles(a, b) {
|
|
1989
|
+
return a.path.length - b.path.length || Number(isIndexPath(a.path)) - Number(isIndexPath(b.path));
|
|
1990
|
+
}
|
|
1991
|
+
function isUnchanged({ stored, source, key, manifest }) {
|
|
1992
|
+
if (stored === null) return false;
|
|
1993
|
+
if (matchesStored({
|
|
1994
|
+
stored,
|
|
1995
|
+
source
|
|
1996
|
+
})) return true;
|
|
1997
|
+
return manifest?.isUpToDate({
|
|
1998
|
+
key,
|
|
1999
|
+
source,
|
|
2000
|
+
disk: stored
|
|
2001
|
+
}) ?? false;
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* In-memory file store for generated files, and the writer that turns them into source
|
|
2005
|
+
* strings on `storage`. Files sharing a `path` are merged (sources/imports/exports
|
|
2006
|
+
* concatenated). The `files` getter is sorted by path length (barrel `index.ts` last
|
|
2007
|
+
* within a bucket).
|
|
2008
|
+
*
|
|
2009
|
+
* @example
|
|
2010
|
+
* ```ts
|
|
2011
|
+
* const manager = new FileManager()
|
|
2012
|
+
* manager.upsert(myFile)
|
|
2013
|
+
* manager.files // sorted view
|
|
2014
|
+
* await manager.write(manager.files, { storage: fsStorage() })
|
|
2015
|
+
* ```
|
|
2016
|
+
*/
|
|
2017
|
+
var FileManager = class {
|
|
2018
|
+
#cache = /* @__PURE__ */ new Map();
|
|
2019
|
+
#sorted = null;
|
|
2020
|
+
add(...files) {
|
|
2021
|
+
return this.#store(files, false);
|
|
2022
|
+
}
|
|
2023
|
+
upsert(...files) {
|
|
2024
|
+
return this.#store(files, true);
|
|
2025
|
+
}
|
|
2026
|
+
/** Stores a generator result: a file array is upserted as is, an element renders through `renderer` first, falsy is a no-op. */
|
|
2027
|
+
async dispatch({ result, renderer }) {
|
|
2028
|
+
try {
|
|
2029
|
+
var _usingCtx$2 = _usingCtx();
|
|
2030
|
+
if (!result) return;
|
|
2031
|
+
if (Array.isArray(result)) {
|
|
2032
|
+
this.upsert(...result);
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
if (!renderer) return;
|
|
2036
|
+
const instance = _usingCtx$2.u(renderer());
|
|
2037
|
+
await instance.render(result);
|
|
2038
|
+
this.upsert(...instance.files);
|
|
2039
|
+
} catch (_) {
|
|
2040
|
+
_usingCtx$2.e = _;
|
|
2041
|
+
} finally {
|
|
2042
|
+
_usingCtx$2.d();
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
#store(files, mergeExisting) {
|
|
2046
|
+
const batch = files.length > 1 ? this.#dedupe(files) : files;
|
|
2047
|
+
const resolved = [];
|
|
2048
|
+
for (const file of batch) {
|
|
2049
|
+
const existing = this.#cache.get(file.path);
|
|
2050
|
+
const merged = existing && mergeExisting ? _kubb_ast.ast.factory.createFile(mergeFile(existing, file)) : _kubb_ast.ast.factory.createFile(file);
|
|
2051
|
+
this.#cache.set(merged.path, merged);
|
|
2052
|
+
resolved.push(merged);
|
|
2053
|
+
}
|
|
2054
|
+
if (resolved.length > 0) this.#sorted = null;
|
|
2055
|
+
return resolved;
|
|
2056
|
+
}
|
|
2057
|
+
#dedupe(files) {
|
|
2058
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2059
|
+
for (const file of files) {
|
|
2060
|
+
const prev = seen.get(file.path);
|
|
2061
|
+
seen.set(file.path, prev ? mergeFile(prev, file) : file);
|
|
2062
|
+
}
|
|
2063
|
+
return [...seen.values()];
|
|
2064
|
+
}
|
|
2065
|
+
/**
|
|
2066
|
+
* Releases all stored files. Called by the core after `kubb:build:end`.
|
|
2067
|
+
*/
|
|
2068
|
+
dispose() {
|
|
2069
|
+
this.#cache.clear();
|
|
2070
|
+
this.#sorted = null;
|
|
2071
|
+
}
|
|
2072
|
+
/**
|
|
2073
|
+
* All stored files in stable sort order (shortest path first, barrel files
|
|
2074
|
+
* last within a length bucket). Returns a cached view, do not mutate.
|
|
2075
|
+
*/
|
|
2076
|
+
get files() {
|
|
2077
|
+
return this.#sorted ??= [...this.#cache.values()].sort(compareFiles);
|
|
2078
|
+
}
|
|
2079
|
+
/**
|
|
2080
|
+
* Converts a file's AST sources (or its `copy` source) into the final on-disk string.
|
|
2081
|
+
*/
|
|
2082
|
+
async parse(file, { parsers } = {}) {
|
|
2083
|
+
const parser = parsers?.get(file.extname);
|
|
2084
|
+
if (file.copy) return parseCopy(file, parser);
|
|
2085
|
+
if (!parser) return joinSources(file);
|
|
2086
|
+
return runParser(parser, () => parser.parse(file));
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Parses and writes every file through a bounded pool of workers. A small spec runs all its files
|
|
2090
|
+
* at once; a spec with thousands of files keeps at most {@link FILE_CONCURRENCY} parsed sources in
|
|
2091
|
+
* memory rather than holding every source, while still overlapping each file's write with the
|
|
2092
|
+
* next file's parse. Files finish in whatever order they parse, so the update rows are sorted back
|
|
2093
|
+
* into input order before `kubb:files:processing:update` fires once with the whole batch.
|
|
2094
|
+
*
|
|
2095
|
+
* A file the storage already holds is skipped, so a rebuild that generates identical output
|
|
2096
|
+
* writes nothing and leaves every mtime where it was.
|
|
2097
|
+
*/
|
|
2098
|
+
async write(files, { storage, parsers, manifest, signal, hooks, config }) {
|
|
2099
|
+
if (files.length === 0) return;
|
|
2100
|
+
await hooks?.callHook("kubb:files:processing:start", { files });
|
|
2101
|
+
const total = files.length;
|
|
2102
|
+
const updates = [];
|
|
2103
|
+
await inParallel({
|
|
2104
|
+
items: files,
|
|
2105
|
+
limit: 50,
|
|
2106
|
+
run: async (file, index) => {
|
|
2107
|
+
signal?.throwIfAborted();
|
|
2108
|
+
const source = await this.parse(file, { parsers });
|
|
2109
|
+
if (config) updates.push({
|
|
2110
|
+
file,
|
|
2111
|
+
processed: index + 1,
|
|
2112
|
+
total,
|
|
2113
|
+
percentage: (index + 1) / total * 100,
|
|
2114
|
+
config
|
|
2115
|
+
});
|
|
2116
|
+
if (!source) return;
|
|
2117
|
+
signal?.throwIfAborted();
|
|
2118
|
+
const stored = await storage.readItem(file.path);
|
|
2119
|
+
if (isUnchanged({
|
|
2120
|
+
stored,
|
|
2121
|
+
source,
|
|
2122
|
+
key: file.path,
|
|
2123
|
+
manifest
|
|
2124
|
+
})) return;
|
|
2125
|
+
signal?.throwIfAborted();
|
|
2126
|
+
await storage.writeItem(file.path, source, { stored });
|
|
2127
|
+
manifest?.track({
|
|
2128
|
+
key: file.path,
|
|
2129
|
+
source
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
});
|
|
2133
|
+
updates.sort((a, b) => a.processed - b.processed);
|
|
2134
|
+
await hooks?.callHook("kubb:files:processing:update", { files: updates });
|
|
2135
|
+
await hooks?.callHook("kubb:files:processing:end", { files });
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
//#endregion
|
|
2139
|
+
//#region src/nodeCache.ts
|
|
2140
|
+
/**
|
|
2141
|
+
* Builds an empty {@link NodeCache} backed by a plain `Map`. Called once per node in the generate
|
|
2142
|
+
* walk.
|
|
109
2143
|
*/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
2144
|
+
function createNodeCache() {
|
|
2145
|
+
const store = /* @__PURE__ */ new Map();
|
|
2146
|
+
return {
|
|
2147
|
+
readItem(key) {
|
|
2148
|
+
return store.get(key);
|
|
2149
|
+
},
|
|
2150
|
+
writeItem(key, value) {
|
|
2151
|
+
store.set(key, value);
|
|
2152
|
+
return value;
|
|
2153
|
+
},
|
|
2154
|
+
ensureItem(key, factory) {
|
|
2155
|
+
if (store.has(key)) return store.get(key);
|
|
2156
|
+
const value = factory();
|
|
2157
|
+
store.set(key, value);
|
|
2158
|
+
return value;
|
|
2159
|
+
}
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
//#endregion
|
|
2163
|
+
//#region src/KubbDriver.ts
|
|
2164
|
+
const ENFORCE_ORDER = {
|
|
2165
|
+
pre: -1,
|
|
2166
|
+
post: 1
|
|
2167
|
+
};
|
|
2168
|
+
/** `include` filter types that select operations, and so prune the schemas they do not reach. */
|
|
2169
|
+
const OPERATION_FILTER_TYPES = /* @__PURE__ */ new Set([
|
|
2170
|
+
"tag",
|
|
2171
|
+
"operationId",
|
|
2172
|
+
"path",
|
|
2173
|
+
"method",
|
|
2174
|
+
"contentType",
|
|
2175
|
+
"extension"
|
|
2176
|
+
]);
|
|
2177
|
+
const enforceWeight = (plugin) => plugin.enforce ? ENFORCE_ORDER[plugin.enforce] : 0;
|
|
115
2178
|
/**
|
|
116
|
-
*
|
|
2179
|
+
* The options bag a `NormalizedPlugin` starts with before a plugin refines it: a directory output
|
|
2180
|
+
* at the plugin root and empty filter lists.
|
|
117
2181
|
*/
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
2182
|
+
const defaultPluginOptions = () => ({
|
|
2183
|
+
output: {
|
|
2184
|
+
path: ".",
|
|
2185
|
+
mode: "directory"
|
|
2186
|
+
},
|
|
2187
|
+
exclude: [],
|
|
2188
|
+
override: []
|
|
2189
|
+
});
|
|
123
2190
|
/**
|
|
124
|
-
*
|
|
2191
|
+
* Fills in the `output`, `exclude`, and `override` a `NormalizedPlugin` needs from a plugin's raw
|
|
2192
|
+
* options, running `output` through `normalizeOutput`. Idempotent, so the driver can apply it after
|
|
2193
|
+
* `setOptions` has already run without disturbing an already-normalized bag.
|
|
125
2194
|
*/
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
2195
|
+
function normalizePluginOptions(rawOptions, pluginName) {
|
|
2196
|
+
const options = {
|
|
2197
|
+
...defaultPluginOptions(),
|
|
2198
|
+
...rawOptions ?? {}
|
|
2199
|
+
};
|
|
2200
|
+
const group = "group" in options ? options.group : void 0;
|
|
2201
|
+
options.output = normalizeOutput({
|
|
2202
|
+
output: options.output,
|
|
2203
|
+
group,
|
|
2204
|
+
pluginName
|
|
2205
|
+
});
|
|
2206
|
+
return options;
|
|
134
2207
|
}
|
|
135
2208
|
/**
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* Not memoized: a long-running host that probes repeatedly should cache the result itself, and a
|
|
139
|
-
* `--watch` build should keep noticing a tool installed mid-session.
|
|
2209
|
+
* Throws `KUBB_INVALID_PLUGIN_OPTIONS` when a `plugins` entry is not a plugin object, such as a
|
|
2210
|
+
* factory passed without calling it.
|
|
140
2211
|
*/
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
2212
|
+
function assertPluginEntry(entry, index) {
|
|
2213
|
+
if (typeof entry === "function") {
|
|
2214
|
+
const name = entry.name || "pluginName";
|
|
2215
|
+
throw new Diagnostics.Error({
|
|
2216
|
+
code: Diagnostics.code.invalidPluginOptions,
|
|
2217
|
+
severity: "error",
|
|
2218
|
+
message: `plugins[${index}] is a function ("${name}"), not a plugin.`,
|
|
2219
|
+
help: `Call it: ${name}()`,
|
|
2220
|
+
location: { kind: "config" }
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
if (typeof entry !== "object" || entry === null) throw new Diagnostics.Error({
|
|
2224
|
+
code: Diagnostics.code.invalidPluginOptions,
|
|
2225
|
+
severity: "error",
|
|
2226
|
+
message: `plugins[${index}] is ${entry === null ? "null" : typeof entry}, not a plugin.`,
|
|
2227
|
+
help: "Pass the result of a plugin factory, for example `pluginTs()`, or remove the entry.",
|
|
2228
|
+
location: { kind: "config" }
|
|
2229
|
+
});
|
|
144
2230
|
}
|
|
145
2231
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* @example
|
|
149
|
-
* ```ts
|
|
150
|
-
* tokenize('git commit -m "initial commit"')
|
|
151
|
-
* // → ['git', 'commit', '-m', 'initial commit']
|
|
152
|
-
* ```
|
|
2232
|
+
* Where a generator crashed: a label naming the node and, when the node has one, a JSON pointer
|
|
2233
|
+
* to it. Built from the node fields so core stays adapter-agnostic.
|
|
153
2234
|
*/
|
|
154
|
-
function
|
|
155
|
-
|
|
2235
|
+
function getCrashSite(node) {
|
|
2236
|
+
if (node.kind === "Schema") return node.name ? {
|
|
2237
|
+
label: `schema "${node.name}"`,
|
|
2238
|
+
location: {
|
|
2239
|
+
kind: "schema",
|
|
2240
|
+
pointer: `#/components/schemas/${escapePointerToken(node.name)}`
|
|
2241
|
+
}
|
|
2242
|
+
} : { label: "an inline schema" };
|
|
2243
|
+
if (!node.method || !node.path) return { label: `operation "${node.operationId}"` };
|
|
2244
|
+
return {
|
|
2245
|
+
label: `operation "${node.operationId}" (${node.method.toUpperCase()} ${node.path})`,
|
|
2246
|
+
location: {
|
|
2247
|
+
kind: "operation",
|
|
2248
|
+
pointer: `#/paths/${escapePointerToken(node.path)}/${node.method.toLowerCase()}`
|
|
2249
|
+
}
|
|
2250
|
+
};
|
|
156
2251
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
*
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*/
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
2252
|
+
var KubbDriver = class {
|
|
2253
|
+
config;
|
|
2254
|
+
options;
|
|
2255
|
+
/**
|
|
2256
|
+
* The `InputNode` produced by the adapter. Set after adapter setup.
|
|
2257
|
+
*/
|
|
2258
|
+
inputNode = null;
|
|
2259
|
+
adapter = null;
|
|
2260
|
+
/**
|
|
2261
|
+
* Raw adapter source so `adapter.parse()` can run lazily.
|
|
2262
|
+
* Intentionally outlives the build, cleared by `dispose()`.
|
|
2263
|
+
*/
|
|
2264
|
+
#adapterSource = null;
|
|
2265
|
+
/**
|
|
2266
|
+
* Central file store for all generated files.
|
|
2267
|
+
* Plugins should use `this.addFile()` / `this.upsertFile()` (via their context) to
|
|
2268
|
+
* add files. This property gives direct read/write access when needed.
|
|
2269
|
+
*/
|
|
2270
|
+
fileManager = new FileManager();
|
|
2271
|
+
plugins = /* @__PURE__ */ new Map();
|
|
2272
|
+
/**
|
|
2273
|
+
* Removers for every listener the driver added (plugin, generator) so `dispose()` can detach
|
|
2274
|
+
* them in one pass. External `hooks.hook(...)` listeners are not tracked.
|
|
2275
|
+
*/
|
|
2276
|
+
#unhooks = [];
|
|
2277
|
+
constructor(config, options) {
|
|
2278
|
+
this.config = config;
|
|
2279
|
+
this.options = options;
|
|
2280
|
+
this.adapter = config.adapter ?? null;
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* Checks the adapter's and parsers' `meta.compatibility`, then normalizes and checks every configured plugin, orders them, and registers their lifecycle handlers.
|
|
2284
|
+
* A plugin that another lists as a dependency runs first, then `enforce: 'pre'` before
|
|
2285
|
+
* `'post'`. When the config has an adapter, the adapter source is resolved from the input
|
|
2286
|
+
* so `run` can parse it later.
|
|
2287
|
+
*/
|
|
2288
|
+
async setup() {
|
|
2289
|
+
this.options.signal?.throwIfAborted();
|
|
2290
|
+
for (const extension of [this.config.adapter, ...this.config.parsers]) if (extension?.meta) checkCompatibility(extension.meta);
|
|
2291
|
+
const names = /* @__PURE__ */ new Set();
|
|
2292
|
+
const normalized = this.#sortPlugins(this.config.plugins.map((rawPlugin, index) => {
|
|
2293
|
+
assertPluginEntry(rawPlugin, index);
|
|
2294
|
+
if (rawPlugin.meta) checkCompatibility(rawPlugin.meta);
|
|
2295
|
+
const meta = resolveMeta(rawPlugin);
|
|
2296
|
+
if (names.has(meta.name)) throw new Diagnostics.Error({
|
|
2297
|
+
code: Diagnostics.code.invalidPluginOptions,
|
|
2298
|
+
severity: "error",
|
|
2299
|
+
message: `Plugin "${meta.name}" appears twice in \`plugins\`.`,
|
|
2300
|
+
help: "Remove the duplicate, or merge its options.",
|
|
2301
|
+
location: { kind: "config" }
|
|
2302
|
+
});
|
|
2303
|
+
names.add(meta.name);
|
|
2304
|
+
return {
|
|
2305
|
+
name: meta.name,
|
|
2306
|
+
meta,
|
|
2307
|
+
dependencies: typeof rawPlugin.dependencies === "function" ? rawPlugin.dependencies(rawPlugin.options ?? {}, { config: this.config }) : rawPlugin.dependencies,
|
|
2308
|
+
enforce: rawPlugin.enforce,
|
|
2309
|
+
hooks: rawPlugin.hooks,
|
|
2310
|
+
setup: rawPlugin.setup,
|
|
2311
|
+
options: rawPlugin.options ?? defaultPluginOptions(),
|
|
2312
|
+
resolver: createResolver({ pluginName: meta.name })
|
|
2313
|
+
};
|
|
2314
|
+
}));
|
|
2315
|
+
const byName = new Map(normalized.map((plugin) => [plugin.name, plugin]));
|
|
2316
|
+
for (const plugin of normalized) checkDependencies({
|
|
2317
|
+
name: plugin.name,
|
|
2318
|
+
dependencies: plugin.dependencies,
|
|
2319
|
+
plugins: byName
|
|
2320
|
+
});
|
|
2321
|
+
for (const plugin of normalized) {
|
|
2322
|
+
this.#registerPlugin(plugin);
|
|
2323
|
+
this.plugins.set(plugin.name, plugin);
|
|
2324
|
+
}
|
|
2325
|
+
if (this.config.adapter) this.#adapterSource = inputToAdapterSource(this.config);
|
|
2326
|
+
}
|
|
2327
|
+
/**
|
|
2328
|
+
* Orders plugins so every dependency runs before its dependents (Kahn's algorithm), with
|
|
2329
|
+
* `enforce` (`'pre'` before normal before `'post'`) and declaration order as tiebreaks.
|
|
2330
|
+
* A pairwise `Array.sort` comparator cannot do this: dependency relations are not transitive
|
|
2331
|
+
* at the comparator level, so a chain where A depends on B and B depends on C could come out
|
|
2332
|
+
* wrong when A and C are never compared directly. Dependencies on plugins missing from the
|
|
2333
|
+
* config are ignored here and surface later through `requirePlugin`.
|
|
2334
|
+
*/
|
|
2335
|
+
#sortPlugins(plugins) {
|
|
2336
|
+
const queue = [...plugins].sort((a, b) => enforceWeight(a) - enforceWeight(b));
|
|
2337
|
+
const names = new Set(queue.map((plugin) => plugin.name));
|
|
2338
|
+
const blockedBy = new Map(queue.map((plugin) => {
|
|
2339
|
+
const dependencies = Array.isArray(plugin.dependencies) ? plugin.dependencies : Object.keys(plugin.dependencies ?? {});
|
|
2340
|
+
return [plugin.name, new Set(dependencies.filter((name) => names.has(name) && name !== plugin.name))];
|
|
2341
|
+
}));
|
|
2342
|
+
const sorted = [];
|
|
2343
|
+
for (const _ of plugins) {
|
|
2344
|
+
const index = queue.findIndex((plugin) => blockedBy.get(plugin.name)?.size === 0);
|
|
2345
|
+
if (index === -1) throw new Diagnostics.Error({
|
|
2346
|
+
code: Diagnostics.code.invalidPluginOptions,
|
|
2347
|
+
severity: "error",
|
|
2348
|
+
message: `Plugin dependencies form a cycle: ${queue.map((plugin) => plugin.name).join(" → ")}.`,
|
|
2349
|
+
help: "Remove one of the `dependencies` entries so the plugins can be ordered.",
|
|
2350
|
+
location: { kind: "config" }
|
|
2351
|
+
});
|
|
2352
|
+
const [plugin] = queue.splice(index, 1);
|
|
2353
|
+
if (!plugin) break;
|
|
2354
|
+
sorted.push(plugin);
|
|
2355
|
+
for (const blockers of blockedBy.values()) blockers.delete(plugin.name);
|
|
2356
|
+
}
|
|
2357
|
+
return sorted;
|
|
2358
|
+
}
|
|
2359
|
+
get hooks() {
|
|
2360
|
+
return this.options.hooks;
|
|
2361
|
+
}
|
|
2362
|
+
/**
|
|
2363
|
+
* Parses the adapter source into `this.inputNode`. Idempotent, so repeated calls from
|
|
2364
|
+
* `run` do not re-parse.
|
|
2365
|
+
*/
|
|
2366
|
+
async #parseInput() {
|
|
2367
|
+
if (this.inputNode || !this.adapter || !this.#adapterSource) return;
|
|
2368
|
+
this.options.signal?.throwIfAborted();
|
|
2369
|
+
try {
|
|
2370
|
+
this.inputNode = await this.adapter.parse(this.#adapterSource, { signal: this.options.signal });
|
|
2371
|
+
} catch (caughtError) {
|
|
2372
|
+
throw new Diagnostics.Error(Diagnostics.fromExtension(caughtError, {
|
|
2373
|
+
kind: "adapter",
|
|
2374
|
+
extension: this.adapter
|
|
2375
|
+
}));
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
/**
|
|
2379
|
+
* Registers a plugin's lifecycle hooks on the shared `Hookable` as pass-through listeners that
|
|
2380
|
+
* external tooling can observe via `hooks.hook(...)`. The returned remover is tracked for
|
|
2381
|
+
* `dispose`. `kubb:plugin:setup` is skipped here; `setupHooks` invokes it directly with a
|
|
2382
|
+
* plugin-scoped context.
|
|
2383
|
+
*
|
|
2384
|
+
* @internal
|
|
2385
|
+
*/
|
|
2386
|
+
#registerPlugin(plugin) {
|
|
2387
|
+
const { hooks } = plugin;
|
|
2388
|
+
if (!hooks) return;
|
|
2389
|
+
const { "kubb:plugin:setup": _setup, ...configHooks } = hooks;
|
|
2390
|
+
this.#unhooks.push(this.hooks.addHooks(configHooks));
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Runs each plugin's `setup` and `kubb:plugin:setup` handler, in plugin order, with a context scoped to that
|
|
2394
|
+
* plugin so `addGenerator`, `setResolver`, `addMacro`, `setMacros`, and `setOptions` target its
|
|
2395
|
+
* `NormalizedPlugin` entry. Called once from `run` before the plugin execution loop begins, so
|
|
2396
|
+
* plugins can configure generators, resolvers, macros, and options before `buildStart`.
|
|
2397
|
+
*/
|
|
2398
|
+
async setupHooks() {
|
|
2399
|
+
for (const plugin of this.plugins.values()) {
|
|
2400
|
+
const setupHook = plugin.hooks?.["kubb:plugin:setup"];
|
|
2401
|
+
if (!plugin.setup && !setupHook) continue;
|
|
2402
|
+
const ctx = {
|
|
2403
|
+
config: this.config,
|
|
2404
|
+
options: plugin.options ?? {},
|
|
2405
|
+
addGenerator: (...generators) => {
|
|
2406
|
+
plugin.generators = [...plugin.generators ?? [], ...generators];
|
|
2407
|
+
},
|
|
2408
|
+
setResolver: (resolver) => {
|
|
2409
|
+
plugin.resolver = Resolver.merge(createResolver({ pluginName: plugin.name }), resolver);
|
|
2410
|
+
},
|
|
2411
|
+
addMacro: (macro) => {
|
|
2412
|
+
plugin.macros = [...plugin.macros ?? [], macro];
|
|
2413
|
+
},
|
|
2414
|
+
setMacros: (macros) => {
|
|
2415
|
+
plugin.macros = [...macros];
|
|
2416
|
+
},
|
|
2417
|
+
setOptions: (opts) => {
|
|
2418
|
+
plugin.options = normalizePluginOptions({
|
|
2419
|
+
...plugin.options,
|
|
2420
|
+
...opts
|
|
2421
|
+
}, plugin.name);
|
|
2422
|
+
},
|
|
2423
|
+
injectFile: (userFileNode) => {
|
|
2424
|
+
this.fileManager.add(_kubb_ast.ast.factory.createFile(userFileNode));
|
|
2425
|
+
},
|
|
2426
|
+
requirePlugin: ((name) => this.requirePlugin(name, { requiredBy: plugin.name }))
|
|
2427
|
+
};
|
|
2428
|
+
try {
|
|
2429
|
+
await kubbContext.run(ctx, async () => {
|
|
2430
|
+
if (plugin.setup) plugin.options = normalizePluginOptions(await plugin.setup(ctx.options, ctx), plugin.name);
|
|
2431
|
+
await setupHook?.(ctx);
|
|
2432
|
+
});
|
|
2433
|
+
} catch (caughtError) {
|
|
2434
|
+
throw new Diagnostics.Error(Diagnostics.fromExtension(caughtError, {
|
|
2435
|
+
kind: "plugin",
|
|
2436
|
+
extension: plugin
|
|
2437
|
+
}));
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
/**
|
|
2442
|
+
* Runs the full plugin pipeline. Returns the diagnostics collected so far even
|
|
2443
|
+
* when an outer hook throws, since the orchestrator preserves partial state by capturing
|
|
2444
|
+
* the failure as a {@link Diagnostic} instead of propagating. Each plugin also
|
|
2445
|
+
* contributes a `timing` diagnostic for the run summary.
|
|
2446
|
+
*/
|
|
2447
|
+
async run() {
|
|
2448
|
+
const { hooks, config, fileManager } = this;
|
|
2449
|
+
const diagnostics = [];
|
|
2450
|
+
const parsersMap = /* @__PURE__ */ new Map();
|
|
2451
|
+
for (const parser of config.parsers) if (parser.extNames) for (const ext of parser.extNames) parsersMap.set(ext, parser);
|
|
2452
|
+
return Diagnostics.scope((diagnostic) => diagnostics.push(diagnostic), async () => {
|
|
2453
|
+
try {
|
|
2454
|
+
this.options.signal?.throwIfAborted();
|
|
2455
|
+
const outputRoot = (0, node_path.resolve)(config.root, config.output.path);
|
|
2456
|
+
await this.#parseInput();
|
|
2457
|
+
await this.setupHooks();
|
|
2458
|
+
for (const plugin of this.plugins.values()) {
|
|
2459
|
+
this.options.signal?.throwIfAborted();
|
|
2460
|
+
plugin.options = normalizePluginOptions(plugin.options, plugin.name);
|
|
2461
|
+
}
|
|
2462
|
+
if (this.adapter && this.inputNode) {
|
|
2463
|
+
const buildStartContext = this.#withFiles({
|
|
2464
|
+
config,
|
|
2465
|
+
adapter: this.adapter,
|
|
2466
|
+
meta: this.inputNode.meta,
|
|
2467
|
+
getPlugin: this.getPlugin.bind(this)
|
|
2468
|
+
});
|
|
2469
|
+
await hooks.callHook("kubb:build:start", buildStartContext);
|
|
2470
|
+
}
|
|
2471
|
+
const generatorPlugins = [];
|
|
2472
|
+
for (const plugin of this.plugins.values()) {
|
|
2473
|
+
const context = this.getContext(plugin);
|
|
2474
|
+
const hrStart = process.hrtime();
|
|
2475
|
+
try {
|
|
2476
|
+
await hooks.callHook("kubb:plugin:start", { plugin });
|
|
2477
|
+
} catch (caughtError) {
|
|
2478
|
+
const error = toError(caughtError);
|
|
2479
|
+
const duration = getElapsedMs(hrStart);
|
|
2480
|
+
await this.#emitPluginEnd({
|
|
2481
|
+
plugin,
|
|
2482
|
+
duration,
|
|
2483
|
+
success: false,
|
|
2484
|
+
error
|
|
2485
|
+
});
|
|
2486
|
+
diagnostics.push(Diagnostics.fromExtension(error, {
|
|
2487
|
+
kind: "plugin",
|
|
2488
|
+
extension: plugin
|
|
2489
|
+
}), Diagnostics.performance({
|
|
2490
|
+
plugin: plugin.name,
|
|
2491
|
+
duration
|
|
2492
|
+
}));
|
|
2493
|
+
continue;
|
|
2494
|
+
}
|
|
2495
|
+
if ((plugin.generators?.length ?? 0) > 0) {
|
|
2496
|
+
generatorPlugins.push({
|
|
2497
|
+
plugin,
|
|
2498
|
+
context,
|
|
2499
|
+
hrStart
|
|
2500
|
+
});
|
|
2501
|
+
continue;
|
|
2502
|
+
}
|
|
2503
|
+
const duration = getElapsedMs(hrStart);
|
|
2504
|
+
diagnostics.push(Diagnostics.performance({
|
|
2505
|
+
plugin: plugin.name,
|
|
2506
|
+
duration
|
|
2507
|
+
}));
|
|
2508
|
+
await this.#emitPluginEnd({
|
|
2509
|
+
plugin,
|
|
2510
|
+
duration,
|
|
2511
|
+
success: true
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
diagnostics.push(...await this.#runGenerators(generatorPlugins));
|
|
2515
|
+
await hooks.callHook("kubb:plugins:end", this.#withFiles({ config }));
|
|
2516
|
+
if (this.options.cleanPath) await config.storage.empty(this.options.cleanPath);
|
|
2517
|
+
await fileManager.write(fileManager.files, {
|
|
2518
|
+
storage: config.storage,
|
|
2519
|
+
parsers: parsersMap,
|
|
2520
|
+
manifest: this.options.manifest,
|
|
2521
|
+
signal: this.options.signal,
|
|
2522
|
+
hooks,
|
|
2523
|
+
config
|
|
2524
|
+
});
|
|
2525
|
+
await hooks.callHook("kubb:build:end", {
|
|
2526
|
+
files: this.fileManager.files,
|
|
2527
|
+
config,
|
|
2528
|
+
outputDir: outputRoot
|
|
2529
|
+
});
|
|
2530
|
+
return { diagnostics: Diagnostics.dedupe(diagnostics) };
|
|
2531
|
+
} catch (caughtError) {
|
|
2532
|
+
diagnostics.push(Diagnostics.from(caughtError));
|
|
2533
|
+
return { diagnostics: Diagnostics.dedupe(diagnostics) };
|
|
2534
|
+
}
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2537
|
+
/**
|
|
2538
|
+
* Widens `extra` with the files present at emit time and a bound `upsertFile`, the shape every
|
|
2539
|
+
* file-carrying hook context shares. Building it here in one place keeps the `files` and
|
|
2540
|
+
* `upsertFile` keys from being dropped by a stray spread at the call site.
|
|
2541
|
+
*/
|
|
2542
|
+
#withFiles(extra) {
|
|
2543
|
+
return {
|
|
2544
|
+
...extra,
|
|
2545
|
+
files: this.fileManager.files,
|
|
2546
|
+
upsertFile: (...files) => this.fileManager.upsert(...files)
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
#emitPluginEnd({ plugin, duration, success, error }) {
|
|
2550
|
+
return this.hooks.callHook("kubb:plugin:end", this.#withFiles({
|
|
2551
|
+
plugin,
|
|
2552
|
+
duration,
|
|
2553
|
+
success,
|
|
2554
|
+
...error ? { error } : {},
|
|
2555
|
+
config: this.config
|
|
2556
|
+
}));
|
|
2557
|
+
}
|
|
2558
|
+
/**
|
|
2559
|
+
* Runs schemas and operations through every plugin's generators. The walk is node-outer: each
|
|
2560
|
+
* schema is visited once and each operation once, and the node fans out to the matching
|
|
2561
|
+
* generators of every plugin in dependency order. A per-node cache (`createNodeCache`) is created
|
|
2562
|
+
* once per node and shared by all of that node's plugins, so node-derived work is computed once
|
|
2563
|
+
* and reused instead of recomputed per plugin. Each node still runs through the plugin's macros
|
|
2564
|
+
* and its exclude/include/override filters before that plugin's
|
|
2565
|
+
* generator sees it, so plugins stay isolated. Schemas run before operations so file output
|
|
2566
|
+
* stays deterministic across runs. A generator with a `match` predicate that resolves `false`
|
|
2567
|
+
* for a node is skipped for that node, without calling `schema`/`operation`.
|
|
2568
|
+
*
|
|
2569
|
+
* A failing plugin is dropped from the remaining walk, contributes an error diagnostic, and no
|
|
2570
|
+
* longer aborts the other plugins. `kubb:plugin:end` and each plugin's `timing` diagnostic fire
|
|
2571
|
+
* in dependency order once the walk finishes, driving the CLI's `Plugins N/M` counter. The
|
|
2572
|
+
* `operations` batch fires once per plugin after the single operation walk.
|
|
2573
|
+
*
|
|
2574
|
+
* When `this.inputNode` is `null`, every entry still gets a `kubb:plugin:end` so
|
|
2575
|
+
* post-plugin listeners (the barrel writer and friends) complete.
|
|
2576
|
+
*/
|
|
2577
|
+
async #runGenerators(entries) {
|
|
2578
|
+
const diagnostics = [];
|
|
2579
|
+
if (entries.length === 0) return diagnostics;
|
|
2580
|
+
if (!this.inputNode) {
|
|
2581
|
+
for (const { plugin, hrStart } of entries) {
|
|
2582
|
+
const duration = getElapsedMs(hrStart);
|
|
2583
|
+
diagnostics.push(Diagnostics.performance({
|
|
2584
|
+
plugin: plugin.name,
|
|
2585
|
+
duration
|
|
2586
|
+
}));
|
|
2587
|
+
await this.#emitPluginEnd({
|
|
2588
|
+
plugin,
|
|
2589
|
+
duration,
|
|
2590
|
+
success: true
|
|
2591
|
+
});
|
|
2592
|
+
}
|
|
2593
|
+
return diagnostics;
|
|
2594
|
+
}
|
|
2595
|
+
const { schemas, operations } = this.inputNode;
|
|
2596
|
+
const allowedSchemaNamesByPlugin = /* @__PURE__ */ new Map();
|
|
2597
|
+
for (const { plugin } of entries) {
|
|
2598
|
+
const { exclude, include, override } = plugin.options;
|
|
2599
|
+
if (!((include?.some(({ type }) => OPERATION_FILTER_TYPES.has(type)) ?? false) && !(include?.some(({ type }) => type === "schemaName") ?? false))) continue;
|
|
2600
|
+
const resolver = this.getResolver(plugin.name);
|
|
2601
|
+
const includedOps = operations.filter((operation) => resolver.default.options(operation, {
|
|
2602
|
+
options: plugin.options,
|
|
2603
|
+
exclude,
|
|
2604
|
+
include,
|
|
2605
|
+
override
|
|
2606
|
+
}) !== null);
|
|
2607
|
+
allowedSchemaNamesByPlugin.set(plugin.name, (0, _kubb_ast.collectUsedSchemaNames)(includedOps, schemas));
|
|
2608
|
+
}
|
|
2609
|
+
const states = entries.map(({ plugin, context, hrStart }) => {
|
|
2610
|
+
const generatorContext = {
|
|
2611
|
+
...context,
|
|
2612
|
+
resolver: this.getResolver(plugin.name)
|
|
2613
|
+
};
|
|
2614
|
+
const { exclude, include, override } = plugin.options;
|
|
2615
|
+
const generators = plugin.generators ?? [];
|
|
2616
|
+
return {
|
|
2617
|
+
plugin,
|
|
2618
|
+
hrStart,
|
|
2619
|
+
generatorContext,
|
|
2620
|
+
exclude,
|
|
2621
|
+
include,
|
|
2622
|
+
override,
|
|
2623
|
+
optionsAreStatic: !exclude?.length && !include?.length && !override?.length,
|
|
2624
|
+
allowedSchemaNames: allowedSchemaNamesByPlugin.get(plugin.name) ?? null,
|
|
2625
|
+
schemaGenerators: generators.filter((generator) => generator.schema),
|
|
2626
|
+
operationGenerators: generators.filter((generator) => generator.operation),
|
|
2627
|
+
operationsGenerators: generators.filter((generator) => generator.operations),
|
|
2628
|
+
pluginOperations: [],
|
|
2629
|
+
error: null,
|
|
2630
|
+
crashSite: void 0
|
|
2631
|
+
};
|
|
2632
|
+
});
|
|
2633
|
+
const resolveForPlugin = (state, node) => {
|
|
2634
|
+
const transformedNode = _kubb_ast.ast.applyMacros(node, state.plugin.macros ?? []);
|
|
2635
|
+
if (state.optionsAreStatic) return {
|
|
2636
|
+
transformedNode,
|
|
2637
|
+
options: state.plugin.options
|
|
2638
|
+
};
|
|
2639
|
+
const options = state.generatorContext.resolver.default.options(transformedNode, {
|
|
2640
|
+
options: state.plugin.options,
|
|
2641
|
+
exclude: state.exclude,
|
|
2642
|
+
include: state.include,
|
|
2643
|
+
override: state.override
|
|
2644
|
+
});
|
|
2645
|
+
if (options === null) return null;
|
|
2646
|
+
return {
|
|
2647
|
+
transformedNode,
|
|
2648
|
+
options
|
|
2649
|
+
};
|
|
2650
|
+
};
|
|
2651
|
+
for (const node of schemas) {
|
|
2652
|
+
const cache = createNodeCache();
|
|
2653
|
+
for (const state of states) {
|
|
2654
|
+
if (state.error || !state.schemaGenerators.length) continue;
|
|
2655
|
+
try {
|
|
2656
|
+
const resolved = resolveForPlugin(state, node);
|
|
2657
|
+
if (!resolved) continue;
|
|
2658
|
+
const { transformedNode, options } = resolved;
|
|
2659
|
+
if (state.allowedSchemaNames !== null && transformedNode.name && !state.allowedSchemaNames.has(transformedNode.name)) continue;
|
|
2660
|
+
const ctx = {
|
|
2661
|
+
...state.generatorContext,
|
|
2662
|
+
options,
|
|
2663
|
+
cache
|
|
2664
|
+
};
|
|
2665
|
+
await this.#dispatchNode({
|
|
2666
|
+
generators: state.schemaGenerators,
|
|
2667
|
+
node: transformedNode,
|
|
2668
|
+
ctx,
|
|
2669
|
+
generate: (generator) => generator.schema(transformedNode, ctx)
|
|
2670
|
+
});
|
|
2671
|
+
await this.hooks.callHook("kubb:generate:schema", transformedNode, ctx);
|
|
2672
|
+
} catch (caughtError) {
|
|
2673
|
+
state.error = toError(caughtError);
|
|
2674
|
+
state.crashSite = getCrashSite(node);
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
for (const node of operations) {
|
|
2679
|
+
const cache = createNodeCache();
|
|
2680
|
+
for (const state of states) {
|
|
2681
|
+
if (state.error || !state.operationGenerators.length && !state.operationsGenerators.length) continue;
|
|
2682
|
+
try {
|
|
2683
|
+
const resolved = resolveForPlugin(state, node);
|
|
2684
|
+
if (!resolved) continue;
|
|
2685
|
+
state.pluginOperations.push(resolved.transformedNode);
|
|
2686
|
+
if (state.operationGenerators.length) {
|
|
2687
|
+
const { transformedNode } = resolved;
|
|
2688
|
+
const ctx = {
|
|
2689
|
+
...state.generatorContext,
|
|
2690
|
+
options: resolved.options,
|
|
2691
|
+
cache
|
|
2692
|
+
};
|
|
2693
|
+
await this.#dispatchNode({
|
|
2694
|
+
generators: state.operationGenerators,
|
|
2695
|
+
node: transformedNode,
|
|
2696
|
+
ctx,
|
|
2697
|
+
generate: (generator) => generator.operation(transformedNode, ctx)
|
|
2698
|
+
});
|
|
2699
|
+
await this.hooks.callHook("kubb:generate:operation", transformedNode, ctx);
|
|
2700
|
+
}
|
|
2701
|
+
} catch (caughtError) {
|
|
2702
|
+
state.error = toError(caughtError);
|
|
2703
|
+
state.crashSite = getCrashSite(node);
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
for (const state of states) {
|
|
2708
|
+
if (state.error || !state.operationsGenerators.length) continue;
|
|
2709
|
+
try {
|
|
2710
|
+
const ctx = {
|
|
2711
|
+
...state.generatorContext,
|
|
2712
|
+
options: state.plugin.options,
|
|
2713
|
+
cache: createNodeCache()
|
|
2714
|
+
};
|
|
2715
|
+
for (const generator of state.operationsGenerators) await this.fileManager.dispatch({
|
|
2716
|
+
result: await generator.operations(state.pluginOperations, ctx),
|
|
2717
|
+
renderer: generator.renderer
|
|
2718
|
+
});
|
|
2719
|
+
await this.hooks.callHook("kubb:generate:operations", state.pluginOperations, ctx);
|
|
2720
|
+
} catch (caughtError) {
|
|
2721
|
+
state.error = toError(caughtError);
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
for (const state of states) {
|
|
2725
|
+
const duration = getElapsedMs(state.hrStart);
|
|
2726
|
+
await this.#emitPluginEnd({
|
|
2727
|
+
plugin: state.plugin,
|
|
2728
|
+
duration,
|
|
2729
|
+
success: !state.error,
|
|
2730
|
+
error: state.error ?? void 0
|
|
2731
|
+
});
|
|
2732
|
+
if (state.error) {
|
|
2733
|
+
const diagnostic = Diagnostics.fromExtension(state.error, {
|
|
2734
|
+
kind: "plugin",
|
|
2735
|
+
extension: state.plugin
|
|
2736
|
+
});
|
|
2737
|
+
const site = diagnostic.code === Diagnostics.code.pluginFailed && !diagnostic.location ? state.crashSite : void 0;
|
|
2738
|
+
diagnostics.push(site ? {
|
|
2739
|
+
...diagnostic,
|
|
2740
|
+
message: `While generating ${site.label}: ${diagnostic.message}`,
|
|
2741
|
+
...site.location ? { location: site.location } : {}
|
|
2742
|
+
} : diagnostic);
|
|
2743
|
+
}
|
|
2744
|
+
diagnostics.push(Diagnostics.performance({
|
|
2745
|
+
plugin: state.plugin.name,
|
|
2746
|
+
duration
|
|
2747
|
+
}));
|
|
2748
|
+
}
|
|
2749
|
+
return diagnostics;
|
|
2750
|
+
}
|
|
2751
|
+
/** Runs `node` through the generators whose `match` accepts it; `generate` picks the method for the node kind. */
|
|
2752
|
+
async #dispatchNode({ generators, node, ctx, generate }) {
|
|
2753
|
+
for (const generator of generators) {
|
|
2754
|
+
if (!(generator.match ? await generator.match(node, ctx) : true)) continue;
|
|
2755
|
+
await this.fileManager.dispatch({
|
|
2756
|
+
result: await generate(generator),
|
|
2757
|
+
renderer: generator.renderer
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
/**
|
|
2762
|
+
* Removes every listener the driver added. Listeners attached directly to `hooks` from outside
|
|
2763
|
+
* the driver survive. Called at the end of a build to prevent leaks across repeated builds.
|
|
2764
|
+
*
|
|
2765
|
+
* @internal
|
|
2766
|
+
*/
|
|
2767
|
+
dispose() {
|
|
2768
|
+
for (const unhook of this.#unhooks) unhook();
|
|
2769
|
+
this.#unhooks.length = 0;
|
|
2770
|
+
this.fileManager.dispose();
|
|
2771
|
+
this.inputNode = null;
|
|
2772
|
+
this.#adapterSource = null;
|
|
2773
|
+
}
|
|
2774
|
+
[Symbol.dispose]() {
|
|
2775
|
+
this.dispose();
|
|
2776
|
+
}
|
|
2777
|
+
getResolver(pluginName) {
|
|
2778
|
+
return this.plugins.get(pluginName)?.resolver ?? createResolver({ pluginName });
|
|
2779
|
+
}
|
|
2780
|
+
getContext(plugin) {
|
|
2781
|
+
const driver = this;
|
|
2782
|
+
const report = (diagnostic) => {
|
|
2783
|
+
Diagnostics.report({
|
|
2784
|
+
...diagnostic,
|
|
2785
|
+
plugin: plugin.name
|
|
2786
|
+
});
|
|
2787
|
+
};
|
|
2788
|
+
return {
|
|
2789
|
+
config: driver.config,
|
|
2790
|
+
signal: driver.options.signal,
|
|
2791
|
+
get root() {
|
|
2792
|
+
return (0, node_path.resolve)(driver.config.root, driver.config.output.path);
|
|
2793
|
+
},
|
|
2794
|
+
hooks: driver.hooks,
|
|
2795
|
+
plugin,
|
|
2796
|
+
getPlugin: driver.getPlugin.bind(driver),
|
|
2797
|
+
requirePlugin: ((name) => driver.requirePlugin(name, { requiredBy: plugin.name })),
|
|
2798
|
+
getResolver: driver.getResolver.bind(driver),
|
|
2799
|
+
driver,
|
|
2800
|
+
addFile: async (...files) => {
|
|
2801
|
+
driver.fileManager.add(...files);
|
|
2802
|
+
},
|
|
2803
|
+
upsertFile: async (...files) => {
|
|
2804
|
+
driver.fileManager.upsert(...files);
|
|
2805
|
+
},
|
|
2806
|
+
get meta() {
|
|
2807
|
+
return driver.inputNode?.meta ?? {
|
|
2808
|
+
circularNames: [],
|
|
2809
|
+
enumNames: []
|
|
2810
|
+
};
|
|
2811
|
+
},
|
|
2812
|
+
get adapter() {
|
|
2813
|
+
return driver.adapter;
|
|
2814
|
+
},
|
|
2815
|
+
get resolver() {
|
|
2816
|
+
return driver.getResolver(plugin.name);
|
|
2817
|
+
},
|
|
2818
|
+
warn(message, options) {
|
|
2819
|
+
report({
|
|
2820
|
+
code: Diagnostics.code.pluginWarning,
|
|
2821
|
+
severity: "warning",
|
|
2822
|
+
message,
|
|
2823
|
+
...options
|
|
2824
|
+
});
|
|
2825
|
+
},
|
|
2826
|
+
error(error, options) {
|
|
2827
|
+
if (Diagnostics.isError(error)) {
|
|
2828
|
+
report({
|
|
2829
|
+
...options,
|
|
2830
|
+
...error.diagnostic
|
|
2831
|
+
});
|
|
2832
|
+
return;
|
|
2833
|
+
}
|
|
2834
|
+
const cause = typeof error === "string" ? void 0 : error;
|
|
2835
|
+
report({
|
|
2836
|
+
code: Diagnostics.code.pluginFailed,
|
|
2837
|
+
severity: "error",
|
|
2838
|
+
message: typeof error === "string" ? error : error.message,
|
|
2839
|
+
cause,
|
|
2840
|
+
...options
|
|
2841
|
+
});
|
|
2842
|
+
},
|
|
2843
|
+
info(message, options) {
|
|
2844
|
+
report({
|
|
2845
|
+
code: Diagnostics.code.pluginInfo,
|
|
2846
|
+
severity: "info",
|
|
2847
|
+
message,
|
|
2848
|
+
...options
|
|
2849
|
+
});
|
|
2850
|
+
}
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
getPlugin(pluginName) {
|
|
2854
|
+
return this.plugins.get(pluginName);
|
|
2855
|
+
}
|
|
2856
|
+
requirePlugin(pluginName, context) {
|
|
2857
|
+
const plugin = this.getPlugin(pluginName);
|
|
2858
|
+
if (plugin) return plugin;
|
|
2859
|
+
throw pluginNotFoundError({
|
|
2860
|
+
pluginName,
|
|
2861
|
+
requiredBy: context?.requiredBy
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
};
|
|
2865
|
+
/** The `KUBB_PLUGIN_NOT_FOUND` error `requirePlugin` throws, naming the plugin that needed it. */
|
|
2866
|
+
function pluginNotFoundError({ pluginName, requiredBy }) {
|
|
2867
|
+
const by = requiredBy ? ` by "${requiredBy}"` : "";
|
|
2868
|
+
const help = requiredBy ? ` (required by "${requiredBy}")` : "";
|
|
2869
|
+
return new Diagnostics.Error({
|
|
2870
|
+
code: Diagnostics.code.pluginNotFound,
|
|
2871
|
+
severity: "error",
|
|
2872
|
+
message: `Plugin "${pluginName}" is required${by} but not found. Make sure it is included in your Kubb config.`,
|
|
2873
|
+
help: `Add "${pluginName}" to the \`plugins\` array in kubb.config.ts${help}, or remove the dependency on it.`,
|
|
2874
|
+
location: { kind: "config" },
|
|
2875
|
+
...requiredBy ? { plugin: requiredBy } : {}
|
|
173
2876
|
});
|
|
174
2877
|
}
|
|
175
2878
|
//#endregion
|
|
@@ -215,7 +2918,7 @@ async function createOutputManifest({ storage, cache }) {
|
|
|
215
2918
|
async commit() {
|
|
216
2919
|
try {
|
|
217
2920
|
const next = { ...entries };
|
|
218
|
-
await
|
|
2921
|
+
await inParallel({
|
|
219
2922
|
items: [...tracked],
|
|
220
2923
|
limit: 50,
|
|
221
2924
|
run: async ([key, source]) => {
|
|
@@ -311,7 +3014,7 @@ const fsStorage = createStorage(() => ({
|
|
|
311
3014
|
}
|
|
312
3015
|
},
|
|
313
3016
|
async writeItem(key, value, options) {
|
|
314
|
-
await
|
|
3017
|
+
await write((0, node_path.resolve)(key), value, {
|
|
315
3018
|
sanity: false,
|
|
316
3019
|
stored: options?.stored
|
|
317
3020
|
});
|
|
@@ -326,7 +3029,7 @@ const fsStorage = createStorage(() => ({
|
|
|
326
3029
|
for await (const entry of (0, node_fs_promises.glob)("**/*", {
|
|
327
3030
|
cwd: resolvedBase,
|
|
328
3031
|
withFileTypes: true
|
|
329
|
-
})) if (entry.isFile()) keys.push(
|
|
3032
|
+
})) if (entry.isFile()) keys.push(toPosixPath((0, node_path.relative)(resolvedBase, (0, node_path.join)(entry.parentPath, entry.name))));
|
|
330
3033
|
} catch (_error) {}
|
|
331
3034
|
return keys;
|
|
332
3035
|
},
|
|
@@ -379,6 +3082,113 @@ const cacheStorage = createStorage(({ root = process.cwd() }) => {
|
|
|
379
3082
|
};
|
|
380
3083
|
});
|
|
381
3084
|
//#endregion
|
|
3085
|
+
//#region src/Hookable.ts
|
|
3086
|
+
/**
|
|
3087
|
+
* Typed hook emitter that awaits all async listeners before resolving.
|
|
3088
|
+
*
|
|
3089
|
+
* @example
|
|
3090
|
+
* ```ts
|
|
3091
|
+
* const hooks = new Hookable<{ build: [name: string] }>()
|
|
3092
|
+
* hooks.hook('build', async (name) => { console.log(name) })
|
|
3093
|
+
* await hooks.callHook('build', 'petstore') // all listeners awaited
|
|
3094
|
+
* ```
|
|
3095
|
+
*/
|
|
3096
|
+
var Hookable = class {
|
|
3097
|
+
#listeners = /* @__PURE__ */ new Map();
|
|
3098
|
+
/**
|
|
3099
|
+
* Calls `hookName` and awaits all registered listeners sequentially.
|
|
3100
|
+
* Throws if any listener rejects, wrapping the cause in an error that names the hook.
|
|
3101
|
+
*
|
|
3102
|
+
* @example
|
|
3103
|
+
* ```ts
|
|
3104
|
+
* await hooks.callHook('build', 'petstore')
|
|
3105
|
+
* ```
|
|
3106
|
+
*/
|
|
3107
|
+
callHook(hookName, ...hookArgs) {
|
|
3108
|
+
const listeners = this.#listeners.get(hookName);
|
|
3109
|
+
if (!listeners?.size) return;
|
|
3110
|
+
return this.#emitAll(hookName, [...listeners], hookArgs);
|
|
3111
|
+
}
|
|
3112
|
+
async #emitAll(hookName, listeners, hookArgs) {
|
|
3113
|
+
for (const listener of listeners) try {
|
|
3114
|
+
await listener(...hookArgs);
|
|
3115
|
+
} catch (err) {
|
|
3116
|
+
throw new Error(`Error in async listener for "${hookName}"`, { cause: toError(err) });
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
/**
|
|
3120
|
+
* Registers a persistent listener for `hookName` and returns a function that removes it.
|
|
3121
|
+
*
|
|
3122
|
+
* @example
|
|
3123
|
+
* ```ts
|
|
3124
|
+
* const unhook = hooks.hook('build', async (name) => { console.log(name) })
|
|
3125
|
+
* unhook() // removes it
|
|
3126
|
+
* ```
|
|
3127
|
+
*/
|
|
3128
|
+
hook(hookName, handler) {
|
|
3129
|
+
let listeners = this.#listeners.get(hookName);
|
|
3130
|
+
if (!listeners) this.#listeners.set(hookName, listeners = /* @__PURE__ */ new Set());
|
|
3131
|
+
listeners.add(handler);
|
|
3132
|
+
return () => this.removeHook(hookName, handler);
|
|
3133
|
+
}
|
|
3134
|
+
/**
|
|
3135
|
+
* Registers every handler in `configHooks` at once and returns a function that removes them
|
|
3136
|
+
* all. Undefined entries are skipped, so a partial hook object registers only its present keys.
|
|
3137
|
+
*
|
|
3138
|
+
* @example
|
|
3139
|
+
* ```ts
|
|
3140
|
+
* const unhook = hooks.addHooks({ build: onBuild, done: onDone })
|
|
3141
|
+
* unhook() // removes both
|
|
3142
|
+
* ```
|
|
3143
|
+
*/
|
|
3144
|
+
addHooks(configHooks) {
|
|
3145
|
+
const unhooks = Object.keys(configHooks).filter((name) => configHooks[name]).map((name) => this.hook(name, configHooks[name]));
|
|
3146
|
+
return () => {
|
|
3147
|
+
for (const unhook of unhooks) unhook();
|
|
3148
|
+
};
|
|
3149
|
+
}
|
|
3150
|
+
/**
|
|
3151
|
+
* Removes a previously registered listener.
|
|
3152
|
+
*
|
|
3153
|
+
* @example
|
|
3154
|
+
* ```ts
|
|
3155
|
+
* hooks.removeHook('build', handler)
|
|
3156
|
+
* ```
|
|
3157
|
+
*/
|
|
3158
|
+
removeHook(hookName, handler) {
|
|
3159
|
+
this.#listeners.get(hookName)?.delete(handler);
|
|
3160
|
+
}
|
|
3161
|
+
/**
|
|
3162
|
+
* Returns the number of listeners registered for `hookName`.
|
|
3163
|
+
*
|
|
3164
|
+
* @example
|
|
3165
|
+
* ```ts
|
|
3166
|
+
* hooks.hook('build', handler)
|
|
3167
|
+
* hooks.listenerCount('build') // 1
|
|
3168
|
+
* ```
|
|
3169
|
+
*/
|
|
3170
|
+
listenerCount(hookName) {
|
|
3171
|
+
return this.#listeners.get(hookName)?.size ?? 0;
|
|
3172
|
+
}
|
|
3173
|
+
/**
|
|
3174
|
+
* No-op kept for callers written against the `EventEmitter`-backed version. Listeners are unbounded.
|
|
3175
|
+
*
|
|
3176
|
+
* @deprecated There is no listener ceiling anymore.
|
|
3177
|
+
*/
|
|
3178
|
+
setMaxListeners(_max) {}
|
|
3179
|
+
/**
|
|
3180
|
+
* Removes all listeners from every hook channel.
|
|
3181
|
+
*
|
|
3182
|
+
* @example
|
|
3183
|
+
* ```ts
|
|
3184
|
+
* hooks.removeAllHooks()
|
|
3185
|
+
* ```
|
|
3186
|
+
*/
|
|
3187
|
+
removeAllHooks() {
|
|
3188
|
+
this.#listeners.clear();
|
|
3189
|
+
}
|
|
3190
|
+
};
|
|
3191
|
+
//#endregion
|
|
382
3192
|
//#region src/output/runHook.ts
|
|
383
3193
|
const KILL_GRACE_MS = 2e3;
|
|
384
3194
|
async function settle(proc, hooks, id, commandWithArgs) {
|
|
@@ -397,7 +3207,7 @@ async function settle(proc, hooks, id, commandWithArgs) {
|
|
|
397
3207
|
id,
|
|
398
3208
|
line
|
|
399
3209
|
})).catch((error) => {
|
|
400
|
-
lineError ??=
|
|
3210
|
+
lineError ??= toError(error);
|
|
401
3211
|
});
|
|
402
3212
|
});
|
|
403
3213
|
}
|
|
@@ -470,14 +3280,14 @@ const TOOL_PASSES = [{
|
|
|
470
3280
|
verb: "Formatting",
|
|
471
3281
|
tools: formatters,
|
|
472
3282
|
preference: FORMATTER_PREFERENCE,
|
|
473
|
-
code:
|
|
3283
|
+
code: Diagnostics.code.formatFailed
|
|
474
3284
|
}, {
|
|
475
3285
|
kind: "lint",
|
|
476
3286
|
label: "linter",
|
|
477
3287
|
verb: "Linting",
|
|
478
3288
|
tools: linters,
|
|
479
3289
|
preference: LINTER_PREFERENCE,
|
|
480
|
-
code:
|
|
3290
|
+
code: Diagnostics.code.lintFailed
|
|
481
3291
|
}];
|
|
482
3292
|
/**
|
|
483
3293
|
* Resolves `auto` to an installed tool and runs it. Returns the failure instead of throwing.
|
|
@@ -528,7 +3338,7 @@ async function runOutputPasses({ config, outputPath, hooks, signal }) {
|
|
|
528
3338
|
cause: error
|
|
529
3339
|
};
|
|
530
3340
|
diagnostics.push(diagnostic);
|
|
531
|
-
await
|
|
3341
|
+
await Diagnostics.emit(hooks, diagnostic);
|
|
532
3342
|
};
|
|
533
3343
|
for (const pass of TOOL_PASSES) {
|
|
534
3344
|
const setting = config.output[pass.kind];
|
|
@@ -565,7 +3375,7 @@ async function runOutputPasses({ config, outputPath, hooks, signal }) {
|
|
|
565
3375
|
signal
|
|
566
3376
|
});
|
|
567
3377
|
if (result.success) await hooks.callHook("kubb:success", { message: `${(0, node_util.styleText)("dim", name ?? command)} successfully executed` });
|
|
568
|
-
if (!result.success) await report(
|
|
3378
|
+
if (!result.success) await report(Diagnostics.code.postGenerateFailed, "Post-generate command", result.error);
|
|
569
3379
|
signal?.throwIfAborted();
|
|
570
3380
|
}
|
|
571
3381
|
await hooks.callHook("kubb:hooks:end");
|
|
@@ -628,15 +3438,15 @@ var Kubb = class {
|
|
|
628
3438
|
#signal;
|
|
629
3439
|
constructor(userConfig, options = {}) {
|
|
630
3440
|
this.config = resolveConfig(userConfig);
|
|
631
|
-
this.hooks = options.hooks ?? new
|
|
3441
|
+
this.hooks = options.hooks ?? new Hookable();
|
|
632
3442
|
this.#signal = options.signal;
|
|
633
3443
|
}
|
|
634
3444
|
get storage() {
|
|
635
3445
|
return this.config.storage;
|
|
636
3446
|
}
|
|
637
3447
|
get driver() {
|
|
638
|
-
if (!this.#driver) throw new
|
|
639
|
-
code:
|
|
3448
|
+
if (!this.#driver) throw new Diagnostics.Error({
|
|
3449
|
+
code: Diagnostics.code.internalError,
|
|
640
3450
|
severity: "error",
|
|
641
3451
|
message: "[kubb] setup() must be called before accessing driver"
|
|
642
3452
|
});
|
|
@@ -654,14 +3464,14 @@ var Kubb = class {
|
|
|
654
3464
|
cache: cacheStorage({ root: config.root })
|
|
655
3465
|
}) : void 0;
|
|
656
3466
|
const cleanPath = config.output.clean ? (0, node_path.resolve)(config.root, config.output.path) : void 0;
|
|
657
|
-
if (cleanPath &&
|
|
658
|
-
code:
|
|
3467
|
+
if (cleanPath && isPathInside(config.root, cleanPath)) throw new Diagnostics.Error({
|
|
3468
|
+
code: Diagnostics.code.cleanRoot,
|
|
659
3469
|
severity: "error",
|
|
660
3470
|
message: `output.clean cannot delete "${cleanPath}" because it is the project root or a parent of it.`,
|
|
661
3471
|
help: "Point `output.path` at a subdirectory such as `./src/gen` so clean only removes generated code.",
|
|
662
3472
|
location: { kind: "config" }
|
|
663
3473
|
});
|
|
664
|
-
const driver = new
|
|
3474
|
+
const driver = new KubbDriver(config, {
|
|
665
3475
|
hooks: this.hooks,
|
|
666
3476
|
manifest,
|
|
667
3477
|
signal,
|
|
@@ -677,8 +3487,8 @@ var Kubb = class {
|
|
|
677
3487
|
*/
|
|
678
3488
|
async build() {
|
|
679
3489
|
const out = await this.safeBuild();
|
|
680
|
-
if (
|
|
681
|
-
const errors = out.diagnostics.filter(
|
|
3490
|
+
if (Diagnostics.hasError(out.diagnostics)) {
|
|
3491
|
+
const errors = out.diagnostics.filter(Diagnostics.isProblem).filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.cause ?? new Diagnostics.Error(diagnostic));
|
|
682
3492
|
throw new AggregateError(errors, `Build failed with ${errors.length} ${errors.length === 1 ? "error" : "errors"}`);
|
|
683
3493
|
}
|
|
684
3494
|
return out;
|
|
@@ -690,7 +3500,7 @@ var Kubb = class {
|
|
|
690
3500
|
*/
|
|
691
3501
|
async safeBuild() {
|
|
692
3502
|
try {
|
|
693
|
-
var _usingCtx$1 =
|
|
3503
|
+
var _usingCtx$1 = _usingCtx();
|
|
694
3504
|
this.#signal?.throwIfAborted();
|
|
695
3505
|
if (!this.#driver) await this.setup();
|
|
696
3506
|
const self = _usingCtx$1.u(this);
|
|
@@ -737,14 +3547,14 @@ var Kubb = class {
|
|
|
737
3547
|
if (signal?.aborted) throw caughtError;
|
|
738
3548
|
output = {
|
|
739
3549
|
files: [],
|
|
740
|
-
diagnostics: [
|
|
3550
|
+
diagnostics: [Diagnostics.from(caughtError)]
|
|
741
3551
|
};
|
|
742
3552
|
}
|
|
743
3553
|
const { files, diagnostics } = output;
|
|
744
3554
|
const storage = this.storage;
|
|
745
3555
|
signal?.throwIfAborted();
|
|
746
|
-
for (const diagnostic of diagnostics) if (
|
|
747
|
-
if (
|
|
3556
|
+
for (const diagnostic of diagnostics) if (Diagnostics.isProblem(diagnostic)) await Diagnostics.emit(hooks, diagnostic);
|
|
3557
|
+
if (Diagnostics.hasError(diagnostics)) {
|
|
748
3558
|
await hooks.callHook("kubb:generation:end", {
|
|
749
3559
|
config,
|
|
750
3560
|
storage,
|
|
@@ -767,7 +3577,7 @@ var Kubb = class {
|
|
|
767
3577
|
signal
|
|
768
3578
|
}) : [];
|
|
769
3579
|
const finalDiagnostics = [...diagnostics, ...outputDiagnostics];
|
|
770
|
-
const failed =
|
|
3580
|
+
const failed = Diagnostics.hasError(outputDiagnostics);
|
|
771
3581
|
if (!failed) await this.#manifest?.commit();
|
|
772
3582
|
await hooks.callHook("kubb:generation:end", {
|
|
773
3583
|
config,
|
|
@@ -872,11 +3682,11 @@ function createReporter(reporter) {
|
|
|
872
3682
|
*/
|
|
873
3683
|
function buildReport(result) {
|
|
874
3684
|
const { config, diagnostics, filesCreated, status, hrStart } = result;
|
|
875
|
-
const failed =
|
|
3685
|
+
const failed = Diagnostics.failedPlugins(diagnostics);
|
|
876
3686
|
const total = config.plugins?.length ?? 0;
|
|
877
|
-
const counts =
|
|
878
|
-
const problems = diagnostics.filter(
|
|
879
|
-
const timings = diagnostics.filter(
|
|
3687
|
+
const counts = Diagnostics.count(diagnostics);
|
|
3688
|
+
const problems = diagnostics.filter(Diagnostics.isProblem);
|
|
3689
|
+
const timings = diagnostics.filter(Diagnostics.isPerformance).sort((a, b) => b.duration - a.duration).map((diagnostic) => ({
|
|
880
3690
|
plugin: diagnostic.plugin,
|
|
881
3691
|
durationMs: diagnostic.duration
|
|
882
3692
|
}));
|
|
@@ -890,10 +3700,10 @@ function buildReport(result) {
|
|
|
890
3700
|
},
|
|
891
3701
|
counts,
|
|
892
3702
|
filesCreated,
|
|
893
|
-
durationMs:
|
|
3703
|
+
durationMs: getElapsedMs(hrStart),
|
|
894
3704
|
output: (0, node_path.resolve)(config.root, config.output.path),
|
|
895
3705
|
timings,
|
|
896
|
-
diagnostics: problems.map((diagnostic) =>
|
|
3706
|
+
diagnostics: problems.map((diagnostic) => Diagnostics.serialize(diagnostic))
|
|
897
3707
|
};
|
|
898
3708
|
}
|
|
899
3709
|
//#endregion
|
|
@@ -918,7 +3728,7 @@ function formatSummary(report, { showTimings }) {
|
|
|
918
3728
|
rows.push(["Issues", issues]);
|
|
919
3729
|
}
|
|
920
3730
|
rows.push(["Files", `${(0, node_util.styleText)("green", String(filesCreated))} generated`]);
|
|
921
|
-
rows.push(["Duration", (0, node_util.styleText)("green",
|
|
3731
|
+
rows.push(["Duration", (0, node_util.styleText)("green", formatMs(durationMs))]);
|
|
922
3732
|
rows.push(["Output", output]);
|
|
923
3733
|
const labelWidth = Math.max(...rows.map(([label]) => label.length), timings.length > 0 ? 7 : 0);
|
|
924
3734
|
const lines = rows.map(([label, value]) => `${(0, node_util.styleText)("dim", label.padStart(labelWidth))} ${value}`);
|
|
@@ -927,7 +3737,7 @@ function formatSummary(report, { showTimings }) {
|
|
|
927
3737
|
const indent = " ".repeat(labelWidth + 2);
|
|
928
3738
|
lines.push((0, node_util.styleText)("dim", "Timings".padStart(labelWidth)));
|
|
929
3739
|
for (const timing of timings) {
|
|
930
|
-
const timeStr =
|
|
3740
|
+
const timeStr = formatMs(timing.durationMs);
|
|
931
3741
|
const barLength = Math.min(Math.ceil(timing.durationMs / 100), 10);
|
|
932
3742
|
const bar = (0, node_util.styleText)("dim", "█".repeat(barLength));
|
|
933
3743
|
lines.push(`${indent}${(0, node_util.styleText)("dim", "•")} ${timing.plugin.padEnd(nameWidth)} ${bar} ${timeStr}`);
|
|
@@ -1022,12 +3832,12 @@ function kubbStorage({ base } = {}) {
|
|
|
1022
3832
|
* can drop the heading.
|
|
1023
3833
|
*/
|
|
1024
3834
|
function buildProblemSection(diagnostics) {
|
|
1025
|
-
const problems = diagnostics.filter(
|
|
3835
|
+
const problems = diagnostics.filter(Diagnostics.isProblem);
|
|
1026
3836
|
if (problems.length === 0) return [];
|
|
1027
3837
|
return [
|
|
1028
3838
|
"## Problems",
|
|
1029
3839
|
"",
|
|
1030
|
-
problems.map((diagnostic) =>
|
|
3840
|
+
problems.map((diagnostic) => Diagnostics.format(diagnostic, { verbose: true }).join("\n")).join("\n\n")
|
|
1031
3841
|
];
|
|
1032
3842
|
}
|
|
1033
3843
|
/**
|
|
@@ -1105,7 +3915,7 @@ const htmlReporter = createReporter({
|
|
|
1105
3915
|
const dir = resolveKubbDir((0, node_path.resolve)(process.cwd(), ".kubb"));
|
|
1106
3916
|
const storage = kubbStorage({ base: dir });
|
|
1107
3917
|
const assets = new URL("./html/", require("url").pathToFileURL(__filename).href);
|
|
1108
|
-
const [html, ui] = await Promise.all([
|
|
3918
|
+
const [html, ui] = await Promise.all([read((0, node_url.fileURLToPath)(new URL("index.html", assets))), read((0, node_url.fileURLToPath)(new URL("ui.iife.js", assets)))]);
|
|
1109
3919
|
await Promise.all([
|
|
1110
3920
|
storage.setItem(`${baseName}/index.html`, html),
|
|
1111
3921
|
storage.setItem(`${baseName}/ui.iife.js`, ui),
|
|
@@ -1192,7 +4002,7 @@ function defineGenerator(generator) {
|
|
|
1192
4002
|
* @deprecated Use `defineExtension` with `kind: 'parser'` instead.
|
|
1193
4003
|
*/
|
|
1194
4004
|
function defineParser(definition) {
|
|
1195
|
-
return
|
|
4005
|
+
return defineExtension({
|
|
1196
4006
|
kind: "parser",
|
|
1197
4007
|
...definition
|
|
1198
4008
|
});
|
|
@@ -1370,54 +4180,54 @@ async function readInstalledPackages({ root, packages = [], resolveFrom = [] })
|
|
|
1370
4180
|
}));
|
|
1371
4181
|
}
|
|
1372
4182
|
//#endregion
|
|
1373
|
-
exports.Diagnostics =
|
|
1374
|
-
exports.Hookable =
|
|
1375
|
-
exports.KubbDriver =
|
|
1376
|
-
exports.Resolver =
|
|
1377
|
-
exports.addGenerator =
|
|
1378
|
-
exports.addMacro =
|
|
1379
|
-
exports.assertPluginOption =
|
|
4183
|
+
exports.Diagnostics = Diagnostics;
|
|
4184
|
+
exports.Hookable = Hookable;
|
|
4185
|
+
exports.KubbDriver = KubbDriver;
|
|
4186
|
+
exports.Resolver = Resolver;
|
|
4187
|
+
exports.addGenerator = addGenerator;
|
|
4188
|
+
exports.addMacro = addMacro;
|
|
4189
|
+
exports.assertPluginOption = assertPluginOption;
|
|
1380
4190
|
exports.cacheStorage = cacheStorage;
|
|
1381
|
-
exports.checkCompatibility =
|
|
4191
|
+
exports.checkCompatibility = checkCompatibility;
|
|
1382
4192
|
exports.cliReporter = cliReporter;
|
|
1383
4193
|
exports.createAdapter = createAdapter;
|
|
1384
4194
|
exports.createCliReporter = createCliReporter;
|
|
1385
4195
|
exports.createKubb = createKubb;
|
|
1386
4196
|
exports.createRenderer = createRenderer;
|
|
1387
4197
|
exports.createReporter = createReporter;
|
|
1388
|
-
exports.createResolver =
|
|
4198
|
+
exports.createResolver = createResolver;
|
|
1389
4199
|
exports.createStorage = createStorage;
|
|
1390
|
-
exports.defineExtension =
|
|
4200
|
+
exports.defineExtension = defineExtension;
|
|
1391
4201
|
exports.defineGenerator = defineGenerator;
|
|
1392
4202
|
exports.defineParser = defineParser;
|
|
1393
|
-
exports.definePlugin =
|
|
4203
|
+
exports.definePlugin = definePlugin;
|
|
1394
4204
|
exports.fileReporter = fileReporter;
|
|
1395
4205
|
exports.fsStorage = fsStorage;
|
|
1396
|
-
exports.getInputKind =
|
|
4206
|
+
exports.getInputKind = getInputKind;
|
|
1397
4207
|
exports.htmlReporter = htmlReporter;
|
|
1398
|
-
exports.injectFile =
|
|
4208
|
+
exports.injectFile = injectFile;
|
|
1399
4209
|
exports.isExtensionManifest = isExtensionManifest;
|
|
1400
4210
|
exports.isPackageName = isPackageName;
|
|
1401
4211
|
exports.isRegistryEntry = isRegistryEntry;
|
|
1402
4212
|
exports.jsonReporter = jsonReporter;
|
|
1403
|
-
exports.kubbContext =
|
|
4213
|
+
exports.kubbContext = kubbContext;
|
|
1404
4214
|
exports.kubbStorage = kubbStorage;
|
|
1405
4215
|
exports.logLevel = logLevel;
|
|
1406
4216
|
exports.manifestProblems = manifestProblems;
|
|
1407
4217
|
exports.memoryStorage = memoryStorage;
|
|
1408
4218
|
exports.readInstalledPackages = readInstalledPackages;
|
|
1409
|
-
exports.requirePlugin =
|
|
4219
|
+
exports.requirePlugin = requirePlugin;
|
|
1410
4220
|
exports.resolveCacheDir = resolveCacheDir;
|
|
1411
4221
|
exports.resolveKubbDir = resolveKubbDir;
|
|
1412
|
-
exports.resolveMeta =
|
|
4222
|
+
exports.resolveMeta = resolveMeta;
|
|
1413
4223
|
exports.runHook = runHook;
|
|
1414
4224
|
exports.runOutputPasses = runOutputPasses;
|
|
1415
|
-
exports.setMacros =
|
|
1416
|
-
exports.setOptions =
|
|
1417
|
-
exports.setResolver =
|
|
1418
|
-
exports.tryUseKubb =
|
|
1419
|
-
exports.useConfig =
|
|
1420
|
-
exports.useKubb =
|
|
1421
|
-
exports.useOptions =
|
|
4225
|
+
exports.setMacros = setMacros;
|
|
4226
|
+
exports.setOptions = setOptions;
|
|
4227
|
+
exports.setResolver = setResolver;
|
|
4228
|
+
exports.tryUseKubb = tryUseKubb;
|
|
4229
|
+
exports.useConfig = useConfig;
|
|
4230
|
+
exports.useKubb = useKubb;
|
|
4231
|
+
exports.useOptions = useOptions;
|
|
1422
4232
|
|
|
1423
4233
|
//# sourceMappingURL=index.cjs.map
|