@kubb/core 5.0.0-beta.6 → 5.0.0-beta.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -10
- package/README.md +25 -158
- package/dist/diagnostics-DiaUv_iK.d.ts +2904 -0
- package/dist/index.cjs +2523 -1071
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +80 -273
- package/dist/index.js +2513 -1067
- package/dist/index.js.map +1 -1
- package/dist/memoryStorage-CUj1hrxa.cjs +823 -0
- package/dist/memoryStorage-CUj1hrxa.cjs.map +1 -0
- package/dist/memoryStorage-CWFzAz4o.js +714 -0
- package/dist/memoryStorage-CWFzAz4o.js.map +1 -0
- package/dist/mocks.cjs +83 -23
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.ts +36 -10
- package/dist/mocks.js +85 -27
- package/dist/mocks.js.map +1 -1
- package/package.json +8 -28
- package/src/FileManager.ts +86 -64
- package/src/FileProcessor.ts +170 -44
- package/src/KubbDriver.ts +909 -0
- package/src/Transform.ts +105 -0
- package/src/constants.ts +111 -20
- package/src/createAdapter.ts +112 -17
- package/src/createKubb.ts +140 -517
- package/src/createRenderer.ts +43 -28
- package/src/createReporter.ts +134 -0
- package/src/createStorage.ts +36 -23
- package/src/defineGenerator.ts +140 -17
- package/src/defineParser.ts +30 -12
- package/src/definePlugin.ts +375 -21
- package/src/defineResolver.ts +402 -212
- package/src/diagnostics.ts +662 -0
- package/src/index.ts +8 -8
- package/src/mocks.ts +97 -26
- package/src/reporters/cliReporter.ts +89 -0
- package/src/reporters/fileReporter.ts +103 -0
- package/src/reporters/jsonReporter.ts +20 -0
- package/src/reporters/report.ts +85 -0
- package/src/storages/fsStorage.ts +23 -55
- package/src/types.ts +411 -887
- package/dist/PluginDriver-BkTRD2H2.js +0 -946
- package/dist/PluginDriver-BkTRD2H2.js.map +0 -1
- package/dist/PluginDriver-Cadu4ORh.cjs +0 -1037
- package/dist/PluginDriver-Cadu4ORh.cjs.map +0 -1
- package/dist/types-DVPKmzw_.d.ts +0 -2159
- package/src/Kubb.ts +0 -300
- package/src/PluginDriver.ts +0 -426
- package/src/defineLogger.ts +0 -19
- package/src/defineMiddleware.ts +0 -62
- package/src/devtools.ts +0 -59
- package/src/renderNode.ts +0 -35
- package/src/utils/diagnostics.ts +0 -18
- package/src/utils/isInputPath.ts +0 -10
- package/src/utils/packageJSON.ts +0 -99
- /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -1,201 +1,176 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const
|
|
3
|
-
let
|
|
2
|
+
const require_memoryStorage = require("./memoryStorage-CUj1hrxa.cjs");
|
|
3
|
+
let node_crypto = require("node:crypto");
|
|
4
|
+
let node_util = require("node:util");
|
|
4
5
|
let node_fs_promises = require("node:fs/promises");
|
|
5
6
|
let node_path = require("node:path");
|
|
7
|
+
node_path = require_memoryStorage.__toESM(node_path, 1);
|
|
6
8
|
let _kubb_ast = require("@kubb/ast");
|
|
7
|
-
_kubb_ast =
|
|
9
|
+
_kubb_ast = require_memoryStorage.__toESM(_kubb_ast, 1);
|
|
10
|
+
let node_async_hooks = require("node:async_hooks");
|
|
11
|
+
let _kubb_ast_factory = require("@kubb/ast/factory");
|
|
12
|
+
_kubb_ast_factory = require_memoryStorage.__toESM(_kubb_ast_factory, 1);
|
|
13
|
+
let _kubb_ast_utils = require("@kubb/ast/utils");
|
|
8
14
|
let node_process = require("node:process");
|
|
9
|
-
|
|
15
|
+
node_process = require_memoryStorage.__toESM(node_process, 1);
|
|
16
|
+
//#region ../../internals/utils/src/time.ts
|
|
10
17
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
18
|
+
* Calculates elapsed time in milliseconds from a high-resolution `process.hrtime` start time.
|
|
19
|
+
* Rounds to 2 decimal places for sub-millisecond precision without noise.
|
|
13
20
|
*
|
|
14
21
|
* @example
|
|
15
22
|
* ```ts
|
|
16
|
-
*
|
|
23
|
+
* const start = process.hrtime()
|
|
24
|
+
* doWork()
|
|
25
|
+
* getElapsedMs(start) // 42.35
|
|
17
26
|
* ```
|
|
18
27
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
this.errors = options.errors;
|
|
25
|
-
}
|
|
26
|
-
};
|
|
28
|
+
function getElapsedMs(hrStart) {
|
|
29
|
+
const [seconds, nanoseconds] = process.hrtime(hrStart);
|
|
30
|
+
const ms = seconds * 1e3 + nanoseconds / 1e6;
|
|
31
|
+
return Math.round(ms * 100) / 100;
|
|
32
|
+
}
|
|
27
33
|
/**
|
|
28
|
-
*
|
|
29
|
-
* Returns the value as-is when it is already an `Error`; otherwise wraps it with `String(value)`.
|
|
34
|
+
* Converts a millisecond duration into a human-readable string (`ms`, `s`, or `m s`).
|
|
30
35
|
*
|
|
31
36
|
* @example
|
|
32
37
|
* ```ts
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
38
|
+
* formatMs(250) // '250ms'
|
|
39
|
+
* formatMs(1500) // '1.50s'
|
|
40
|
+
* formatMs(90000) // '1m 30.0s'
|
|
36
41
|
* ```
|
|
37
42
|
*/
|
|
38
|
-
function
|
|
39
|
-
return
|
|
43
|
+
function formatMs(ms) {
|
|
44
|
+
if (ms >= 6e4) return `${Math.floor(ms / 6e4)}m ${(ms % 6e4 / 1e3).toFixed(1)}s`;
|
|
45
|
+
if (ms >= 1e3) return `${(ms / 1e3).toFixed(2)}s`;
|
|
46
|
+
return `${Math.round(ms)}ms`;
|
|
40
47
|
}
|
|
41
48
|
//#endregion
|
|
42
|
-
//#region ../../internals/utils/src/
|
|
49
|
+
//#region ../../internals/utils/src/colors.ts
|
|
50
|
+
/**
|
|
51
|
+
* Parses a CSS hex color string (`#RGB`) into its RGB channels.
|
|
52
|
+
* Falls back to `255` for any channel that cannot be parsed.
|
|
53
|
+
*/
|
|
54
|
+
function parseHex(color) {
|
|
55
|
+
const int = Number.parseInt(color.replace("#", ""), 16);
|
|
56
|
+
return Number.isNaN(int) ? {
|
|
57
|
+
r: 255,
|
|
58
|
+
g: 255,
|
|
59
|
+
b: 255
|
|
60
|
+
} : {
|
|
61
|
+
r: int >> 16 & 255,
|
|
62
|
+
g: int >> 8 & 255,
|
|
63
|
+
b: int & 255
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Returns a function that wraps a string in a 24-bit ANSI true-color escape sequence
|
|
68
|
+
* for the given hex color.
|
|
69
|
+
*/
|
|
70
|
+
function hex(color) {
|
|
71
|
+
const { r, g, b } = parseHex(color);
|
|
72
|
+
return (text) => `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
|
|
73
|
+
}
|
|
74
|
+
hex("#F55A17"), hex("#F5A217"), hex("#F58517"), hex("#B45309"), hex("#FFFFFF"), hex("#adadc6"), hex("#FDA4AF");
|
|
75
|
+
/**
|
|
76
|
+
* ANSI color names used by {@link randomCliColor} for deterministic terminal coloring.
|
|
77
|
+
*/
|
|
78
|
+
const randomColors = [
|
|
79
|
+
"black",
|
|
80
|
+
"red",
|
|
81
|
+
"green",
|
|
82
|
+
"yellow",
|
|
83
|
+
"blue",
|
|
84
|
+
"white",
|
|
85
|
+
"magenta",
|
|
86
|
+
"cyan",
|
|
87
|
+
"gray"
|
|
88
|
+
];
|
|
43
89
|
/**
|
|
44
|
-
*
|
|
45
|
-
* Wraps Node's `EventEmitter` with full TypeScript event-map inference.
|
|
90
|
+
* Wraps `text` in a deterministic ANSI color derived from the text's SHA-256 hash.
|
|
46
91
|
*
|
|
47
92
|
* @example
|
|
48
93
|
* ```ts
|
|
49
|
-
*
|
|
50
|
-
* emitter.on('build', async (name) => { console.log(name) })
|
|
51
|
-
* await emitter.emit('build', 'petstore') // all listeners awaited
|
|
94
|
+
* randomCliColor('petstore') // '\x1b[33m' + 'petstore' + '\x1b[39m' (always the same color for 'petstore')
|
|
52
95
|
* ```
|
|
53
96
|
*/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
97
|
+
function randomCliColor(text) {
|
|
98
|
+
if (!text) return "";
|
|
99
|
+
return (0, node_util.styleText)(randomColors[(0, node_crypto.hash)("sha256", text, "buffer").readUInt32BE(0) % randomColors.length] ?? "white", text);
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region ../../internals/utils/src/runtime.ts
|
|
103
|
+
/**
|
|
104
|
+
* Detects the JavaScript runtime executing the current process and exposes its name and version.
|
|
105
|
+
*
|
|
106
|
+
* Prefer the shared {@link runtime} instance over constructing your own.
|
|
107
|
+
*/
|
|
108
|
+
var Runtime = class {
|
|
63
109
|
/**
|
|
64
|
-
*
|
|
65
|
-
* Throws if any listener rejects, wrapping the cause with the event name and serialized arguments.
|
|
110
|
+
* `true` when the current process is running under Bun.
|
|
66
111
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* ```
|
|
71
|
-
*/
|
|
72
|
-
async emit(eventName, ...eventArgs) {
|
|
73
|
-
const listeners = this.#emitter.listeners(eventName);
|
|
74
|
-
if (listeners.length === 0) return;
|
|
75
|
-
for (const listener of listeners) try {
|
|
76
|
-
await listener(...eventArgs);
|
|
77
|
-
} catch (err) {
|
|
78
|
-
let serializedArgs;
|
|
79
|
-
try {
|
|
80
|
-
serializedArgs = JSON.stringify(eventArgs);
|
|
81
|
-
} catch {
|
|
82
|
-
serializedArgs = String(eventArgs);
|
|
83
|
-
}
|
|
84
|
-
throw new Error(`Error in async listener for "${eventName}" with eventArgs ${serializedArgs}`, { cause: toError(err) });
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Registers a persistent listener for `eventName`.
|
|
112
|
+
* Detection keys off the global `Bun` object rather than `process.versions`,
|
|
113
|
+
* because Bun polyfills `process.versions.node` for Node compatibility and would
|
|
114
|
+
* otherwise look like Node.
|
|
89
115
|
*
|
|
90
116
|
* @example
|
|
91
117
|
* ```ts
|
|
92
|
-
*
|
|
118
|
+
* if (runtime.isBun) {
|
|
119
|
+
* await Bun.write(path, data)
|
|
120
|
+
* }
|
|
93
121
|
* ```
|
|
94
122
|
*/
|
|
95
|
-
|
|
96
|
-
|
|
123
|
+
get isBun() {
|
|
124
|
+
return typeof Bun !== "undefined";
|
|
97
125
|
}
|
|
98
126
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* @example
|
|
102
|
-
* ```ts
|
|
103
|
-
* emitter.onOnce('build', async (name) => { console.log(name) })
|
|
104
|
-
* ```
|
|
127
|
+
* `true` when the current process is running under Deno.
|
|
105
128
|
*/
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
this.off(eventName, wrapper);
|
|
109
|
-
return handler(...args);
|
|
110
|
-
};
|
|
111
|
-
this.on(eventName, wrapper);
|
|
129
|
+
get isDeno() {
|
|
130
|
+
return typeof globalThis.Deno !== "undefined";
|
|
112
131
|
}
|
|
113
132
|
/**
|
|
114
|
-
*
|
|
133
|
+
* `true` when the current process is running under Node.
|
|
115
134
|
*
|
|
116
|
-
*
|
|
117
|
-
* ```ts
|
|
118
|
-
* emitter.off('build', handler)
|
|
119
|
-
* ```
|
|
135
|
+
* Bun and Deno are excluded first so a polyfilled `process` does not register as Node.
|
|
120
136
|
*/
|
|
121
|
-
|
|
122
|
-
this
|
|
137
|
+
get isNode() {
|
|
138
|
+
return !this.isBun && !this.isDeno && typeof process !== "undefined" && process.versions?.node != null;
|
|
123
139
|
}
|
|
124
140
|
/**
|
|
125
|
-
*
|
|
141
|
+
* Name of the runtime executing the current process.
|
|
126
142
|
*
|
|
127
143
|
* @example
|
|
128
144
|
* ```ts
|
|
129
|
-
*
|
|
130
|
-
* emitter.listenerCount('build') // 1
|
|
145
|
+
* runtime.name // 'bun' when run with `bun kubb`, 'node' otherwise
|
|
131
146
|
* ```
|
|
132
147
|
*/
|
|
133
|
-
|
|
134
|
-
|
|
148
|
+
get name() {
|
|
149
|
+
if (this.isBun) return "bun";
|
|
150
|
+
if (this.isDeno) return "deno";
|
|
151
|
+
return "node";
|
|
135
152
|
}
|
|
136
153
|
/**
|
|
137
|
-
*
|
|
154
|
+
* Version of the active runtime, or an empty string when it cannot be read.
|
|
138
155
|
*
|
|
139
156
|
* @example
|
|
140
157
|
* ```ts
|
|
141
|
-
*
|
|
158
|
+
* runtime.version // '1.3.11' under Bun, '22.22.2' under Node
|
|
142
159
|
* ```
|
|
143
160
|
*/
|
|
144
|
-
|
|
145
|
-
this
|
|
161
|
+
get version() {
|
|
162
|
+
if (this.isBun) return process.versions.bun ?? "";
|
|
163
|
+
if (this.isDeno) return globalThis.Deno?.version?.deno ?? "";
|
|
164
|
+
return process.versions?.node ?? "";
|
|
146
165
|
}
|
|
147
166
|
};
|
|
148
|
-
//#endregion
|
|
149
|
-
//#region ../../internals/utils/src/time.ts
|
|
150
|
-
/**
|
|
151
|
-
* Calculates elapsed time in milliseconds from a high-resolution `process.hrtime` start time.
|
|
152
|
-
* Rounds to 2 decimal places for sub-millisecond precision without noise.
|
|
153
|
-
*
|
|
154
|
-
* @example
|
|
155
|
-
* ```ts
|
|
156
|
-
* const start = process.hrtime()
|
|
157
|
-
* doWork()
|
|
158
|
-
* getElapsedMs(start) // 42.35
|
|
159
|
-
* ```
|
|
160
|
-
*/
|
|
161
|
-
function getElapsedMs(hrStart) {
|
|
162
|
-
const [seconds, nanoseconds] = process.hrtime(hrStart);
|
|
163
|
-
const ms = seconds * 1e3 + nanoseconds / 1e6;
|
|
164
|
-
return Math.round(ms * 100) / 100;
|
|
165
|
-
}
|
|
166
167
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* @example
|
|
170
|
-
* ```ts
|
|
171
|
-
* formatMs(250) // '250ms'
|
|
172
|
-
* formatMs(1500) // '1.50s'
|
|
173
|
-
* formatMs(90000) // '1m 30.0s'
|
|
174
|
-
* ```
|
|
168
|
+
* Shared {@link Runtime} instance describing the JavaScript runtime executing the current process.
|
|
175
169
|
*/
|
|
176
|
-
|
|
177
|
-
if (ms >= 6e4) return `${Math.floor(ms / 6e4)}m ${(ms % 6e4 / 1e3).toFixed(1)}s`;
|
|
178
|
-
if (ms >= 1e3) return `${(ms / 1e3).toFixed(2)}s`;
|
|
179
|
-
return `${Math.round(ms)}ms`;
|
|
180
|
-
}
|
|
170
|
+
const runtime = new Runtime();
|
|
181
171
|
//#endregion
|
|
182
172
|
//#region ../../internals/utils/src/fs.ts
|
|
183
173
|
/**
|
|
184
|
-
* Resolves to `true` when the file or directory at `path` exists.
|
|
185
|
-
* Uses `Bun.file().exists()` when running under Bun, `fs.access` otherwise.
|
|
186
|
-
*
|
|
187
|
-
* @example
|
|
188
|
-
* ```ts
|
|
189
|
-
* if (await exists('./kubb.config.ts')) {
|
|
190
|
-
* const content = await read('./kubb.config.ts')
|
|
191
|
-
* }
|
|
192
|
-
* ```
|
|
193
|
-
*/
|
|
194
|
-
async function exists(path) {
|
|
195
|
-
if (typeof Bun !== "undefined") return Bun.file(path).exists();
|
|
196
|
-
return (0, node_fs_promises.access)(path).then(() => true, () => false);
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
174
|
* Writes `data` to `path`, trimming leading/trailing whitespace before saving.
|
|
200
175
|
* Skips the write when the trimmed content is empty or identical to what is already on disk.
|
|
201
176
|
* Creates any missing parent directories automatically.
|
|
@@ -212,7 +187,7 @@ async function write(path, data, options = {}) {
|
|
|
212
187
|
const trimmed = data.trim();
|
|
213
188
|
if (trimmed === "") return null;
|
|
214
189
|
const resolved = (0, node_path.resolve)(path);
|
|
215
|
-
if (
|
|
190
|
+
if (runtime.isBun) {
|
|
216
191
|
const file = Bun.file(resolved);
|
|
217
192
|
if ((await file.exists() ? await file.text() : null) === trimmed) return null;
|
|
218
193
|
await Bun.write(resolved, trimmed);
|
|
@@ -244,6 +219,156 @@ async function clean(path) {
|
|
|
244
219
|
force: true
|
|
245
220
|
});
|
|
246
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Converts a filesystem path to use POSIX (`/`) separators.
|
|
224
|
+
*
|
|
225
|
+
* Most of the codebase compares and composes paths as strings (prefix matching, joining for
|
|
226
|
+
* import specifiers, splitting on `/`). On POSIX `path.resolve` already returns `/`-separated
|
|
227
|
+
* paths, but on Windows it returns `\`-separated paths, which breaks every such comparison.
|
|
228
|
+
*
|
|
229
|
+
* Routing every path that crosses a module boundary through `toPosixPath` keeps the rest of the
|
|
230
|
+
* code platform-agnostic. The conversion runs unconditionally so Windows-specific behavior is
|
|
231
|
+
* exercisable from POSIX CI.
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* toPosixPath('C:\\repo\\src\\pet.ts') // 'C:/repo/src/pet.ts'
|
|
235
|
+
*/
|
|
236
|
+
function toPosixPath(filePath) {
|
|
237
|
+
return filePath.replaceAll("\\", "/");
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Builds a nested file path from a dotted name. Splits on dots that precede a letter
|
|
241
|
+
* (so version numbers embedded in operationIds like `v2025.0` stay intact), camelCases
|
|
242
|
+
* every earlier segment, applies `caseLast` to the final segment, and joins with `/`.
|
|
243
|
+
*
|
|
244
|
+
* Empty segments are dropped before joining. They arise when the name starts with a dot
|
|
245
|
+
* followed by a letter (e.g. `..Schema` splits into `['..', 'Schema']` and `'..'` cases to
|
|
246
|
+
* an empty string). Without this a leading `/` would form, which `path.resolve` reads as an
|
|
247
|
+
* absolute path, letting generated files escape the configured output directory.
|
|
248
|
+
*
|
|
249
|
+
* @example Nested path from a dotted name
|
|
250
|
+
* `toFilePath('pet.petId') // 'pet/petId'`
|
|
251
|
+
*
|
|
252
|
+
* @example PascalCase the final segment
|
|
253
|
+
* `toFilePath('pet.Pet', pascalCase) // 'pet/Pet'`
|
|
254
|
+
*
|
|
255
|
+
* @example Suffix applied to the final segment only
|
|
256
|
+
* `toFilePath('tag.tag', (part) => camelCase(part, { suffix: 'schema' })) // 'tag/tagSchema'`
|
|
257
|
+
*/
|
|
258
|
+
function toFilePath(name, caseLast = require_memoryStorage.camelCase) {
|
|
259
|
+
const parts = name.split(/\.(?=[a-zA-Z])/);
|
|
260
|
+
return parts.map((part, i) => i === parts.length - 1 ? caseLast(part) : require_memoryStorage.camelCase(part)).filter(Boolean).join("/");
|
|
261
|
+
}
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region ../../internals/utils/src/promise.ts
|
|
264
|
+
function* chunks(arr, size) {
|
|
265
|
+
for (let i = 0; i < arr.length; i += size) yield arr.slice(i, i + size);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Slices `source` into batches of `concurrency` items and awaits `process` for each batch.
|
|
269
|
+
* Accepts both plain arrays (sync) and `AsyncIterable` (streaming).
|
|
270
|
+
*
|
|
271
|
+
* `process` controls whether items inside a batch run in parallel; this helper only
|
|
272
|
+
* controls batch size and per-batch flushing.
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* ```ts
|
|
276
|
+
* // parallel dispatch inside each batch
|
|
277
|
+
* await forBatches(schemas, (batch) => Promise.all(batch.map(process)), { concurrency: 8 })
|
|
278
|
+
*
|
|
279
|
+
* // async iterable with a flush after every batch
|
|
280
|
+
* await forBatches(stream.schemas, (batch) => dispatch(batch), { concurrency: 8, flush })
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
283
|
+
async function forBatches(source, process, options) {
|
|
284
|
+
const { concurrency, flush } = options;
|
|
285
|
+
if (Array.isArray(source)) {
|
|
286
|
+
for (const batch of chunks(source, concurrency)) {
|
|
287
|
+
await process(batch);
|
|
288
|
+
if (flush) await flush();
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const batch = [];
|
|
293
|
+
for await (const item of source) {
|
|
294
|
+
batch.push(item);
|
|
295
|
+
if (batch.length >= concurrency) {
|
|
296
|
+
await process(batch.splice(0));
|
|
297
|
+
if (flush) await flush();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (batch.length > 0) {
|
|
301
|
+
await process(batch.splice(0));
|
|
302
|
+
if (flush) await flush();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/** Returns `true` when `result` is a thenable `Promise`.
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* ```ts
|
|
309
|
+
* isPromise(Promise.resolve(1)) // true
|
|
310
|
+
* isPromise(42) // false
|
|
311
|
+
* ```
|
|
312
|
+
*/
|
|
313
|
+
function isPromise(result) {
|
|
314
|
+
return result !== null && result !== void 0 && typeof result["then"] === "function";
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Wraps `factory` with a keyed cache backed by the provided store.
|
|
318
|
+
*
|
|
319
|
+
* Pass a `WeakMap` for object keys (results are GC-eligible when the key is
|
|
320
|
+
* collected) or a `Map` for primitive keys. For multi-argument functions,
|
|
321
|
+
* nest two `memoize` calls — the outer keyed by the first argument, the
|
|
322
|
+
* inner (created once per outer miss) keyed by the second.
|
|
323
|
+
*
|
|
324
|
+
* Because the cache is owned by the caller, it can be shared, inspected, or
|
|
325
|
+
* cleared independently of the memoized function.
|
|
326
|
+
*
|
|
327
|
+
* @example Single WeakMap key
|
|
328
|
+
* ```ts
|
|
329
|
+
* const cache = new WeakMap<SchemaNode, Set<string>>()
|
|
330
|
+
* const getRefs = memoize(cache, (node) => collectRefs(node))
|
|
331
|
+
* ```
|
|
332
|
+
*
|
|
333
|
+
* @example Single Map key (primitive)
|
|
334
|
+
* ```ts
|
|
335
|
+
* const cache = new Map<string, Resolver>()
|
|
336
|
+
* const getResolver = memoize(cache, (name) => buildResolver(name))
|
|
337
|
+
* ```
|
|
338
|
+
*
|
|
339
|
+
* @example Two-level (object + primitive)
|
|
340
|
+
* ```ts
|
|
341
|
+
* const outer = new WeakMap<Params[], Map<string, Params[]>>()
|
|
342
|
+
* const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
|
|
343
|
+
* fn(params)('camelcase')
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
346
|
+
function memoize(store, factory) {
|
|
347
|
+
return (key) => {
|
|
348
|
+
if (store.has(key)) return store.get(key);
|
|
349
|
+
const value = factory(key);
|
|
350
|
+
store.set(key, value);
|
|
351
|
+
return value;
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Wraps a plain array in a reusable `AsyncIterable`.
|
|
356
|
+
* Each `[Symbol.asyncIterator]()` call returns a fresh generator so the
|
|
357
|
+
* iterable can be consumed multiple times (e.g. once per plugin pre-scan).
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* ```ts
|
|
361
|
+
* const stream = arrayToAsyncIterable([1, 2, 3])
|
|
362
|
+
* for await (const n of stream) console.log(n) // 1, 2, 3
|
|
363
|
+
* ```
|
|
364
|
+
*/
|
|
365
|
+
function arrayToAsyncIterable(arr) {
|
|
366
|
+
return { [Symbol.asyncIterator]() {
|
|
367
|
+
return (async function* () {
|
|
368
|
+
yield* arr;
|
|
369
|
+
})();
|
|
370
|
+
} };
|
|
371
|
+
}
|
|
247
372
|
//#endregion
|
|
248
373
|
//#region ../../internals/utils/src/reserved.ts
|
|
249
374
|
/**
|
|
@@ -345,102 +470,99 @@ const reservedWords = new Set([
|
|
|
345
470
|
*/
|
|
346
471
|
function isValidVarName(name) {
|
|
347
472
|
if (!name || reservedWords.has(name)) return false;
|
|
348
|
-
return
|
|
473
|
+
return isIdentifier(name);
|
|
349
474
|
}
|
|
350
|
-
//#endregion
|
|
351
|
-
//#region ../../internals/utils/src/urlPath.ts
|
|
352
475
|
/**
|
|
353
|
-
*
|
|
476
|
+
* Returns `true` when `name` is syntactically a valid identifier, ignoring reserved words.
|
|
477
|
+
*
|
|
478
|
+
* Reserved words and globals (`class`, `name`, `Date`, …) are valid as bare object-literal keys
|
|
479
|
+
* even though they are not valid variable names, so use this (not {@link isValidVarName}) when
|
|
480
|
+
* deciding whether an object key needs quoting.
|
|
354
481
|
*
|
|
355
482
|
* @example
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
483
|
+
* ```ts
|
|
484
|
+
* isIdentifier('name') // true
|
|
485
|
+
* isIdentifier('x-total')// false
|
|
486
|
+
* ```
|
|
359
487
|
*/
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
488
|
+
function isIdentifier(name) {
|
|
489
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
|
|
490
|
+
}
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region ../../internals/utils/src/url.ts
|
|
493
|
+
function transformParam(raw, casing) {
|
|
494
|
+
const param = isValidVarName(raw) ? raw : require_memoryStorage.camelCase(raw);
|
|
495
|
+
return casing === "camelcase" ? require_memoryStorage.camelCase(param) : param;
|
|
496
|
+
}
|
|
497
|
+
function toParamsObject(path, { replacer, casing } = {}) {
|
|
498
|
+
const params = {};
|
|
499
|
+
for (const match of path.matchAll(/\{([^}]+)\}/g)) {
|
|
500
|
+
const param = transformParam(match[1], casing);
|
|
501
|
+
const key = replacer ? replacer(param) : param;
|
|
502
|
+
params[key] = key;
|
|
369
503
|
}
|
|
370
|
-
|
|
504
|
+
return Object.keys(params).length > 0 ? params : null;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Helpers for OpenAPI/Swagger paths, plus a thin wrapper over the native `URL`.
|
|
508
|
+
*/
|
|
509
|
+
var Url = class Url {
|
|
510
|
+
/**
|
|
511
|
+
* Reports whether `url` is a parseable absolute URL. Delegates to the native `URL.canParse`.
|
|
371
512
|
*
|
|
372
513
|
* @example
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
* ```
|
|
514
|
+
* Url.canParse('https://petstore.swagger.io/v2') // true
|
|
515
|
+
* Url.canParse('/pet/{petId}') // false
|
|
376
516
|
*/
|
|
377
|
-
|
|
378
|
-
return
|
|
517
|
+
static canParse(url, base) {
|
|
518
|
+
return URL.canParse(url, base);
|
|
379
519
|
}
|
|
380
|
-
/**
|
|
520
|
+
/**
|
|
521
|
+
* Converts an OpenAPI/Swagger path to Express-style colon syntax.
|
|
381
522
|
*
|
|
382
523
|
* @example
|
|
383
|
-
*
|
|
384
|
-
* new URLPath('https://petstore.swagger.io/v2/pet').isURL // true
|
|
385
|
-
* new URLPath('/pet/{petId}').isURL // false
|
|
386
|
-
* ```
|
|
524
|
+
* Url.toPath('/pet/{petId}') // '/pet/:petId'
|
|
387
525
|
*/
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
return !!new URL(this.path).href;
|
|
391
|
-
} catch {
|
|
392
|
-
return false;
|
|
393
|
-
}
|
|
526
|
+
static toPath(path) {
|
|
527
|
+
return path.replace(/\{([^}]+)\}/g, ":$1");
|
|
394
528
|
}
|
|
395
529
|
/**
|
|
396
|
-
* Converts
|
|
530
|
+
* Converts an OpenAPI/Swagger path to a TypeScript template literal string.
|
|
531
|
+
* `prefix` is prepended inside the literal, `replacer` transforms each parameter name,
|
|
532
|
+
* and `casing` controls parameter identifier casing.
|
|
397
533
|
*
|
|
398
534
|
* @example
|
|
399
|
-
*
|
|
400
|
-
* new URLPath('/account/monetary-accountID').template // '`/account/${monetaryAccountId}`'
|
|
401
|
-
*/
|
|
402
|
-
get template() {
|
|
403
|
-
return this.toTemplateString();
|
|
404
|
-
}
|
|
405
|
-
/** Returns the path and its extracted params as a structured `URLObject`, or as a stringified expression when `stringify` is set.
|
|
535
|
+
* Url.toTemplateString('/pet/{petId}') // '`/pet/${petId}`'
|
|
406
536
|
*
|
|
407
537
|
* @example
|
|
408
|
-
*
|
|
409
|
-
* new URLPath('/pet/{petId}').object
|
|
410
|
-
* // { url: '/pet/:petId', params: { petId: 'petId' } }
|
|
411
|
-
* ```
|
|
538
|
+
* Url.toTemplateString('/pet/{petId}', { prefix: 'https://api' }) // '`https://api/pet/${petId}`'
|
|
412
539
|
*/
|
|
413
|
-
|
|
414
|
-
|
|
540
|
+
static toTemplateString(path, { prefix, replacer, casing } = {}) {
|
|
541
|
+
const result = path.split(/\{([^}]+)\}/).map((part, i) => {
|
|
542
|
+
if (i % 2 === 0) return part;
|
|
543
|
+
const param = transformParam(part, casing);
|
|
544
|
+
return `\${${replacer ? replacer(param) : param}}`;
|
|
545
|
+
}).join("");
|
|
546
|
+
return `\`${prefix ?? ""}${result}\``;
|
|
415
547
|
}
|
|
416
|
-
/**
|
|
548
|
+
/**
|
|
549
|
+
* Returns the path and its extracted params as a structured `URLObject`, or as a stringified
|
|
550
|
+
* expression when `stringify` is set.
|
|
417
551
|
*
|
|
418
552
|
* @example
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
* new URLPath('/pet').params // undefined
|
|
422
|
-
* ```
|
|
423
|
-
*/
|
|
424
|
-
get params() {
|
|
425
|
-
return this.getParams();
|
|
426
|
-
}
|
|
427
|
-
#transformParam(raw) {
|
|
428
|
-
const param = isValidVarName(raw) ? raw : require_PluginDriver.camelCase(raw);
|
|
429
|
-
return this.#options.casing === "camelcase" ? require_PluginDriver.camelCase(param) : param;
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* Iterates over every `{param}` token in `path`, calling `fn` with the raw token and transformed name.
|
|
553
|
+
* Url.toObject('/pet/{petId}')
|
|
554
|
+
* // { url: '/pet/:petId', params: { petId: 'petId' } }
|
|
433
555
|
*/
|
|
434
|
-
|
|
435
|
-
for (const match of this.path.matchAll(/\{([^}]+)\}/g)) {
|
|
436
|
-
const raw = match[1];
|
|
437
|
-
fn(raw, this.#transformParam(raw));
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
toObject({ type = "path", replacer, stringify } = {}) {
|
|
556
|
+
static toObject(path, { type = "path", replacer, stringify, casing } = {}) {
|
|
441
557
|
const object = {
|
|
442
|
-
url: type === "path" ?
|
|
443
|
-
|
|
558
|
+
url: type === "path" ? Url.toPath(path) : Url.toTemplateString(path, {
|
|
559
|
+
replacer,
|
|
560
|
+
casing
|
|
561
|
+
}),
|
|
562
|
+
params: toParamsObject(path, {
|
|
563
|
+
replacer,
|
|
564
|
+
casing
|
|
565
|
+
})
|
|
444
566
|
};
|
|
445
567
|
if (stringify) {
|
|
446
568
|
if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
|
|
@@ -449,758 +571,1756 @@ var URLPath = class {
|
|
|
449
571
|
}
|
|
450
572
|
return object;
|
|
451
573
|
}
|
|
452
|
-
/**
|
|
453
|
-
* Converts the OpenAPI path to a TypeScript template literal string.
|
|
454
|
-
* An optional `replacer` can transform each extracted parameter name before interpolation.
|
|
455
|
-
*
|
|
456
|
-
* @example
|
|
457
|
-
* new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
|
|
458
|
-
*/
|
|
459
|
-
toTemplateString({ prefix = "", replacer } = {}) {
|
|
460
|
-
return `\`${prefix}${this.path.split(/\{([^}]+)\}/).map((part, i) => {
|
|
461
|
-
if (i % 2 === 0) return part;
|
|
462
|
-
const param = this.#transformParam(part);
|
|
463
|
-
return `\${${replacer ? replacer(param) : param}}`;
|
|
464
|
-
}).join("")}\``;
|
|
465
|
-
}
|
|
466
|
-
/**
|
|
467
|
-
* Extracts all `{param}` segments from the path and returns them as a key-value map.
|
|
468
|
-
* An optional `replacer` transforms each parameter name in both key and value positions.
|
|
469
|
-
* Returns `undefined` when no path parameters are found.
|
|
470
|
-
*
|
|
471
|
-
* @example
|
|
472
|
-
* ```ts
|
|
473
|
-
* new URLPath('/pet/{petId}/tag/{tagId}').getParams()
|
|
474
|
-
* // { petId: 'petId', tagId: 'tagId' }
|
|
475
|
-
* ```
|
|
476
|
-
*/
|
|
477
|
-
getParams(replacer) {
|
|
478
|
-
const params = {};
|
|
479
|
-
this.#eachParam((_raw, param) => {
|
|
480
|
-
const key = replacer ? replacer(param) : param;
|
|
481
|
-
params[key] = key;
|
|
482
|
-
});
|
|
483
|
-
return Object.keys(params).length > 0 ? params : void 0;
|
|
484
|
-
}
|
|
485
|
-
/** Converts the OpenAPI path to Express-style colon syntax.
|
|
486
|
-
*
|
|
487
|
-
* @example
|
|
488
|
-
* ```ts
|
|
489
|
-
* new URLPath('/pet/{petId}').toURLPath() // '/pet/:petId'
|
|
490
|
-
* ```
|
|
491
|
-
*/
|
|
492
|
-
toURLPath() {
|
|
493
|
-
return this.path.replace(/\{([^}]+)\}/g, ":$1");
|
|
494
|
-
}
|
|
495
574
|
};
|
|
496
575
|
//#endregion
|
|
497
576
|
//#region src/createAdapter.ts
|
|
498
577
|
/**
|
|
499
|
-
*
|
|
578
|
+
* Defines a custom adapter that translates a spec format into Kubb's universal
|
|
579
|
+
* AST, for example GraphQL, gRPC, or AsyncAPI. The built-in `@kubb/adapter-oas`
|
|
580
|
+
* handles OpenAPI/Swagger documents.
|
|
500
581
|
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
* @note Adapters must parse their input format to Kubb's `InputNode` structure.
|
|
582
|
+
* Adapters must return an `InputNode` from `parse`. That node is what every
|
|
583
|
+
* plugin in the build consumes.
|
|
505
584
|
*
|
|
506
585
|
* @example
|
|
507
586
|
* ```ts
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
* options,
|
|
512
|
-
* async parse(source) {
|
|
513
|
-
* // Transform source format to InputNode
|
|
514
|
-
* return { ... }
|
|
515
|
-
* },
|
|
516
|
-
* }
|
|
517
|
-
* })
|
|
587
|
+
* import { createAdapter, ast, type AdapterFactoryOptions } from '@kubb/core'
|
|
588
|
+
*
|
|
589
|
+
* type MyAdapter = AdapterFactoryOptions<'my-adapter', { validate?: boolean }>
|
|
518
590
|
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
591
|
+
* export const myAdapter = createAdapter<MyAdapter>((options) => ({
|
|
592
|
+
* name: 'my-adapter',
|
|
593
|
+
* options,
|
|
594
|
+
* document: null,
|
|
595
|
+
* async parse(_source) {
|
|
596
|
+
* // Convert `source` (path or inline data) into an InputNode.
|
|
597
|
+
* return ast.factory.createInput()
|
|
598
|
+
* },
|
|
599
|
+
* getImports: () => [],
|
|
600
|
+
* async validate() {
|
|
601
|
+
* // Throw or call ctx.error here when the spec is invalid.
|
|
602
|
+
* },
|
|
603
|
+
* }))
|
|
521
604
|
* ```
|
|
522
605
|
*/
|
|
523
606
|
function createAdapter(build) {
|
|
524
607
|
return (options) => build(options ?? {});
|
|
525
608
|
}
|
|
526
609
|
//#endregion
|
|
527
|
-
//#region
|
|
528
|
-
var Node = class {
|
|
529
|
-
value;
|
|
530
|
-
next;
|
|
531
|
-
constructor(value) {
|
|
532
|
-
this.value = value;
|
|
533
|
-
}
|
|
534
|
-
};
|
|
535
|
-
var Queue = class {
|
|
536
|
-
#head;
|
|
537
|
-
#tail;
|
|
538
|
-
#size;
|
|
539
|
-
constructor() {
|
|
540
|
-
this.clear();
|
|
541
|
-
}
|
|
542
|
-
enqueue(value) {
|
|
543
|
-
const node = new Node(value);
|
|
544
|
-
if (this.#head) {
|
|
545
|
-
this.#tail.next = node;
|
|
546
|
-
this.#tail = node;
|
|
547
|
-
} else {
|
|
548
|
-
this.#head = node;
|
|
549
|
-
this.#tail = node;
|
|
550
|
-
}
|
|
551
|
-
this.#size++;
|
|
552
|
-
}
|
|
553
|
-
dequeue() {
|
|
554
|
-
const current = this.#head;
|
|
555
|
-
if (!current) return;
|
|
556
|
-
this.#head = this.#head.next;
|
|
557
|
-
this.#size--;
|
|
558
|
-
if (!this.#head) this.#tail = void 0;
|
|
559
|
-
return current.value;
|
|
560
|
-
}
|
|
561
|
-
peek() {
|
|
562
|
-
if (!this.#head) return;
|
|
563
|
-
return this.#head.value;
|
|
564
|
-
}
|
|
565
|
-
clear() {
|
|
566
|
-
this.#head = void 0;
|
|
567
|
-
this.#tail = void 0;
|
|
568
|
-
this.#size = 0;
|
|
569
|
-
}
|
|
570
|
-
get size() {
|
|
571
|
-
return this.#size;
|
|
572
|
-
}
|
|
573
|
-
*[Symbol.iterator]() {
|
|
574
|
-
let current = this.#head;
|
|
575
|
-
while (current) {
|
|
576
|
-
yield current.value;
|
|
577
|
-
current = current.next;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
*drain() {
|
|
581
|
-
while (this.#head) yield this.dequeue();
|
|
582
|
-
}
|
|
583
|
-
};
|
|
584
|
-
//#endregion
|
|
585
|
-
//#region ../../node_modules/.pnpm/p-limit@7.3.0/node_modules/p-limit/index.js
|
|
586
|
-
function pLimit(concurrency) {
|
|
587
|
-
let rejectOnClear = false;
|
|
588
|
-
if (typeof concurrency === "object") ({concurrency, rejectOnClear = false} = concurrency);
|
|
589
|
-
validateConcurrency(concurrency);
|
|
590
|
-
if (typeof rejectOnClear !== "boolean") throw new TypeError("Expected `rejectOnClear` to be a boolean");
|
|
591
|
-
const queue = new Queue();
|
|
592
|
-
let activeCount = 0;
|
|
593
|
-
const resumeNext = () => {
|
|
594
|
-
if (activeCount < concurrency && queue.size > 0) {
|
|
595
|
-
activeCount++;
|
|
596
|
-
queue.dequeue().run();
|
|
597
|
-
}
|
|
598
|
-
};
|
|
599
|
-
const next = () => {
|
|
600
|
-
activeCount--;
|
|
601
|
-
resumeNext();
|
|
602
|
-
};
|
|
603
|
-
const run = async (function_, resolve, arguments_) => {
|
|
604
|
-
const result = (async () => function_(...arguments_))();
|
|
605
|
-
resolve(result);
|
|
606
|
-
try {
|
|
607
|
-
await result;
|
|
608
|
-
} catch {}
|
|
609
|
-
next();
|
|
610
|
-
};
|
|
611
|
-
const enqueue = (function_, resolve, reject, arguments_) => {
|
|
612
|
-
const queueItem = { reject };
|
|
613
|
-
new Promise((internalResolve) => {
|
|
614
|
-
queueItem.run = internalResolve;
|
|
615
|
-
queue.enqueue(queueItem);
|
|
616
|
-
}).then(run.bind(void 0, function_, resolve, arguments_));
|
|
617
|
-
if (activeCount < concurrency) resumeNext();
|
|
618
|
-
};
|
|
619
|
-
const generator = (function_, ...arguments_) => new Promise((resolve, reject) => {
|
|
620
|
-
enqueue(function_, resolve, reject, arguments_);
|
|
621
|
-
});
|
|
622
|
-
Object.defineProperties(generator, {
|
|
623
|
-
activeCount: { get: () => activeCount },
|
|
624
|
-
pendingCount: { get: () => queue.size },
|
|
625
|
-
clearQueue: { value() {
|
|
626
|
-
if (!rejectOnClear) {
|
|
627
|
-
queue.clear();
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
|
-
const abortError = AbortSignal.abort().reason;
|
|
631
|
-
while (queue.size > 0) queue.dequeue().reject(abortError);
|
|
632
|
-
} },
|
|
633
|
-
concurrency: {
|
|
634
|
-
get: () => concurrency,
|
|
635
|
-
set(newConcurrency) {
|
|
636
|
-
validateConcurrency(newConcurrency);
|
|
637
|
-
concurrency = newConcurrency;
|
|
638
|
-
queueMicrotask(() => {
|
|
639
|
-
while (activeCount < concurrency && queue.size > 0) resumeNext();
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
},
|
|
643
|
-
map: { async value(iterable, function_) {
|
|
644
|
-
const promises = Array.from(iterable, (value, index) => this(function_, value, index));
|
|
645
|
-
return Promise.all(promises);
|
|
646
|
-
} }
|
|
647
|
-
});
|
|
648
|
-
return generator;
|
|
649
|
-
}
|
|
650
|
-
function validateConcurrency(concurrency) {
|
|
651
|
-
if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) throw new TypeError("Expected `concurrency` to be a number from 1 and up");
|
|
652
|
-
}
|
|
653
|
-
//#endregion
|
|
654
|
-
//#region src/FileProcessor.ts
|
|
655
|
-
function joinSources(file) {
|
|
656
|
-
return file.sources.map((item) => (0, _kubb_ast.extractStringsFromNodes)(item.nodes)).filter(Boolean).join("\n\n");
|
|
657
|
-
}
|
|
610
|
+
//#region src/diagnostics.ts
|
|
658
611
|
/**
|
|
659
|
-
*
|
|
660
|
-
* Falls back to joining source values when no matching parser is found.
|
|
661
|
-
*
|
|
662
|
-
* @internal
|
|
612
|
+
* Docs major version, derived from the package version so the link tracks the published major.
|
|
663
613
|
*/
|
|
664
|
-
|
|
665
|
-
#limit = pLimit(100);
|
|
666
|
-
async parse(file, { parsers, extension } = {}) {
|
|
667
|
-
const parseExtName = extension?.[file.extname] || void 0;
|
|
668
|
-
if (!parsers || !file.extname) return joinSources(file);
|
|
669
|
-
const parser = parsers.get(file.extname);
|
|
670
|
-
if (!parser) return joinSources(file);
|
|
671
|
-
return parser.parse(file, { extname: parseExtName });
|
|
672
|
-
}
|
|
673
|
-
async run(files, { parsers, mode = "sequential", extension, onStart, onEnd, onUpdate } = {}) {
|
|
674
|
-
await onStart?.(files);
|
|
675
|
-
const total = files.length;
|
|
676
|
-
let processed = 0;
|
|
677
|
-
const processOne = async (file) => {
|
|
678
|
-
const source = await this.parse(file, {
|
|
679
|
-
extension,
|
|
680
|
-
parsers
|
|
681
|
-
});
|
|
682
|
-
const currentProcessed = ++processed;
|
|
683
|
-
const percentage = currentProcessed / total * 100;
|
|
684
|
-
await onUpdate?.({
|
|
685
|
-
file,
|
|
686
|
-
source,
|
|
687
|
-
processed: currentProcessed,
|
|
688
|
-
percentage,
|
|
689
|
-
total
|
|
690
|
-
});
|
|
691
|
-
};
|
|
692
|
-
if (mode === "sequential") for (const file of files) await processOne(file);
|
|
693
|
-
else await Promise.all(files.map((file) => this.#limit(() => processOne(file))));
|
|
694
|
-
await onEnd?.(files);
|
|
695
|
-
return files;
|
|
696
|
-
}
|
|
697
|
-
};
|
|
698
|
-
//#endregion
|
|
699
|
-
//#region src/createStorage.ts
|
|
614
|
+
const docsMajor = "5.0.0-beta.61".split(".")[0] ?? "5";
|
|
700
615
|
/**
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
* Takes a builder function `(options: TOptions) => Storage` and returns a factory `(options?: TOptions) => Storage`.
|
|
704
|
-
* Kubb provides filesystem and in-memory implementations out of the box.
|
|
705
|
-
*
|
|
706
|
-
* @note Call the returned factory with optional options to instantiate the storage adapter.
|
|
616
|
+
* Narrows a {@link Diagnostic} to the variant for `code`, or `null` when it does not match.
|
|
707
617
|
*
|
|
708
618
|
* @example
|
|
709
619
|
* ```ts
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
* return {
|
|
715
|
-
* name: 'memory',
|
|
716
|
-
* async hasItem(key) { return store.has(key) },
|
|
717
|
-
* async getItem(key) { return store.get(key) ?? null },
|
|
718
|
-
* async setItem(key, value) { store.set(key, value) },
|
|
719
|
-
* async removeItem(key) { store.delete(key) },
|
|
720
|
-
* async getKeys(base) {
|
|
721
|
-
* const keys = [...store.keys()]
|
|
722
|
-
* return base ? keys.filter((k) => k.startsWith(base)) : keys
|
|
723
|
-
* },
|
|
724
|
-
* async clear(base) { if (!base) store.clear() },
|
|
725
|
-
* }
|
|
726
|
-
* })
|
|
727
|
-
*
|
|
728
|
-
* // Instantiate:
|
|
729
|
-
* const storage = memoryStorage()
|
|
620
|
+
* const update = narrow(diagnostic, diagnosticCode.updateAvailable)
|
|
621
|
+
* if (update) {
|
|
622
|
+
* console.log(update.latestVersion)
|
|
623
|
+
* }
|
|
730
624
|
* ```
|
|
731
625
|
*/
|
|
732
|
-
function
|
|
733
|
-
return
|
|
626
|
+
function narrow(diagnostic, code) {
|
|
627
|
+
return diagnostic.code === code ? diagnostic : null;
|
|
734
628
|
}
|
|
735
|
-
//#endregion
|
|
736
|
-
//#region src/storages/fsStorage.ts
|
|
737
629
|
/**
|
|
738
|
-
*
|
|
630
|
+
* Builds a type guard that narrows a {@link Diagnostic} to the variant for `kind`. A diagnostic
|
|
631
|
+
* with no `kind` is treated as a `problem`.
|
|
739
632
|
*/
|
|
740
|
-
function
|
|
741
|
-
return
|
|
633
|
+
function isKind(kind) {
|
|
634
|
+
return (diagnostic) => (diagnostic.kind ?? "problem") === kind;
|
|
742
635
|
}
|
|
743
636
|
/**
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
* This is the default storage when no `storage` option is configured in the root config.
|
|
747
|
-
* Keys are resolved against `process.cwd()`, so root-relative paths such as
|
|
748
|
-
* `src/gen/api/getPets.ts` are written to the correct location without extra configuration.
|
|
637
|
+
* Returns `true` when the diagnostic is a build {@link ProblemDiagnostic}.
|
|
749
638
|
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
753
|
-
*
|
|
754
|
-
*
|
|
639
|
+
* @example
|
|
640
|
+
* ```ts
|
|
641
|
+
* if (isProblem(diagnostic)) {
|
|
642
|
+
* console.log(diagnostic.location)
|
|
643
|
+
* }
|
|
644
|
+
* ```
|
|
645
|
+
*/
|
|
646
|
+
const isProblem = isKind("problem");
|
|
647
|
+
/**
|
|
648
|
+
* Returns `true` when the diagnostic is a per-plugin {@link PerformanceDiagnostic}.
|
|
755
649
|
*
|
|
756
650
|
* @example
|
|
757
651
|
* ```ts
|
|
758
|
-
*
|
|
759
|
-
*
|
|
652
|
+
* const timings = diagnostics.filter(isPerformance)
|
|
653
|
+
* ```
|
|
654
|
+
*/
|
|
655
|
+
const isPerformance = isKind("performance");
|
|
656
|
+
/**
|
|
657
|
+
* Returns `true` when the diagnostic is a version-update {@link UpdateDiagnostic}.
|
|
760
658
|
*
|
|
761
|
-
*
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
* }
|
|
659
|
+
* @example
|
|
660
|
+
* ```ts
|
|
661
|
+
* if (isUpdate(diagnostic)) {
|
|
662
|
+
* console.log(diagnostic.latestVersion)
|
|
663
|
+
* }
|
|
766
664
|
* ```
|
|
767
665
|
*/
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
}
|
|
666
|
+
const isUpdate = isKind("update");
|
|
667
|
+
/**
|
|
668
|
+
* Glyph and accent color per severity, matching the miette/oxlint convention
|
|
669
|
+
* (`×` error, `⚠` warning, `ℹ` advice).
|
|
670
|
+
*/
|
|
671
|
+
const severityStyle = {
|
|
672
|
+
error: {
|
|
673
|
+
glyph: "×",
|
|
674
|
+
color: "red"
|
|
778
675
|
},
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
} catch (error) {
|
|
783
|
-
if (isMissingPathError(error)) return null;
|
|
784
|
-
throw new Error(`Failed to read storage item "${key}"`, { cause: error });
|
|
785
|
-
}
|
|
676
|
+
warning: {
|
|
677
|
+
glyph: "⚠",
|
|
678
|
+
color: "yellow"
|
|
786
679
|
},
|
|
787
|
-
|
|
788
|
-
|
|
680
|
+
info: {
|
|
681
|
+
glyph: "ℹ",
|
|
682
|
+
color: "blue"
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
/**
|
|
686
|
+
* Explanation for every {@link diagnosticCode}. Use {@link Diagnostics.explain} to look one up
|
|
687
|
+
* and `Diagnostics.docsUrl` for the matching kubb.dev page.
|
|
688
|
+
*/
|
|
689
|
+
const diagnosticCatalog = {
|
|
690
|
+
[require_memoryStorage.diagnosticCode.unknown]: {
|
|
691
|
+
title: "Unknown error",
|
|
692
|
+
cause: "An error was thrown without a stable Kubb code, so it is reported as-is.",
|
|
693
|
+
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."
|
|
789
694
|
},
|
|
790
|
-
|
|
791
|
-
|
|
695
|
+
[require_memoryStorage.diagnosticCode.inputNotFound]: {
|
|
696
|
+
title: "Input not found",
|
|
697
|
+
cause: "The file or URL set in `input.path` (or passed as `kubb generate PATH`) could not be read.",
|
|
698
|
+
fix: "Check that the path or URL exists and is readable, then set it in `input.path` or pass it on the CLI."
|
|
792
699
|
},
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
let entries;
|
|
798
|
-
try {
|
|
799
|
-
entries = await (0, node_fs_promises.readdir)(dir, { withFileTypes: true });
|
|
800
|
-
} catch (error) {
|
|
801
|
-
if (isMissingPathError(error)) return;
|
|
802
|
-
throw new Error(`Failed to list storage keys under "${resolvedBase}"`, { cause: error });
|
|
803
|
-
}
|
|
804
|
-
for (const entry of entries) {
|
|
805
|
-
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
806
|
-
if (entry.isDirectory()) await walk((0, node_path.join)(dir, entry.name), rel);
|
|
807
|
-
else keys.push(rel);
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
await walk(resolvedBase, "");
|
|
811
|
-
return keys;
|
|
700
|
+
[require_memoryStorage.diagnosticCode.inputRequired]: {
|
|
701
|
+
title: "Input required",
|
|
702
|
+
cause: "An adapter is configured but no `input` was provided.",
|
|
703
|
+
fix: "Set `input.path` (a file or URL) or `input.data` (an inline spec) in your Kubb config."
|
|
812
704
|
},
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
705
|
+
[require_memoryStorage.diagnosticCode.refNotFound]: {
|
|
706
|
+
title: "Reference not found",
|
|
707
|
+
cause: "A `$ref` could not be resolved in the source document.",
|
|
708
|
+
fix: "Add the missing definition (for example under `components.schemas`) or fix the `$ref`. Run `kubb validate` to check the spec."
|
|
709
|
+
},
|
|
710
|
+
[require_memoryStorage.diagnosticCode.invalidServerVariable]: {
|
|
711
|
+
title: "Invalid server variable",
|
|
712
|
+
cause: "A server variable value is not allowed by its `enum`.",
|
|
713
|
+
fix: "Use one of the values listed in the server variable `enum`, or update the spec."
|
|
714
|
+
},
|
|
715
|
+
[require_memoryStorage.diagnosticCode.pluginNotFound]: {
|
|
716
|
+
title: "Plugin not found",
|
|
717
|
+
cause: "A plugin that another plugin depends on is missing from the config.",
|
|
718
|
+
fix: "Add the required plugin to the `plugins` array in kubb.config.ts, or remove the dependency on it."
|
|
719
|
+
},
|
|
720
|
+
[require_memoryStorage.diagnosticCode.pluginFailed]: {
|
|
721
|
+
title: "Plugin failed",
|
|
722
|
+
cause: "A plugin threw while generating, or reported an error through `ctx.error`.",
|
|
723
|
+
fix: "Read the underlying error and check the plugin options and the schema or operation it failed on."
|
|
724
|
+
},
|
|
725
|
+
[require_memoryStorage.diagnosticCode.pluginWarning]: {
|
|
726
|
+
title: "Plugin warning",
|
|
727
|
+
cause: "A plugin reported a non-fatal warning through `ctx.warn`.",
|
|
728
|
+
fix: "Review the message. It does not fail the build; adjust the plugin options or input if the warning is unwanted."
|
|
729
|
+
},
|
|
730
|
+
[require_memoryStorage.diagnosticCode.pluginInfo]: {
|
|
731
|
+
title: "Plugin info",
|
|
732
|
+
cause: "A plugin reported an informational message through `ctx.info`.",
|
|
733
|
+
fix: "Informational only. No action is required."
|
|
734
|
+
},
|
|
735
|
+
[require_memoryStorage.diagnosticCode.unsupportedFormat]: {
|
|
736
|
+
title: "Unsupported format",
|
|
737
|
+
cause: "A schema uses a `format` Kubb does not map to a specific type, so it falls back to the base type.",
|
|
738
|
+
fix: "Use a format Kubb supports, or handle the custom format with a parser or plugin."
|
|
739
|
+
},
|
|
740
|
+
[require_memoryStorage.diagnosticCode.deprecated]: {
|
|
741
|
+
title: "Deprecated",
|
|
742
|
+
cause: "A referenced schema or operation is marked `deprecated`.",
|
|
743
|
+
fix: "Migrate off the deprecated definition if the warning is unwanted."
|
|
744
|
+
},
|
|
745
|
+
[require_memoryStorage.diagnosticCode.adapterRequired]: {
|
|
746
|
+
title: "Adapter required",
|
|
747
|
+
cause: "An action needs an adapter but none is configured.",
|
|
748
|
+
fix: "Set `adapter` in kubb.config.ts, for example `adapterOas()`."
|
|
749
|
+
},
|
|
750
|
+
[require_memoryStorage.diagnosticCode.pathTraversal]: {
|
|
751
|
+
title: "Path traversal",
|
|
752
|
+
cause: "A resolved output path escaped the output directory, which can stem from a path traversal in the spec or a misconfigured `group.name`.",
|
|
753
|
+
fix: "Keep generated paths within the output directory. Review the `group.name` function and the names coming from the spec."
|
|
754
|
+
},
|
|
755
|
+
[require_memoryStorage.diagnosticCode.invalidPluginOptions]: {
|
|
756
|
+
title: "Invalid plugin options",
|
|
757
|
+
cause: "A plugin was configured with options that cannot be honored, for example `output.mode: 'file'` paired with a `group` option.",
|
|
758
|
+
fix: "Fix the plugin options. A single-file output has nothing to group, so remove the `group` option or use `output.mode: 'directory'`."
|
|
759
|
+
},
|
|
760
|
+
[require_memoryStorage.diagnosticCode.hookFailed]: {
|
|
761
|
+
title: "Hook failed",
|
|
762
|
+
cause: "A post-generate shell hook (`hooks.done`) exited with a non-zero status.",
|
|
763
|
+
fix: "Check the command is installed and correct, and run it manually to see the error."
|
|
764
|
+
},
|
|
765
|
+
[require_memoryStorage.diagnosticCode.formatFailed]: {
|
|
766
|
+
title: "Format failed",
|
|
767
|
+
cause: "The formatter pass over the generated files failed.",
|
|
768
|
+
fix: "Check the formatter (oxfmt, biome, or prettier) is installed and its config is valid, then run it manually on the output."
|
|
769
|
+
},
|
|
770
|
+
[require_memoryStorage.diagnosticCode.lintFailed]: {
|
|
771
|
+
title: "Lint failed",
|
|
772
|
+
cause: "The linter pass over the generated files failed.",
|
|
773
|
+
fix: "Check the linter (oxlint, biome, or eslint) is installed and its config is valid, then run it manually on the output."
|
|
774
|
+
},
|
|
775
|
+
[require_memoryStorage.diagnosticCode.performance]: {
|
|
776
|
+
title: "Performance",
|
|
777
|
+
cause: "Not a failure. Records a plugin’s elapsed time, summed into the run total.",
|
|
778
|
+
fix: "No action. This is an informational metric."
|
|
779
|
+
},
|
|
780
|
+
[require_memoryStorage.diagnosticCode.updateAvailable]: {
|
|
781
|
+
title: "Update available",
|
|
782
|
+
cause: "A newer Kubb version is published on npm than the one running.",
|
|
783
|
+
fix: "Update the `@kubb/*` packages, for example `npm install -g @kubb/cli`, to get the latest fixes."
|
|
816
784
|
}
|
|
817
|
-
}
|
|
818
|
-
//#endregion
|
|
819
|
-
//#region package.json
|
|
820
|
-
var version = "5.0.0-beta.6";
|
|
821
|
-
//#endregion
|
|
822
|
-
//#region src/utils/diagnostics.ts
|
|
785
|
+
};
|
|
823
786
|
/**
|
|
824
|
-
*
|
|
787
|
+
* Static helpers for working with {@link Diagnostic}s, plus the run-scoped sink
|
|
788
|
+
* that lets deep code report a diagnostic without threading a callback.
|
|
825
789
|
*
|
|
826
|
-
*
|
|
827
|
-
*
|
|
790
|
+
* The sink lives in a single `AsyncLocalStorage` in the `@kubb/core` bundle.
|
|
791
|
+
* `Diagnostics.scope` activates it for a run, so anything inside that run (the
|
|
792
|
+
* adapter parse, a lazily consumed stream, a generator) reports through
|
|
793
|
+
* `Diagnostics.report` and lands in the same run.
|
|
828
794
|
*/
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
795
|
+
var Diagnostics = class Diagnostics {
|
|
796
|
+
static #reporterStorage = new node_async_hooks.AsyncLocalStorage();
|
|
797
|
+
/**
|
|
798
|
+
* The diagnostic code catalog, exposed as `Diagnostics.code` (e.g. `Diagnostics.code.refNotFound`).
|
|
799
|
+
*/
|
|
800
|
+
static code = require_memoryStorage.diagnosticCode;
|
|
801
|
+
/**
|
|
802
|
+
* Type guard for a build {@link ProblemDiagnostic}.
|
|
803
|
+
*/
|
|
804
|
+
static isProblem = isProblem;
|
|
805
|
+
/**
|
|
806
|
+
* Type guard for a version-update {@link UpdateDiagnostic}.
|
|
807
|
+
*/
|
|
808
|
+
static isUpdate = isUpdate;
|
|
809
|
+
/**
|
|
810
|
+
* Type guard for a per-plugin {@link PerformanceDiagnostic}.
|
|
811
|
+
*/
|
|
812
|
+
static isPerformance = isPerformance;
|
|
813
|
+
/**
|
|
814
|
+
* Narrows a {@link Diagnostic} to the variant for `code`, or `null` when it does not match.
|
|
815
|
+
*/
|
|
816
|
+
static narrow = narrow;
|
|
817
|
+
/**
|
|
818
|
+
* An `Error` that carries a {@link Diagnostic}, so structured problems can flow
|
|
819
|
+
* through the existing throw/catch paths while keeping their code and location.
|
|
820
|
+
*
|
|
821
|
+
* @example
|
|
822
|
+
* ```ts
|
|
823
|
+
* throw new Diagnostics.Error({ code: diagnosticCode.refNotFound, severity: 'error', message: `Could not find ${ref}`, location: { kind: 'schema', pointer: ref, ref } })
|
|
824
|
+
* ```
|
|
825
|
+
*/
|
|
826
|
+
static Error = class DiagnosticError extends Error {
|
|
827
|
+
diagnostic;
|
|
828
|
+
constructor(diagnostic) {
|
|
829
|
+
super(diagnostic.message, { cause: diagnostic.cause });
|
|
830
|
+
this.name = "DiagnosticError";
|
|
831
|
+
this.diagnostic = diagnostic;
|
|
832
|
+
}
|
|
836
833
|
};
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
834
|
+
/**
|
|
835
|
+
* Structural check for a {@link Diagnostics.Error}, including one thrown from a duplicated
|
|
836
|
+
* `@kubb/core` copy where `instanceof` fails. Matches on the `name` and a `diagnostic`
|
|
837
|
+
* that carries a `code`.
|
|
838
|
+
*/
|
|
839
|
+
static isError(error) {
|
|
840
|
+
if (error instanceof Diagnostics.Error) return true;
|
|
841
|
+
return error instanceof Error && error.name === "DiagnosticError" && "diagnostic" in error && typeof error.diagnostic === "object" && error.diagnostic !== null && typeof error.diagnostic?.code === "string";
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Runs `fn` with `sink` as the active diagnostic sink for the whole async
|
|
845
|
+
* subtree, so {@link Diagnostics.report} reaches it from anywhere inside.
|
|
846
|
+
*/
|
|
847
|
+
static scope(sink, fn) {
|
|
848
|
+
return Diagnostics.#reporterStorage.run(sink, fn);
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Collects a diagnostic into the active build via the run-scoped sink, without throwing.
|
|
852
|
+
* Returns `true` when a run consumed it, `false` when called outside a {@link Diagnostics.scope}
|
|
853
|
+
* (so callers can fall back to throwing). Use a `warning`/`info` severity for non-fatal issues.
|
|
854
|
+
* For rendering a diagnostic live on the hook bus, use {@link Diagnostics.emit} instead.
|
|
855
|
+
*/
|
|
856
|
+
static report(diagnostic) {
|
|
857
|
+
const sink = Diagnostics.#reporterStorage.getStore();
|
|
858
|
+
if (!sink) return false;
|
|
859
|
+
sink(diagnostic);
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Emits a diagnostic on the run's `kubb:diagnostic` event so the loggers render it live.
|
|
864
|
+
* Use it instead of calling `hooks.emit('kubb:diagnostic', ...)` directly. To collect a
|
|
865
|
+
* diagnostic into the build result from deep in a run, use {@link Diagnostics.report} instead.
|
|
866
|
+
*/
|
|
867
|
+
static async emit(hooks, diagnostic) {
|
|
868
|
+
await hooks.emit("kubb:diagnostic", { diagnostic });
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Coerces any thrown value into a {@link ProblemDiagnostic}. A {@link Diagnostics.Error}
|
|
872
|
+
* keeps its structured data, and anything else becomes a `KUBB_UNKNOWN` error.
|
|
873
|
+
*/
|
|
874
|
+
static from(error) {
|
|
875
|
+
const seen = /* @__PURE__ */ new Set();
|
|
876
|
+
let current = error;
|
|
877
|
+
let root;
|
|
878
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
879
|
+
if (Diagnostics.isError(current)) return current.diagnostic;
|
|
880
|
+
seen.add(current);
|
|
881
|
+
root = current;
|
|
882
|
+
current = current.cause;
|
|
873
883
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
884
|
+
return {
|
|
885
|
+
code: require_memoryStorage.diagnosticCode.unknown,
|
|
886
|
+
severity: "error",
|
|
887
|
+
message: root ? root.message : require_memoryStorage.getErrorMessage(error),
|
|
888
|
+
cause: root
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Builds a per-plugin performance record. Reporters sum these into the run total.
|
|
893
|
+
*/
|
|
894
|
+
static performance({ plugin, duration }) {
|
|
895
|
+
return {
|
|
896
|
+
kind: "performance",
|
|
897
|
+
code: require_memoryStorage.diagnosticCode.performance,
|
|
898
|
+
severity: "info",
|
|
899
|
+
message: `${plugin} generated in ${Math.round(duration)}ms`,
|
|
900
|
+
plugin,
|
|
901
|
+
duration
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Builds the version-update notice shown when a newer Kubb is published on npm.
|
|
906
|
+
*/
|
|
907
|
+
static update({ currentVersion, latestVersion }) {
|
|
908
|
+
return {
|
|
909
|
+
kind: "update",
|
|
910
|
+
code: require_memoryStorage.diagnosticCode.updateAvailable,
|
|
911
|
+
severity: "info",
|
|
912
|
+
message: `Update available: v${currentVersion} → v${latestVersion}. Run \`npm install -g @kubb/cli\` to update.`,
|
|
913
|
+
currentVersion,
|
|
914
|
+
latestVersion
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* True when any diagnostic is an error, the severity that fails a build. Non-error
|
|
919
|
+
* diagnostics are ignored.
|
|
920
|
+
*/
|
|
921
|
+
static hasError(diagnostics) {
|
|
922
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Names of the plugins that failed, deduped, derived from the error diagnostics
|
|
926
|
+
* that carry a `plugin`.
|
|
927
|
+
*/
|
|
928
|
+
static failedPlugins(diagnostics) {
|
|
929
|
+
const names = /* @__PURE__ */ new Set();
|
|
930
|
+
for (const diagnostic of diagnostics) if (diagnostic.severity === "error" && diagnostic.plugin) names.add(diagnostic.plugin);
|
|
931
|
+
return [...names];
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Counts `problem` diagnostics by severity for the run summary. `timing`
|
|
935
|
+
* diagnostics are ignored.
|
|
936
|
+
*/
|
|
937
|
+
static count(diagnostics) {
|
|
938
|
+
let errors = 0;
|
|
939
|
+
let warnings = 0;
|
|
940
|
+
let infos = 0;
|
|
941
|
+
for (const diagnostic of diagnostics) {
|
|
942
|
+
if (!isProblem(diagnostic)) continue;
|
|
943
|
+
if (diagnostic.severity === "error") errors += 1;
|
|
944
|
+
else if (diagnostic.severity === "warning") warnings += 1;
|
|
945
|
+
else infos += 1;
|
|
878
946
|
}
|
|
947
|
+
return {
|
|
948
|
+
errors,
|
|
949
|
+
warnings,
|
|
950
|
+
infos
|
|
951
|
+
};
|
|
879
952
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
953
|
+
/**
|
|
954
|
+
* Drops duplicate `problem` diagnostics that share a code, location pointer, and
|
|
955
|
+
* plugin, so the same issue reported across several passes is shown once. Non-problem
|
|
956
|
+
* diagnostics are always kept.
|
|
957
|
+
*/
|
|
958
|
+
static dedupe(diagnostics) {
|
|
959
|
+
const seen = /* @__PURE__ */ new Set();
|
|
960
|
+
const result = [];
|
|
961
|
+
for (const diagnostic of diagnostics) {
|
|
962
|
+
if (!isProblem(diagnostic)) {
|
|
963
|
+
result.push(diagnostic);
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
const pointer = diagnostic.location && "pointer" in diagnostic.location ? diagnostic.location.pointer : "";
|
|
967
|
+
const key = `${diagnostic.code} ${pointer} ${diagnostic.plugin ?? ""}`;
|
|
968
|
+
if (seen.has(key)) continue;
|
|
969
|
+
seen.add(key);
|
|
970
|
+
result.push(diagnostic);
|
|
971
|
+
}
|
|
972
|
+
return result;
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Builds the kubb.dev docs URL for a diagnostic code, e.g.
|
|
976
|
+
* `KUBB_REF_NOT_FOUND` → `https://kubb.dev/docs/5.x/reference/diagnostics/kubb-ref-not-found`.
|
|
977
|
+
*/
|
|
978
|
+
static docsUrl(code) {
|
|
979
|
+
return `https://kubb.dev/docs/${docsMajor}.x/reference/diagnostics/${code.toLowerCase().replaceAll("_", "-")}`;
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* The catalog entry for a code: its title, cause, and fix. Mirrors the kubb.dev
|
|
983
|
+
* `/diagnostics/<slug>` page.
|
|
984
|
+
*/
|
|
985
|
+
static explain(code) {
|
|
986
|
+
return diagnosticCatalog[code];
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Reduces a diagnostic to its JSON-safe fields plus a `docsUrl`, for machine-readable
|
|
990
|
+
* consumers. The `cause`, `kind`, and `duration` are dropped, and absent optional
|
|
991
|
+
* fields are omitted rather than set to `undefined`.
|
|
992
|
+
*/
|
|
993
|
+
static serialize(diagnostic) {
|
|
994
|
+
const problem = isProblem(diagnostic) ? diagnostic : void 0;
|
|
995
|
+
return {
|
|
996
|
+
code: diagnostic.code,
|
|
997
|
+
severity: diagnostic.severity,
|
|
998
|
+
message: diagnostic.message,
|
|
999
|
+
...problem?.location ? { location: problem.location } : {},
|
|
1000
|
+
...problem?.help ? { help: problem.help } : {},
|
|
1001
|
+
...problem?.plugin ? { plugin: problem.plugin } : {},
|
|
1002
|
+
...diagnostic.code === require_memoryStorage.diagnosticCode.unknown ? {} : { docsUrl: Diagnostics.docsUrl(diagnostic.code) }
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Renders a {@link Diagnostic} for terminal output as its parts: the colored severity `symbol`
|
|
1007
|
+
* (the gutter glyph), the `plugin(CODE): message` `headline`, and the `details` lines (optional
|
|
1008
|
+
* `at <pointer>`, `help:`, and `docs:`).
|
|
1009
|
+
*
|
|
1010
|
+
* Hosts compose these to fit their gutter: a clack logger passes `symbol` as its own gutter and
|
|
1011
|
+
* `[headline, ...details]` as the message, while plain text outputs use {@link Diagnostics.formatLines}.
|
|
1012
|
+
*/
|
|
1013
|
+
static format(diagnostic) {
|
|
1014
|
+
const { code, severity, message } = diagnostic;
|
|
1015
|
+
const { glyph, color } = severityStyle[severity];
|
|
1016
|
+
const problem = isProblem(diagnostic) ? diagnostic : void 0;
|
|
1017
|
+
const rule = (0, node_util.styleText)(color, (0, node_util.styleText)("bold", problem?.plugin ? `${problem.plugin}(${code})` : code));
|
|
1018
|
+
const details = [];
|
|
1019
|
+
if (problem?.location && "pointer" in problem.location) details.push(` ${(0, node_util.styleText)("dim", "at")} ${(0, node_util.styleText)("cyan", problem.location.pointer)}`);
|
|
1020
|
+
if (problem?.help) details.push(` ${(0, node_util.styleText)("cyan", "help:")} ${problem.help}`);
|
|
1021
|
+
if (code !== require_memoryStorage.diagnosticCode.unknown) details.push(` ${(0, node_util.styleText)("dim", "docs:")} ${(0, node_util.styleText)("cyan", Diagnostics.docsUrl(code))}`);
|
|
1022
|
+
return {
|
|
1023
|
+
symbol: (0, node_util.styleText)(color, (0, node_util.styleText)("bold", glyph)),
|
|
1024
|
+
headline: `${rule}: ${message}`,
|
|
1025
|
+
details
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* The self-contained block form of {@link Diagnostics.format}: `${symbol} ${headline}` followed by
|
|
1030
|
+
* the detail lines. Used where there is no gutter to own the symbol (plain and file output).
|
|
1031
|
+
*/
|
|
1032
|
+
static formatLines(diagnostic) {
|
|
1033
|
+
const { symbol, headline, details } = Diagnostics.format(diagnostic);
|
|
1034
|
+
return [`${symbol} ${headline}`, ...details];
|
|
929
1035
|
}
|
|
1036
|
+
};
|
|
1037
|
+
//#endregion
|
|
1038
|
+
//#region src/definePlugin.ts
|
|
1039
|
+
/**
|
|
1040
|
+
* Merges the `output.mode` default into the output config and validates the combination.
|
|
1041
|
+
* Throws `KUBB_INVALID_PLUGIN_OPTIONS` when `mode: 'file'` is paired with a `group` option,
|
|
1042
|
+
* since a single-file output has nothing to group.
|
|
1043
|
+
*/
|
|
1044
|
+
function normalizeOutput({ output, group, pluginName }) {
|
|
1045
|
+
const mode = output.mode ?? "directory";
|
|
1046
|
+
if (mode === "file" && group) throw new Diagnostics.Error({
|
|
1047
|
+
code: require_memoryStorage.diagnosticCode.invalidPluginOptions,
|
|
1048
|
+
severity: "error",
|
|
1049
|
+
message: `Plugin "${pluginName}" sets \`output.mode: 'file'\` but also configures a \`group\` option.`,
|
|
1050
|
+
help: "A single-file output has nothing to group. Remove the `group` option, or use `output.mode: 'directory'` to organize files into subdirectories.",
|
|
1051
|
+
location: { kind: "config" },
|
|
1052
|
+
plugin: pluginName
|
|
1053
|
+
});
|
|
930
1054
|
return {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
driver,
|
|
934
|
-
sources,
|
|
935
|
-
storage
|
|
1055
|
+
...output,
|
|
1056
|
+
mode
|
|
936
1057
|
};
|
|
937
1058
|
}
|
|
938
1059
|
/**
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
942
|
-
*
|
|
943
|
-
* `
|
|
944
|
-
*
|
|
945
|
-
*
|
|
946
|
-
*
|
|
1060
|
+
* Wraps a plugin factory and returns a function that accepts user options and
|
|
1061
|
+
* yields a typed `Plugin`. Lifecycle handlers go inside a single `hooks` object.
|
|
1062
|
+
*
|
|
1063
|
+
* Pass a `PluginFactoryOptions` type parameter to get a typed `ctx` inside
|
|
1064
|
+
* `kubb:plugin:setup`. Plugin names should follow the `plugin-<feature>`
|
|
1065
|
+
* convention (`plugin-react-query`, `plugin-zod`, ...).
|
|
1066
|
+
*
|
|
1067
|
+
* @example
|
|
1068
|
+
* ```ts
|
|
1069
|
+
* import { definePlugin } from '@kubb/core'
|
|
1070
|
+
*
|
|
1071
|
+
* export const pluginTs = definePlugin((options: { prefix?: string } = {}) => ({
|
|
1072
|
+
* name: 'plugin-ts',
|
|
1073
|
+
* hooks: {
|
|
1074
|
+
* 'kubb:plugin:setup'(ctx) {
|
|
1075
|
+
* ctx.setResolver(resolverTs)
|
|
1076
|
+
* },
|
|
1077
|
+
* },
|
|
1078
|
+
* }))
|
|
1079
|
+
* ```
|
|
1080
|
+
*/
|
|
1081
|
+
function definePlugin(factory) {
|
|
1082
|
+
return (options) => factory(options ?? {});
|
|
1083
|
+
}
|
|
1084
|
+
//#endregion
|
|
1085
|
+
//#region src/defineResolver.ts
|
|
1086
|
+
/**
|
|
1087
|
+
* Merges document `meta` with per-file `file` context into the `BannerMeta` passed to a
|
|
1088
|
+
* `banner`/`footer` function. Missing fields default to empty/`false` so the object shape
|
|
1089
|
+
* is stable even when a caller (e.g. the barrel plugin) has no document metadata.
|
|
947
1090
|
*/
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1091
|
+
function buildBannerMeta({ meta, file }) {
|
|
1092
|
+
return {
|
|
1093
|
+
title: meta?.title,
|
|
1094
|
+
description: meta?.description,
|
|
1095
|
+
version: meta?.version,
|
|
1096
|
+
baseURL: meta?.baseURL,
|
|
1097
|
+
circularNames: meta?.circularNames ?? [],
|
|
1098
|
+
enumNames: meta?.enumNames ?? [],
|
|
1099
|
+
filePath: file?.path ?? "",
|
|
1100
|
+
baseName: file?.baseName ?? "",
|
|
1101
|
+
isBarrel: file?.isBarrel ?? false,
|
|
1102
|
+
isAggregation: file?.isAggregation ?? false
|
|
960
1103
|
};
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
const hasSchemaNameIncludes = include?.some(({ type }) => type === "schemaName") ?? false;
|
|
970
|
-
let allowedSchemaNames;
|
|
971
|
-
if (hasOperationBasedIncludes && !hasSchemaNameIncludes) allowedSchemaNames = (0, _kubb_ast.collectUsedSchemaNames)(inputNode.operations.filter((op) => resolver.resolveOptions(op, {
|
|
972
|
-
options: plugin.options,
|
|
973
|
-
exclude,
|
|
974
|
-
include,
|
|
975
|
-
override
|
|
976
|
-
}) !== null), inputNode.schemas);
|
|
977
|
-
await (0, _kubb_ast.walk)(inputNode, {
|
|
978
|
-
depth: "shallow",
|
|
979
|
-
async schema(node) {
|
|
980
|
-
const transformedNode = plugin.transformer ? (0, _kubb_ast.transform)(node, plugin.transformer) : node;
|
|
981
|
-
if (allowedSchemaNames !== void 0 && transformedNode.name && !allowedSchemaNames.has(transformedNode.name)) return;
|
|
982
|
-
const options = resolver.resolveOptions(transformedNode, {
|
|
983
|
-
options: plugin.options,
|
|
984
|
-
exclude,
|
|
985
|
-
include,
|
|
986
|
-
override
|
|
987
|
-
});
|
|
988
|
-
if (options === null) return;
|
|
989
|
-
const ctx = {
|
|
990
|
-
...generatorContext,
|
|
991
|
-
options
|
|
992
|
-
};
|
|
993
|
-
for (const gen of generators) {
|
|
994
|
-
if (!gen.schema) continue;
|
|
995
|
-
await require_PluginDriver.applyHookResult(await gen.schema(transformedNode, ctx), driver, resolveRenderer(gen));
|
|
996
|
-
}
|
|
997
|
-
await driver.hooks.emit("kubb:generate:schema", transformedNode, ctx);
|
|
998
|
-
},
|
|
999
|
-
async operation(node) {
|
|
1000
|
-
const transformedNode = plugin.transformer ? (0, _kubb_ast.transform)(node, plugin.transformer) : node;
|
|
1001
|
-
const options = resolver.resolveOptions(transformedNode, {
|
|
1002
|
-
options: plugin.options,
|
|
1003
|
-
exclude,
|
|
1004
|
-
include,
|
|
1005
|
-
override
|
|
1006
|
-
});
|
|
1007
|
-
if (options !== null) {
|
|
1008
|
-
collectedOperations.push(transformedNode);
|
|
1009
|
-
const ctx = {
|
|
1010
|
-
...generatorContext,
|
|
1011
|
-
options
|
|
1012
|
-
};
|
|
1013
|
-
for (const gen of generators) {
|
|
1014
|
-
if (!gen.operation) continue;
|
|
1015
|
-
await require_PluginDriver.applyHookResult(await gen.operation(transformedNode, ctx), driver, resolveRenderer(gen));
|
|
1016
|
-
}
|
|
1017
|
-
await driver.hooks.emit("kubb:generate:operation", transformedNode, ctx);
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
});
|
|
1021
|
-
if (collectedOperations.length > 0) {
|
|
1022
|
-
const ctx = {
|
|
1023
|
-
...generatorContext,
|
|
1024
|
-
options: plugin.options
|
|
1025
|
-
};
|
|
1026
|
-
for (const gen of generators) {
|
|
1027
|
-
if (!gen.operations) continue;
|
|
1028
|
-
await require_PluginDriver.applyHookResult(await gen.operations(collectedOperations, ctx), driver, resolveRenderer(gen));
|
|
1104
|
+
}
|
|
1105
|
+
const stringPatternCache = /* @__PURE__ */ new Map();
|
|
1106
|
+
function testPattern(value, pattern) {
|
|
1107
|
+
if (typeof pattern === "string") {
|
|
1108
|
+
let regex = stringPatternCache.get(pattern);
|
|
1109
|
+
if (!regex) {
|
|
1110
|
+
regex = new RegExp(pattern);
|
|
1111
|
+
stringPatternCache.set(pattern, regex);
|
|
1029
1112
|
}
|
|
1030
|
-
|
|
1113
|
+
return regex.test(value);
|
|
1031
1114
|
}
|
|
1115
|
+
return value.match(pattern) !== null;
|
|
1032
1116
|
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1117
|
+
/**
|
|
1118
|
+
* Checks if an operation matches a pattern for a given filter type (`tag`, `operationId`, `path`, `method`).
|
|
1119
|
+
*/
|
|
1120
|
+
function matchesOperationPattern(node, type, pattern) {
|
|
1121
|
+
if (type === "tag") return node.tags.some((tag) => testPattern(tag, pattern));
|
|
1122
|
+
if (type === "operationId") return testPattern(node.operationId, pattern);
|
|
1123
|
+
if (type === "path") return node.path !== void 0 && testPattern(node.path, pattern);
|
|
1124
|
+
if (type === "method") return node.method !== void 0 && testPattern(node.method.toLowerCase(), pattern);
|
|
1125
|
+
if (type === "contentType") return node.requestBody?.content?.some((c) => testPattern(c.contentType, pattern)) ?? false;
|
|
1126
|
+
return false;
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Checks if a schema matches a pattern for a given filter type (`schemaName`).
|
|
1130
|
+
*
|
|
1131
|
+
* Returns `null` when the filter type doesn't apply to schemas.
|
|
1132
|
+
*/
|
|
1133
|
+
function matchesSchemaPattern(node, type, pattern) {
|
|
1134
|
+
if (type === "schemaName") return node.name ? testPattern(node.name, pattern) : false;
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Default name resolver used by `defineResolver`.
|
|
1139
|
+
*
|
|
1140
|
+
* - `camelCase` for `file`, with dotted names split into `/`-joined nested paths.
|
|
1141
|
+
* - `PascalCase` for `type`.
|
|
1142
|
+
* - `camelCase` for `function` and everything else.
|
|
1143
|
+
*/
|
|
1144
|
+
function defaultResolver(name, type) {
|
|
1145
|
+
if (type === "file") return toFilePath(name);
|
|
1146
|
+
if (type === "type") return require_memoryStorage.pascalCase(name);
|
|
1147
|
+
return require_memoryStorage.camelCase(name);
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Default option resolver. Applies include/exclude filters and merges matching override options.
|
|
1151
|
+
*
|
|
1152
|
+
* Returns `null` when the node is filtered out by an `exclude` rule or not matched by any `include` rule.
|
|
1153
|
+
*
|
|
1154
|
+
* @example Include/exclude filtering
|
|
1155
|
+
* ```ts
|
|
1156
|
+
* const options = defaultResolveOptions(operationNode, {
|
|
1157
|
+
* options: { output: 'types' },
|
|
1158
|
+
* exclude: [{ type: 'tag', pattern: 'internal' }],
|
|
1159
|
+
* })
|
|
1160
|
+
* // → null when node has tag 'internal'
|
|
1161
|
+
* ```
|
|
1162
|
+
*
|
|
1163
|
+
* @example Override merging
|
|
1164
|
+
* ```ts
|
|
1165
|
+
* const options = defaultResolveOptions(operationNode, {
|
|
1166
|
+
* options: { enumType: 'asConst' },
|
|
1167
|
+
* override: [{ type: 'operationId', pattern: 'listPets', options: { enumType: 'enum' } }],
|
|
1168
|
+
* })
|
|
1169
|
+
* // → { enumType: 'enum' } when operationId matches
|
|
1170
|
+
* ```
|
|
1171
|
+
*/
|
|
1172
|
+
const resolveOptionsCache = /* @__PURE__ */ new WeakMap();
|
|
1173
|
+
function computeOptions(node, options, exclude, include, override) {
|
|
1174
|
+
if (_kubb_ast.operationDef.is(node)) {
|
|
1175
|
+
if (exclude.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null;
|
|
1176
|
+
if (include && !include.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null;
|
|
1177
|
+
const overrideOptions = override.find(({ type, pattern }) => matchesOperationPattern(node, type, pattern))?.options;
|
|
1178
|
+
return {
|
|
1179
|
+
...options,
|
|
1180
|
+
...overrideOptions
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
if (_kubb_ast.schemaDef.is(node)) {
|
|
1184
|
+
if (exclude.some(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)) return null;
|
|
1185
|
+
if (include) {
|
|
1186
|
+
const applicable = include.map(({ type, pattern }) => matchesSchemaPattern(node, type, pattern)).filter((result) => result !== null);
|
|
1187
|
+
if (applicable.length > 0 && !applicable.includes(true)) return null;
|
|
1188
|
+
}
|
|
1189
|
+
const overrideOptions = override.find(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)?.options;
|
|
1190
|
+
return {
|
|
1191
|
+
...options,
|
|
1192
|
+
...overrideOptions
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
return options;
|
|
1196
|
+
}
|
|
1197
|
+
function defaultResolveOptions(node, { options, exclude = [], include, override = [] }) {
|
|
1198
|
+
const optionsKey = options;
|
|
1199
|
+
let byOptions = resolveOptionsCache.get(optionsKey);
|
|
1200
|
+
if (!byOptions) {
|
|
1201
|
+
byOptions = /* @__PURE__ */ new WeakMap();
|
|
1202
|
+
resolveOptionsCache.set(optionsKey, byOptions);
|
|
1203
|
+
}
|
|
1204
|
+
const cached = byOptions.get(node);
|
|
1205
|
+
if (cached !== void 0) return cached.value;
|
|
1206
|
+
const result = computeOptions(node, options, exclude, include, override);
|
|
1207
|
+
byOptions.set(node, { value: result });
|
|
1208
|
+
return result;
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Default path resolver used by `defineResolver`.
|
|
1212
|
+
*
|
|
1213
|
+
* - `mode: 'file'` resolves directly to `output.path` (the full file path, extension included).
|
|
1214
|
+
* - `mode: 'directory'` (default) resolves to `output.path/{baseName}`, or into a
|
|
1215
|
+
* subdirectory when `group` and a `tag`/`path` value are provided.
|
|
1216
|
+
*
|
|
1217
|
+
* A custom `group.name` function overrides the default subdirectory naming.
|
|
1218
|
+
* For `tag` groups the default is the camelCased tag.
|
|
1219
|
+
* For `path` groups the default is the first path segment after `/`.
|
|
1220
|
+
*
|
|
1221
|
+
* @example Flat output
|
|
1222
|
+
* ```ts
|
|
1223
|
+
* defaultResolvePath({ baseName: 'petTypes.ts' }, { root: '/src', output: { path: 'types' } })
|
|
1224
|
+
* // → '/src/types/petTypes.ts'
|
|
1225
|
+
* ```
|
|
1226
|
+
*
|
|
1227
|
+
* @example Tag-based grouping
|
|
1228
|
+
* ```ts
|
|
1229
|
+
* defaultResolvePath(
|
|
1230
|
+
* { baseName: 'petTypes.ts', tag: 'pets' },
|
|
1231
|
+
* { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
|
|
1232
|
+
* )
|
|
1233
|
+
* // → '/src/types/pets/petTypes.ts'
|
|
1234
|
+
* ```
|
|
1235
|
+
*
|
|
1236
|
+
* @example Path-based grouping
|
|
1237
|
+
* ```ts
|
|
1238
|
+
* defaultResolvePath(
|
|
1239
|
+
* { baseName: 'petTypes.ts', path: '/pets/list' },
|
|
1240
|
+
* { root: '/src', output: { path: 'types' }, group: { type: 'path' } },
|
|
1241
|
+
* )
|
|
1242
|
+
* // → '/src/types/pets/petTypes.ts'
|
|
1243
|
+
* ```
|
|
1244
|
+
*
|
|
1245
|
+
* @example Single file (`mode: 'file'`)
|
|
1246
|
+
* ```ts
|
|
1247
|
+
* defaultResolvePath(
|
|
1248
|
+
* { baseName: 'petTypes.ts' },
|
|
1249
|
+
* { root: '/src', output: { path: 'types.ts', mode: 'file' } },
|
|
1250
|
+
* )
|
|
1251
|
+
* // → '/src/types.ts'
|
|
1252
|
+
* ```
|
|
1253
|
+
*/
|
|
1254
|
+
function defaultResolvePath({ baseName, tag, path: groupPath }, { root, output, group }) {
|
|
1255
|
+
if ((output.mode ?? "directory") === "file") return node_path.default.resolve(root, output.path);
|
|
1256
|
+
const result = (() => {
|
|
1257
|
+
if (group && (groupPath || tag)) {
|
|
1258
|
+
const groupValue = group.type === "path" ? groupPath : tag;
|
|
1259
|
+
const defaultName = group.type === "tag" ? ({ group: groupName }) => require_memoryStorage.camelCase(groupName) : ({ group: groupName }) => {
|
|
1260
|
+
const segment = groupName.split("/").filter((part) => part !== "" && part !== "." && part !== "..")[0];
|
|
1261
|
+
return segment ? require_memoryStorage.camelCase(segment) : "";
|
|
1262
|
+
};
|
|
1263
|
+
const groupName = (group.name ?? defaultName)({ group: groupValue });
|
|
1264
|
+
return node_path.default.resolve(root, output.path, groupName, baseName);
|
|
1265
|
+
}
|
|
1266
|
+
return node_path.default.resolve(root, output.path, baseName);
|
|
1267
|
+
})();
|
|
1268
|
+
const outputDir = node_path.default.resolve(root, output.path);
|
|
1269
|
+
const outputDirWithSep = outputDir.endsWith(node_path.default.sep) ? outputDir : `${outputDir}${node_path.default.sep}`;
|
|
1270
|
+
if (result !== outputDir && !result.startsWith(outputDirWithSep)) throw new Diagnostics.Error({
|
|
1271
|
+
code: Diagnostics.code.pathTraversal,
|
|
1272
|
+
severity: "error",
|
|
1273
|
+
message: `Resolved path "${result}" is outside the output directory "${outputDir}".`,
|
|
1274
|
+
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.",
|
|
1275
|
+
location: { kind: "config" }
|
|
1276
|
+
});
|
|
1277
|
+
return result;
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Default file resolver used by `defineResolver`.
|
|
1281
|
+
*
|
|
1282
|
+
* Resolves a `FileNode` by combining name resolution (`resolver.default`) with
|
|
1283
|
+
* path resolution (`resolver.resolvePath`). The resolved file always has empty
|
|
1284
|
+
* `sources`, `imports`, and `exports` arrays, which consumers populate separately.
|
|
1285
|
+
*
|
|
1286
|
+
* In `mode: 'file'` the name is omitted and the file sits directly at the output path.
|
|
1287
|
+
*
|
|
1288
|
+
* @example Resolve a schema file
|
|
1289
|
+
* ```ts
|
|
1290
|
+
* const file = defaultResolveFile.call(
|
|
1291
|
+
* resolver,
|
|
1292
|
+
* { name: 'pet', extname: '.ts' },
|
|
1293
|
+
* { root: '/src', output: { path: 'types' } },
|
|
1294
|
+
* )
|
|
1295
|
+
* // → { baseName: 'pet.ts', path: '/src/types/pet.ts', sources: [], ... }
|
|
1296
|
+
* ```
|
|
1297
|
+
*
|
|
1298
|
+
* @example Resolve an operation file with tag grouping
|
|
1299
|
+
* ```ts
|
|
1300
|
+
* const file = defaultResolveFile.call(
|
|
1301
|
+
* resolver,
|
|
1302
|
+
* { name: 'listPets', extname: '.ts', tag: 'pets' },
|
|
1303
|
+
* { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
|
|
1304
|
+
* )
|
|
1305
|
+
* // → { baseName: 'listPets.ts', path: '/src/types/pets/listPets.ts', ... }
|
|
1306
|
+
* ```
|
|
1307
|
+
*/
|
|
1308
|
+
function defaultResolveFile({ name, extname, tag, path: groupPath }, context) {
|
|
1309
|
+
const baseName = `${(context.output.mode ?? "directory") === "file" ? "" : this.default(name, "file")}${extname}`;
|
|
1310
|
+
const filePath = this.resolvePath({
|
|
1311
|
+
baseName,
|
|
1312
|
+
tag,
|
|
1313
|
+
path: groupPath
|
|
1314
|
+
}, context);
|
|
1315
|
+
return _kubb_ast_factory.createFile({
|
|
1316
|
+
path: filePath,
|
|
1317
|
+
baseName: node_path.default.basename(filePath),
|
|
1318
|
+
meta: { pluginName: this.pluginName },
|
|
1319
|
+
sources: [],
|
|
1320
|
+
imports: [],
|
|
1321
|
+
exports: []
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Generates the default "Generated by Kubb" banner from config and optional node metadata.
|
|
1326
|
+
*/
|
|
1327
|
+
function buildDefaultBanner({ title, description, version, config }) {
|
|
1328
|
+
try {
|
|
1329
|
+
const source = (() => {
|
|
1330
|
+
if (Array.isArray(config.input)) {
|
|
1331
|
+
const first = config.input[0];
|
|
1332
|
+
if (first && "path" in first) return node_path.default.basename(first.path);
|
|
1333
|
+
return "";
|
|
1334
|
+
}
|
|
1335
|
+
if (config.input && "path" in config.input) return node_path.default.basename(config.input.path);
|
|
1336
|
+
if (config.input && "data" in config.input) return "text content";
|
|
1337
|
+
return "";
|
|
1338
|
+
})();
|
|
1339
|
+
let banner = "/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n";
|
|
1340
|
+
if (config.output.defaultBanner === "simple") {
|
|
1341
|
+
banner += "*/\n";
|
|
1342
|
+
return banner;
|
|
1343
|
+
}
|
|
1344
|
+
if (source) banner += `* Source: ${source}\n`;
|
|
1345
|
+
if (title) banner += `* Title: ${title}\n`;
|
|
1346
|
+
if (description) {
|
|
1347
|
+
const formattedDescription = description.replace(/\n/gm, "\n* ");
|
|
1348
|
+
banner += `* Description: ${formattedDescription}\n`;
|
|
1349
|
+
}
|
|
1350
|
+
if (version) banner += `* OpenAPI spec version: ${version}\n`;
|
|
1351
|
+
banner += "*/\n";
|
|
1352
|
+
return banner;
|
|
1353
|
+
} catch (_error) {
|
|
1354
|
+
return "/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n*/";
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Default banner resolver. Returns the banner string for a generated file.
|
|
1359
|
+
*
|
|
1360
|
+
* A user-supplied `output.banner` overrides the default Kubb "Generated by Kubb" notice.
|
|
1361
|
+
* When no `output.banner` is set, the Kubb notice is used (including `title` and `version`
|
|
1362
|
+
* from the document metadata when `meta` is provided).
|
|
1363
|
+
*
|
|
1364
|
+
* - When `output.banner` is a function, calls it with the file's `BannerMeta` and returns the result.
|
|
1365
|
+
* - When `output.banner` is a string, returns it directly.
|
|
1366
|
+
* - When `config.output.defaultBanner` is `false`, returns `undefined`.
|
|
1367
|
+
* - Otherwise returns the Kubb "Generated by Kubb" notice.
|
|
1368
|
+
*
|
|
1369
|
+
* @example String banner overrides default
|
|
1370
|
+
* ```ts
|
|
1371
|
+
* defaultResolveBanner(undefined, { output: { banner: '// my banner' }, config })
|
|
1372
|
+
* // → '// my banner'
|
|
1373
|
+
* ```
|
|
1374
|
+
*
|
|
1375
|
+
* @example Function banner with metadata
|
|
1376
|
+
* ```ts
|
|
1377
|
+
* defaultResolveBanner(meta, { output: { banner: (m) => `// v${m.version}` }, config })
|
|
1378
|
+
* // → '// v3.0.0'
|
|
1379
|
+
* ```
|
|
1380
|
+
*
|
|
1381
|
+
* @example Function banner skips re-export files
|
|
1382
|
+
* ```ts
|
|
1383
|
+
* defaultResolveBanner(meta, { output: { banner: (m) => (m.isBarrel ? '' : "'use server'") }, config, file: { path, baseName, isBarrel: true } })
|
|
1384
|
+
* // → ''
|
|
1385
|
+
* ```
|
|
1386
|
+
*
|
|
1387
|
+
* @example No user banner, Kubb notice with OAS metadata
|
|
1388
|
+
* ```ts
|
|
1389
|
+
* defaultResolveBanner(meta, { config })
|
|
1390
|
+
* // → '/** Generated by Kubb ... Title: Pet Store ... *\/'
|
|
1391
|
+
* ```
|
|
1392
|
+
*
|
|
1393
|
+
* @example Disabled default banner
|
|
1394
|
+
* ```ts
|
|
1395
|
+
* defaultResolveBanner(undefined, { config: { output: { defaultBanner: false }, ...config } })
|
|
1396
|
+
* // → null
|
|
1397
|
+
* ```
|
|
1398
|
+
*/
|
|
1399
|
+
function defaultResolveBanner(meta, { output, config, file }) {
|
|
1400
|
+
if (typeof output?.banner === "function") return output.banner(buildBannerMeta({
|
|
1401
|
+
meta,
|
|
1402
|
+
file
|
|
1403
|
+
}));
|
|
1404
|
+
if (typeof output?.banner === "string") return output.banner;
|
|
1405
|
+
if (config.output.defaultBanner === false) return null;
|
|
1406
|
+
return buildDefaultBanner({
|
|
1407
|
+
title: meta?.title,
|
|
1408
|
+
version: meta?.version,
|
|
1409
|
+
config
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Default footer resolver. Returns the footer string for a generated file.
|
|
1414
|
+
*
|
|
1415
|
+
* - When `output.footer` is a function, calls it with the file's `BannerMeta` and returns the result.
|
|
1416
|
+
* - When `output.footer` is a string, returns it directly.
|
|
1417
|
+
* - Otherwise returns `undefined`.
|
|
1418
|
+
*
|
|
1419
|
+
* @example String footer
|
|
1420
|
+
* ```ts
|
|
1421
|
+
* defaultResolveFooter(undefined, { output: { footer: '// end of file' }, config })
|
|
1422
|
+
* // → '// end of file'
|
|
1423
|
+
* ```
|
|
1424
|
+
*
|
|
1425
|
+
* @example Function footer with metadata
|
|
1426
|
+
* ```ts
|
|
1427
|
+
* defaultResolveFooter(meta, { output: { footer: (m) => `// ${m.title}` }, config })
|
|
1428
|
+
* // → '// Pet Store'
|
|
1429
|
+
* ```
|
|
1430
|
+
*/
|
|
1431
|
+
function defaultResolveFooter(meta, { output, file }) {
|
|
1432
|
+
if (typeof output?.footer === "function") return output.footer(buildBannerMeta({
|
|
1433
|
+
meta,
|
|
1434
|
+
file
|
|
1435
|
+
}));
|
|
1436
|
+
if (typeof output?.footer === "string") return output.footer;
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Defines a plugin resolver. The resolver is the object that decides what
|
|
1441
|
+
* every generated symbol and file path is called. Built-in defaults handle
|
|
1442
|
+
* name casing, include/exclude/override filtering, output path computation,
|
|
1443
|
+
* and file construction. Supply your own to override any of them:
|
|
1444
|
+
*
|
|
1445
|
+
* - `default` sets the name casing strategy (camelCase or PascalCase).
|
|
1446
|
+
* - `resolveOptions` does include/exclude/override filtering.
|
|
1447
|
+
* - `resolvePath` computes the output path.
|
|
1448
|
+
* - `resolveFile` builds the full `FileNode`.
|
|
1449
|
+
* - `resolveBanner` and `resolveFooter` produce the top and bottom of file text.
|
|
1450
|
+
*
|
|
1451
|
+
* Methods in the returned object can call sibling resolver methods via `this`.
|
|
1452
|
+
* A custom rule can delegate to a default, for example `this.default(name, 'type')`.
|
|
1453
|
+
*
|
|
1454
|
+
* @example Basic resolver with naming helpers
|
|
1455
|
+
* ```ts
|
|
1456
|
+
* export const resolverTs = defineResolver<PluginTs>(() => ({
|
|
1457
|
+
* name: 'default',
|
|
1458
|
+
* resolveName(name) {
|
|
1459
|
+
* return this.default(name, 'function')
|
|
1460
|
+
* },
|
|
1461
|
+
* resolveTypeName(name) {
|
|
1462
|
+
* return this.default(name, 'type')
|
|
1463
|
+
* },
|
|
1464
|
+
* }))
|
|
1465
|
+
* ```
|
|
1466
|
+
*
|
|
1467
|
+
* @example Custom output path
|
|
1468
|
+
* ```ts
|
|
1469
|
+
* import path from 'node:path'
|
|
1470
|
+
*
|
|
1471
|
+
* export const resolverTs = defineResolver<PluginTs>(() => ({
|
|
1472
|
+
* name: 'custom',
|
|
1473
|
+
* resolvePath({ baseName }, { root, output }) {
|
|
1474
|
+
* return path.resolve(root, output.path, 'generated', baseName)
|
|
1475
|
+
* },
|
|
1476
|
+
* }))
|
|
1477
|
+
* ```
|
|
1478
|
+
*/
|
|
1479
|
+
function defineResolver(build) {
|
|
1480
|
+
let resolver;
|
|
1481
|
+
resolver = {
|
|
1482
|
+
default: defaultResolver,
|
|
1483
|
+
resolveOptions: defaultResolveOptions,
|
|
1484
|
+
resolvePath: defaultResolvePath,
|
|
1485
|
+
resolveFile: (params, context) => defaultResolveFile.call(resolver, params, context),
|
|
1486
|
+
resolveBanner: defaultResolveBanner,
|
|
1487
|
+
resolveFooter: defaultResolveFooter,
|
|
1488
|
+
...build()
|
|
1489
|
+
};
|
|
1490
|
+
return resolver;
|
|
1491
|
+
}
|
|
1492
|
+
//#endregion
|
|
1493
|
+
//#region src/Transform.ts
|
|
1494
|
+
/**
|
|
1495
|
+
* Holds an ordered list of macros per plugin, keyed by plugin name. Each plugin's macros run in
|
|
1496
|
+
* isolation on the original adapter node and are composed into a single `Visitor` that the
|
|
1497
|
+
* `@kubb/ast` `transform` primitive applies. `applyTo` is a per-plugin lookup, not a cross-plugin
|
|
1498
|
+
* chain, so plugin A's macros never see plugin B's output. When a plugin has no macros, `applyTo`
|
|
1499
|
+
* returns the original node reference, and `transform` does the same when the composed visitor
|
|
1500
|
+
* leaves the tree untouched, so callers can detect a no-op by identity.
|
|
1501
|
+
*
|
|
1502
|
+
* Registration order matches the order setup hooks fire, which the driver has already sorted by
|
|
1503
|
+
* `enforce` and dependency edges. The registry preserves that order; macro `enforce` only reorders
|
|
1504
|
+
* within a single plugin's list.
|
|
1505
|
+
*/
|
|
1506
|
+
var Transform = class {
|
|
1507
|
+
#macros = /* @__PURE__ */ new Map();
|
|
1508
|
+
#composed = /* @__PURE__ */ new Map();
|
|
1509
|
+
#memo = /* @__PURE__ */ new Map();
|
|
1510
|
+
/**
|
|
1511
|
+
* Number of plugins with at least one registered macro.
|
|
1512
|
+
*/
|
|
1513
|
+
get size() {
|
|
1514
|
+
return this.#macros.size;
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Appends `macro` to the plugin's list, after any macros already registered.
|
|
1518
|
+
*/
|
|
1519
|
+
add(pluginName, macro) {
|
|
1520
|
+
const list = this.#macros.get(pluginName);
|
|
1521
|
+
if (list) list.push(macro);
|
|
1522
|
+
else this.#macros.set(pluginName, [macro]);
|
|
1523
|
+
this.#invalidate(pluginName);
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Replaces the plugin's macro list with `macros`.
|
|
1527
|
+
*/
|
|
1528
|
+
set(pluginName, macros) {
|
|
1529
|
+
this.#macros.set(pluginName, [...macros]);
|
|
1530
|
+
this.#invalidate(pluginName);
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* Looks up the composed visitor for `pluginName`, or `undefined` when the plugin has no macros.
|
|
1534
|
+
*/
|
|
1535
|
+
get(pluginName) {
|
|
1536
|
+
return this.#visitorFor(pluginName);
|
|
1537
|
+
}
|
|
1538
|
+
/**
|
|
1539
|
+
* Runs the plugin's macros on `node`. Returns the original node reference when the plugin has no
|
|
1540
|
+
* macros, so callers can compare by identity to detect a no-op.
|
|
1541
|
+
*/
|
|
1542
|
+
applyTo(pluginName, node) {
|
|
1543
|
+
const visitor = this.#visitorFor(pluginName);
|
|
1544
|
+
if (!visitor) return node;
|
|
1545
|
+
let memo = this.#memo.get(pluginName);
|
|
1546
|
+
if (!memo) {
|
|
1547
|
+
memo = /* @__PURE__ */ new WeakMap();
|
|
1548
|
+
this.#memo.set(pluginName, memo);
|
|
1549
|
+
}
|
|
1550
|
+
const cached = memo.get(node);
|
|
1551
|
+
if (cached) return cached;
|
|
1552
|
+
const result = (0, _kubb_ast.transform)(node, visitor);
|
|
1553
|
+
memo.set(node, result);
|
|
1554
|
+
return result;
|
|
1555
|
+
}
|
|
1556
|
+
/**
|
|
1557
|
+
* Clears every registration. Called from the driver's `dispose()` so macros do not leak across
|
|
1558
|
+
* builds.
|
|
1559
|
+
*/
|
|
1560
|
+
dispose() {
|
|
1561
|
+
this.#macros.clear();
|
|
1562
|
+
this.#composed.clear();
|
|
1563
|
+
this.#memo.clear();
|
|
1564
|
+
}
|
|
1565
|
+
#invalidate(pluginName) {
|
|
1566
|
+
this.#composed.delete(pluginName);
|
|
1567
|
+
this.#memo.delete(pluginName);
|
|
1568
|
+
}
|
|
1569
|
+
#visitorFor(pluginName) {
|
|
1570
|
+
const macros = this.#macros.get(pluginName);
|
|
1571
|
+
if (!macros || macros.length === 0) return void 0;
|
|
1572
|
+
let composed = this.#composed.get(pluginName);
|
|
1573
|
+
if (!composed) {
|
|
1574
|
+
composed = (0, _kubb_ast.composeMacros)(macros);
|
|
1575
|
+
this.#composed.set(pluginName, composed);
|
|
1576
|
+
}
|
|
1577
|
+
return composed;
|
|
1578
|
+
}
|
|
1579
|
+
};
|
|
1580
|
+
//#endregion
|
|
1581
|
+
//#region src/KubbDriver.ts
|
|
1582
|
+
function enforceOrder(enforce) {
|
|
1583
|
+
return enforce === "pre" ? -1 : enforce === "post" ? 1 : 0;
|
|
1584
|
+
}
|
|
1585
|
+
var KubbDriver = class {
|
|
1586
|
+
config;
|
|
1587
|
+
options;
|
|
1588
|
+
/**
|
|
1589
|
+
* The streaming `InputNode<true>` produced by the adapter. Set after adapter setup.
|
|
1590
|
+
* Parse-only adapters are wrapped automatically.
|
|
1591
|
+
*/
|
|
1592
|
+
inputNode = null;
|
|
1593
|
+
adapter = null;
|
|
1594
|
+
/**
|
|
1595
|
+
* Raw adapter source so `adapter.parse()` / `adapter.stream()` can run lazily.
|
|
1596
|
+
* Intentionally outlives the build, cleared by `dispose()`.
|
|
1597
|
+
*/
|
|
1598
|
+
#adapterSource = null;
|
|
1599
|
+
/**
|
|
1600
|
+
* Central file store for all generated files.
|
|
1601
|
+
* Plugins should use `this.addFile()` / `this.upsertFile()` (via their context) to
|
|
1602
|
+
* add files. This property gives direct read/write access when needed.
|
|
1603
|
+
*/
|
|
1604
|
+
fileManager = new require_memoryStorage.FileManager();
|
|
1605
|
+
plugins = /* @__PURE__ */ new Map();
|
|
1606
|
+
/**
|
|
1607
|
+
* Tracks which plugins have generators registered via `addGenerator()` (event-based path).
|
|
1608
|
+
* Used by the build loop to decide whether to emit generator events for a given plugin.
|
|
1609
|
+
*/
|
|
1610
|
+
#eventGeneratorPlugins = /* @__PURE__ */ new Set();
|
|
1611
|
+
#resolvers = /* @__PURE__ */ new Map();
|
|
1612
|
+
#defaultResolvers = /* @__PURE__ */ new Map();
|
|
1613
|
+
/**
|
|
1614
|
+
* Tracks every listener the driver added (plugin, generator) so `dispose()` can remove them
|
|
1615
|
+
* in one pass. External `hooks.on(...)` listeners are not tracked.
|
|
1616
|
+
*/
|
|
1617
|
+
#listeners = [];
|
|
1618
|
+
/**
|
|
1619
|
+
* Transform registry. Plugins populate it during `kubb:plugin:setup` via `addMacro`/`setMacros`,
|
|
1620
|
+
* and `#runGenerators` reads it once per `(plugin, node)` pair through `applyTo`.
|
|
1621
|
+
*/
|
|
1622
|
+
#transforms = new Transform();
|
|
1623
|
+
constructor(config, options) {
|
|
1624
|
+
this.config = config;
|
|
1625
|
+
this.options = options;
|
|
1626
|
+
this.adapter = config.adapter ?? null;
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Attaches a listener to the shared emitter and tracks it so `dispose()` can remove it later.
|
|
1630
|
+
* Listeners attached directly via `hooks.on(...)` are not tracked and survive disposal.
|
|
1631
|
+
*/
|
|
1632
|
+
#trackListener(event, handler) {
|
|
1633
|
+
this.hooks.on(event, handler);
|
|
1634
|
+
this.#listeners.push([event, handler]);
|
|
1635
|
+
}
|
|
1636
|
+
async setup() {
|
|
1637
|
+
const normalized = this.config.plugins.map((rawPlugin) => this.#normalizePlugin(rawPlugin));
|
|
1638
|
+
const dependenciesByName = new Map(normalized.map((plugin) => [plugin.name, new Set(plugin.dependencies ?? [])]));
|
|
1639
|
+
normalized.sort((a, b) => {
|
|
1640
|
+
if (dependenciesByName.get(b.name)?.has(a.name)) return -1;
|
|
1641
|
+
if (dependenciesByName.get(a.name)?.has(b.name)) return 1;
|
|
1642
|
+
return enforceOrder(a.enforce) - enforceOrder(b.enforce);
|
|
1643
|
+
});
|
|
1644
|
+
for (const plugin of normalized) {
|
|
1645
|
+
if (plugin.apply) plugin.apply(this.config);
|
|
1646
|
+
this.#registerPlugin(plugin);
|
|
1647
|
+
this.plugins.set(plugin.name, plugin);
|
|
1648
|
+
}
|
|
1649
|
+
if (this.config.adapter) this.#adapterSource = inputToAdapterSource(this.config);
|
|
1650
|
+
}
|
|
1651
|
+
get hooks() {
|
|
1652
|
+
return this.options.hooks;
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Creates an `NormalizedPlugin` from a hook-style plugin and registers
|
|
1656
|
+
* its lifecycle handlers on the `AsyncEventEmitter`.
|
|
1657
|
+
*/
|
|
1658
|
+
#normalizePlugin(plugin) {
|
|
1659
|
+
const normalized = {
|
|
1660
|
+
name: plugin.name,
|
|
1661
|
+
dependencies: plugin.dependencies,
|
|
1662
|
+
enforce: plugin.enforce,
|
|
1663
|
+
hooks: plugin.hooks,
|
|
1664
|
+
options: plugin.options ?? {
|
|
1665
|
+
output: {
|
|
1666
|
+
path: ".",
|
|
1667
|
+
mode: "directory"
|
|
1668
|
+
},
|
|
1669
|
+
exclude: [],
|
|
1670
|
+
override: []
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
if ("apply" in plugin && typeof plugin.apply === "function") normalized.apply = plugin.apply;
|
|
1674
|
+
return normalized;
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Parses the adapter source into `this.inputNode`. Idempotent, so repeated calls from
|
|
1678
|
+
* `run` do not re-parse. Adapters with `stream()` are used directly.
|
|
1679
|
+
* Adapters with only `parse()` are wrapped via `factory.createInput({ stream: true })` so the dispatch loop
|
|
1680
|
+
* stays stream-only.
|
|
1681
|
+
*/
|
|
1682
|
+
async #parseInput() {
|
|
1683
|
+
if (this.inputNode || !this.adapter || !this.#adapterSource) return;
|
|
1684
|
+
const adapter = this.adapter;
|
|
1685
|
+
const source = this.#adapterSource;
|
|
1686
|
+
if (adapter.stream) {
|
|
1687
|
+
this.inputNode = await adapter.stream(source);
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
const parsed = await adapter.parse(source);
|
|
1691
|
+
this.inputNode = _kubb_ast_factory.createInput({
|
|
1692
|
+
stream: true,
|
|
1693
|
+
schemas: arrayToAsyncIterable(parsed.schemas),
|
|
1694
|
+
operations: arrayToAsyncIterable(parsed.operations),
|
|
1695
|
+
meta: parsed.meta
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
/**
|
|
1699
|
+
* Registers a hook-style plugin's lifecycle handlers on the shared `AsyncEventEmitter`.
|
|
1700
|
+
*
|
|
1701
|
+
* The `kubb:plugin:setup` listener wraps the global context in a plugin-specific one so
|
|
1702
|
+
* `addGenerator`, `setResolver`, and `setMacros` target the right `normalizedPlugin`.
|
|
1703
|
+
* Every other `KubbHooks` event registers as a pass-through listener that external tooling
|
|
1704
|
+
* can observe via `hooks.on(...)`.
|
|
1705
|
+
*
|
|
1706
|
+
* @internal
|
|
1707
|
+
*/
|
|
1708
|
+
#registerPlugin(plugin) {
|
|
1709
|
+
const { hooks } = plugin;
|
|
1710
|
+
if (!hooks) return;
|
|
1711
|
+
if (hooks["kubb:plugin:setup"]) {
|
|
1712
|
+
const setupHandler = (globalCtx) => {
|
|
1713
|
+
const pluginCtx = {
|
|
1714
|
+
...globalCtx,
|
|
1715
|
+
options: plugin.options ?? {},
|
|
1716
|
+
addGenerator: (gen) => {
|
|
1717
|
+
this.registerGenerator(plugin.name, gen);
|
|
1070
1718
|
},
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
});
|
|
1077
|
-
} catch (caughtError) {
|
|
1078
|
-
const error = caughtError;
|
|
1079
|
-
const errorTimestamp = /* @__PURE__ */ new Date();
|
|
1080
|
-
const duration = getElapsedMs(hrStart);
|
|
1081
|
-
await hooks.emit("kubb:plugin:end", {
|
|
1082
|
-
plugin,
|
|
1083
|
-
duration,
|
|
1084
|
-
success: false,
|
|
1085
|
-
error,
|
|
1086
|
-
config,
|
|
1087
|
-
get files() {
|
|
1088
|
-
return driver.fileManager.files;
|
|
1719
|
+
setResolver: (resolver) => {
|
|
1720
|
+
this.setPluginResolver(plugin.name, resolver);
|
|
1721
|
+
},
|
|
1722
|
+
addMacro: (macro) => {
|
|
1723
|
+
this.#transforms.add(plugin.name, macro);
|
|
1089
1724
|
},
|
|
1090
|
-
|
|
1725
|
+
setMacros: (macros) => {
|
|
1726
|
+
this.#transforms.set(plugin.name, macros);
|
|
1727
|
+
},
|
|
1728
|
+
setOptions: (opts) => {
|
|
1729
|
+
plugin.options = {
|
|
1730
|
+
...plugin.options,
|
|
1731
|
+
...opts
|
|
1732
|
+
};
|
|
1733
|
+
if (plugin.options.output) {
|
|
1734
|
+
const group = "group" in plugin.options ? plugin.options.group : void 0;
|
|
1735
|
+
plugin.options.output = normalizeOutput({
|
|
1736
|
+
output: plugin.options.output,
|
|
1737
|
+
group,
|
|
1738
|
+
pluginName: plugin.name
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
},
|
|
1742
|
+
injectFile: (userFileNode) => {
|
|
1743
|
+
this.fileManager.add(_kubb_ast_factory.createFile(userFileNode));
|
|
1744
|
+
}
|
|
1745
|
+
};
|
|
1746
|
+
return hooks["kubb:plugin:setup"](pluginCtx);
|
|
1747
|
+
};
|
|
1748
|
+
this.#trackListener("kubb:plugin:setup", setupHandler);
|
|
1749
|
+
}
|
|
1750
|
+
for (const event of Object.keys(hooks)) {
|
|
1751
|
+
if (event === "kubb:plugin:setup") continue;
|
|
1752
|
+
const handler = hooks[event];
|
|
1753
|
+
if (!handler) continue;
|
|
1754
|
+
this.#trackListener(event, handler);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Emits the `kubb:plugin:setup` event so that all registered hook-style plugin listeners
|
|
1759
|
+
* can configure generators, resolvers, macros and renderers before `buildStart` runs.
|
|
1760
|
+
*
|
|
1761
|
+
* Call this once from `safeBuild` before the plugin execution loop begins.
|
|
1762
|
+
*/
|
|
1763
|
+
async emitSetupHooks() {
|
|
1764
|
+
const noop = () => {};
|
|
1765
|
+
await this.hooks.emit("kubb:plugin:setup", {
|
|
1766
|
+
config: this.config,
|
|
1767
|
+
options: {},
|
|
1768
|
+
addGenerator: noop,
|
|
1769
|
+
setResolver: noop,
|
|
1770
|
+
addMacro: noop,
|
|
1771
|
+
setMacros: noop,
|
|
1772
|
+
setOptions: noop,
|
|
1773
|
+
injectFile: noop,
|
|
1774
|
+
updateConfig: noop
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* Registers a generator for the given plugin on the shared event emitter.
|
|
1779
|
+
*
|
|
1780
|
+
* The generator's `schema`, `operation`, and `operations` methods are registered as
|
|
1781
|
+
* listeners on `kubb:generate:schema`, `kubb:generate:operation`, and `kubb:generate:operations`
|
|
1782
|
+
* respectively. Each listener is scoped to the owning plugin via a `ctx.plugin.name` check
|
|
1783
|
+
* so that generators from different plugins do not cross-fire.
|
|
1784
|
+
*
|
|
1785
|
+
* The renderer comes from `generator.renderer`. Set `generator.renderer = null` (or leave it
|
|
1786
|
+
* unset) to opt out of rendering.
|
|
1787
|
+
*
|
|
1788
|
+
* Call this method inside `addGenerator()` (in `kubb:plugin:setup`) to wire up a generator.
|
|
1789
|
+
*/
|
|
1790
|
+
registerGenerator(pluginName, generator) {
|
|
1791
|
+
if (generator.schema) {
|
|
1792
|
+
const schemaHandler = async (node, ctx) => {
|
|
1793
|
+
if (ctx.plugin.name !== pluginName) return;
|
|
1794
|
+
const result = await generator.schema(node, ctx);
|
|
1795
|
+
await this.dispatch({
|
|
1796
|
+
result,
|
|
1797
|
+
renderer: generator.renderer
|
|
1091
1798
|
});
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1799
|
+
};
|
|
1800
|
+
this.#trackListener("kubb:generate:schema", schemaHandler);
|
|
1801
|
+
}
|
|
1802
|
+
if (generator.operation) {
|
|
1803
|
+
const operationHandler = async (node, ctx) => {
|
|
1804
|
+
if (ctx.plugin.name !== pluginName) return;
|
|
1805
|
+
const result = await generator.operation(node, ctx);
|
|
1806
|
+
await this.dispatch({
|
|
1807
|
+
result,
|
|
1808
|
+
renderer: generator.renderer
|
|
1101
1809
|
});
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1810
|
+
};
|
|
1811
|
+
this.#trackListener("kubb:generate:operation", operationHandler);
|
|
1812
|
+
}
|
|
1813
|
+
if (generator.operations) {
|
|
1814
|
+
const operationsHandler = async (nodes, ctx) => {
|
|
1815
|
+
if (ctx.plugin.name !== pluginName) return;
|
|
1816
|
+
const result = await generator.operations(nodes, ctx);
|
|
1817
|
+
await this.dispatch({
|
|
1818
|
+
result,
|
|
1819
|
+
renderer: generator.renderer
|
|
1105
1820
|
});
|
|
1106
|
-
}
|
|
1821
|
+
};
|
|
1822
|
+
this.#trackListener("kubb:generate:operations", operationsHandler);
|
|
1107
1823
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1824
|
+
this.#eventGeneratorPlugins.add(pluginName);
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Returns `true` when at least one generator was registered for the given plugin
|
|
1828
|
+
* via `addGenerator()` in `kubb:plugin:setup` (event-based path).
|
|
1829
|
+
*
|
|
1830
|
+
* Used by the build loop to decide whether to walk the AST and emit generator events
|
|
1831
|
+
* for a plugin that has no static `plugin.generators`.
|
|
1832
|
+
*/
|
|
1833
|
+
hasEventGenerators(pluginName) {
|
|
1834
|
+
return this.#eventGeneratorPlugins.has(pluginName);
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Runs the full plugin pipeline. Returns the diagnostics collected so far even
|
|
1838
|
+
* when an outer hook throws, since the orchestrator preserves partial state by capturing
|
|
1839
|
+
* the failure as a {@link Diagnostic} instead of propagating. Each plugin also
|
|
1840
|
+
* contributes a `timing` diagnostic for the run summary.
|
|
1841
|
+
*/
|
|
1842
|
+
async run({ storage }) {
|
|
1843
|
+
const { hooks, config } = this;
|
|
1844
|
+
const diagnostics = [];
|
|
1116
1845
|
const parsersMap = /* @__PURE__ */ new Map();
|
|
1117
|
-
for (const parser of config.parsers) if (parser.extNames) for (const
|
|
1118
|
-
const
|
|
1119
|
-
await hooks.emit("kubb:debug", {
|
|
1120
|
-
date: /* @__PURE__ */ new Date(),
|
|
1121
|
-
logs: [`Writing ${files.length} files...`]
|
|
1122
|
-
});
|
|
1123
|
-
await fileProcessor.run(files, {
|
|
1846
|
+
for (const parser of config.parsers) if (parser.extNames) for (const ext of parser.extNames) parsersMap.set(ext, parser);
|
|
1847
|
+
const processor = new require_memoryStorage.FileProcessor({
|
|
1124
1848
|
parsers: parsersMap,
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1849
|
+
storage,
|
|
1850
|
+
extension: config.output.extension
|
|
1851
|
+
});
|
|
1852
|
+
processor.hooks.on("start", async (files) => {
|
|
1853
|
+
await hooks.emit("kubb:files:processing:start", { files });
|
|
1854
|
+
});
|
|
1855
|
+
const updateBuffer = [];
|
|
1856
|
+
processor.hooks.on("update", (item) => {
|
|
1857
|
+
updateBuffer.push(item);
|
|
1858
|
+
});
|
|
1859
|
+
processor.hooks.on("end", async (files) => {
|
|
1860
|
+
await hooks.emit("kubb:files:processing:update", { files: updateBuffer.map((item) => ({
|
|
1861
|
+
...item,
|
|
1862
|
+
config
|
|
1863
|
+
})) });
|
|
1864
|
+
updateBuffer.length = 0;
|
|
1865
|
+
await hooks.emit("kubb:files:processing:end", { files });
|
|
1866
|
+
});
|
|
1867
|
+
const onFileUpsert = (file) => {
|
|
1868
|
+
processor.enqueue(file);
|
|
1869
|
+
};
|
|
1870
|
+
this.fileManager.hooks.on("upsert", onFileUpsert);
|
|
1871
|
+
return Diagnostics.scope((diagnostic) => diagnostics.push(diagnostic), async () => {
|
|
1872
|
+
try {
|
|
1873
|
+
const outputRoot = (0, node_path.resolve)(config.root, config.output.path);
|
|
1874
|
+
await this.#parseInput();
|
|
1875
|
+
await this.emitSetupHooks();
|
|
1876
|
+
if (this.adapter && this.inputNode) await hooks.emit("kubb:build:start", Object.assign({
|
|
1877
|
+
config,
|
|
1878
|
+
adapter: this.adapter,
|
|
1879
|
+
meta: this.inputNode.meta,
|
|
1880
|
+
getPlugin: this.getPlugin.bind(this)
|
|
1881
|
+
}, this.#filesPayload()));
|
|
1882
|
+
const generatorPlugins = [];
|
|
1883
|
+
for (const plugin of this.plugins.values()) {
|
|
1884
|
+
const context = this.getContext(plugin);
|
|
1885
|
+
const hrStart = process.hrtime();
|
|
1886
|
+
try {
|
|
1887
|
+
await hooks.emit("kubb:plugin:start", { plugin });
|
|
1888
|
+
} catch (caughtError) {
|
|
1889
|
+
const error = caughtError;
|
|
1890
|
+
const duration = getElapsedMs(hrStart);
|
|
1891
|
+
await this.#emitPluginEnd({
|
|
1892
|
+
plugin,
|
|
1893
|
+
duration,
|
|
1894
|
+
success: false,
|
|
1895
|
+
error
|
|
1896
|
+
});
|
|
1897
|
+
diagnostics.push({
|
|
1898
|
+
...Diagnostics.from(error),
|
|
1899
|
+
plugin: plugin.name
|
|
1900
|
+
}, Diagnostics.performance({
|
|
1901
|
+
plugin: plugin.name,
|
|
1902
|
+
duration
|
|
1903
|
+
}));
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
if (this.hasEventGenerators(plugin.name)) {
|
|
1907
|
+
generatorPlugins.push({
|
|
1908
|
+
plugin,
|
|
1909
|
+
context,
|
|
1910
|
+
hrStart
|
|
1911
|
+
});
|
|
1912
|
+
continue;
|
|
1913
|
+
}
|
|
1914
|
+
const duration = getElapsedMs(hrStart);
|
|
1915
|
+
diagnostics.push(Diagnostics.performance({
|
|
1916
|
+
plugin: plugin.name,
|
|
1917
|
+
duration
|
|
1918
|
+
}));
|
|
1919
|
+
await this.#emitPluginEnd({
|
|
1920
|
+
plugin,
|
|
1921
|
+
duration,
|
|
1922
|
+
success: true
|
|
1923
|
+
});
|
|
1141
1924
|
}
|
|
1925
|
+
diagnostics.push(...await this.#runGenerators(generatorPlugins, () => processor.flush()));
|
|
1926
|
+
await processor.drain();
|
|
1927
|
+
await hooks.emit("kubb:plugins:end", Object.assign({ config }, this.#filesPayload()));
|
|
1928
|
+
await processor.drain();
|
|
1929
|
+
await hooks.emit("kubb:build:end", {
|
|
1930
|
+
files: this.fileManager.files,
|
|
1931
|
+
config,
|
|
1932
|
+
outputDir: outputRoot
|
|
1933
|
+
});
|
|
1934
|
+
return { diagnostics: Diagnostics.dedupe(diagnostics) };
|
|
1935
|
+
} catch (caughtError) {
|
|
1936
|
+
diagnostics.push(Diagnostics.from(caughtError));
|
|
1937
|
+
return { diagnostics: Diagnostics.dedupe(diagnostics) };
|
|
1938
|
+
} finally {
|
|
1939
|
+
this.fileManager.hooks.off("upsert", onFileUpsert);
|
|
1940
|
+
}
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
#filesPayload() {
|
|
1944
|
+
const driver = this;
|
|
1945
|
+
return {
|
|
1946
|
+
get files() {
|
|
1947
|
+
return driver.fileManager.files;
|
|
1142
1948
|
},
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1949
|
+
upsertFile: (...files) => driver.fileManager.upsert(...files)
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
#emitPluginEnd({ plugin, duration, success, error }) {
|
|
1953
|
+
return this.hooks.emit("kubb:plugin:end", Object.assign({
|
|
1954
|
+
plugin,
|
|
1955
|
+
duration,
|
|
1956
|
+
success,
|
|
1957
|
+
...error ? { error } : {},
|
|
1958
|
+
config: this.config
|
|
1959
|
+
}, this.#filesPayload()));
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Streams schemas and operations through every plugin's generators. Each node is run
|
|
1963
|
+
* through the plugin's macros (from `this.#transforms`) before the generator sees it,
|
|
1964
|
+
* so plugins stay isolated and the hot path stays per-node. Schemas run before operations
|
|
1965
|
+
* because the two passes share `flushPending` and the FileProcessor's event emitter.
|
|
1966
|
+
* A failing plugin contributes an error diagnostic so the rest of the build continues.
|
|
1967
|
+
* Every plugin also contributes a `timing` diagnostic.
|
|
1968
|
+
*
|
|
1969
|
+
* Plugins run sequentially so `kubb:plugin:end` fires as each plugin completes, instead
|
|
1970
|
+
* of all at once after every plugin has marched through the parallel batches together.
|
|
1971
|
+
* That ordering is what drives the CLI's `Plugins N/M` counter. Without it the bar would
|
|
1972
|
+
* sit at the initial value until the very end of the run.
|
|
1973
|
+
*
|
|
1974
|
+
* When `entries` is empty or `this.inputNode` is `null`, every entry still gets a
|
|
1975
|
+
* `kubb:plugin:end` so post-plugin listeners (the barrel writer and friends) complete.
|
|
1976
|
+
*/
|
|
1977
|
+
async #runGenerators(entries, flushPending) {
|
|
1978
|
+
const diagnostics = [];
|
|
1979
|
+
if (entries.length === 0) return diagnostics;
|
|
1980
|
+
if (!this.inputNode) {
|
|
1981
|
+
for (const { plugin, hrStart } of entries) {
|
|
1982
|
+
const duration = getElapsedMs(hrStart);
|
|
1983
|
+
diagnostics.push(Diagnostics.performance({
|
|
1984
|
+
plugin: plugin.name,
|
|
1985
|
+
duration
|
|
1986
|
+
}));
|
|
1987
|
+
await this.#emitPluginEnd({
|
|
1988
|
+
plugin,
|
|
1989
|
+
duration,
|
|
1990
|
+
success: true
|
|
1148
1991
|
});
|
|
1149
1992
|
}
|
|
1993
|
+
return diagnostics;
|
|
1994
|
+
}
|
|
1995
|
+
const transforms = this.#transforms;
|
|
1996
|
+
const { schemas, operations } = this.inputNode;
|
|
1997
|
+
const states = entries.map(({ plugin, context, hrStart }) => {
|
|
1998
|
+
const { exclude, include, override } = plugin.options;
|
|
1999
|
+
const hasExclude = Array.isArray(exclude) && exclude.length > 0;
|
|
2000
|
+
const hasInclude = Array.isArray(include) && include.length > 0;
|
|
2001
|
+
const hasOverride = Array.isArray(override) && override.length > 0;
|
|
2002
|
+
return {
|
|
2003
|
+
plugin,
|
|
2004
|
+
generatorContext: {
|
|
2005
|
+
...context,
|
|
2006
|
+
resolver: this.getResolver(plugin.name)
|
|
2007
|
+
},
|
|
2008
|
+
generators: plugin.generators ?? [],
|
|
2009
|
+
hrStart,
|
|
2010
|
+
failed: false,
|
|
2011
|
+
error: null,
|
|
2012
|
+
optionsAreStatic: !hasExclude && !hasInclude && !hasOverride,
|
|
2013
|
+
allowedSchemaNames: null
|
|
2014
|
+
};
|
|
1150
2015
|
});
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
2016
|
+
const emitsSchemaHook = this.hooks.listenerCount("kubb:generate:schema") > 0;
|
|
2017
|
+
const emitsOperationHook = this.hooks.listenerCount("kubb:generate:operation") > 0;
|
|
2018
|
+
const emitsOperationsHook = this.hooks.listenerCount("kubb:generate:operations") > 0;
|
|
2019
|
+
const schemasBuffer = await Array.fromAsync(schemas);
|
|
2020
|
+
const operationsBuffer = await Array.fromAsync(operations);
|
|
2021
|
+
const pruningStates = states.filter(({ plugin }) => {
|
|
2022
|
+
const { include } = plugin.options;
|
|
2023
|
+
return (include?.some(({ type }) => require_memoryStorage.OPERATION_FILTER_TYPES.has(type)) ?? false) && !(include?.some(({ type }) => type === "schemaName") ?? false);
|
|
1155
2024
|
});
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
2025
|
+
if (pruningStates.length > 0) {
|
|
2026
|
+
const includedOpsByState = new Map(pruningStates.map((state) => [state, []]));
|
|
2027
|
+
for (const operation of operationsBuffer) for (const state of pruningStates) {
|
|
2028
|
+
const { exclude, include, override } = state.plugin.options;
|
|
2029
|
+
if (state.generatorContext.resolver.resolveOptions(operation, {
|
|
2030
|
+
options: state.plugin.options,
|
|
2031
|
+
exclude,
|
|
2032
|
+
include,
|
|
2033
|
+
override
|
|
2034
|
+
}) !== null) includedOpsByState.get(state)?.push(operation);
|
|
2035
|
+
}
|
|
2036
|
+
for (const state of pruningStates) {
|
|
2037
|
+
state.allowedSchemaNames = (0, _kubb_ast_utils.collectUsedSchemaNames)(includedOpsByState.get(state) ?? [], schemasBuffer);
|
|
2038
|
+
includedOpsByState.delete(state);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
const resolveForPlugin = (state, node) => {
|
|
2042
|
+
const { plugin, generatorContext } = state;
|
|
2043
|
+
const transformedNode = transforms.applyTo(plugin.name, node);
|
|
2044
|
+
if (state.optionsAreStatic) return {
|
|
2045
|
+
transformedNode,
|
|
2046
|
+
options: plugin.options
|
|
2047
|
+
};
|
|
2048
|
+
const { exclude, include, override } = plugin.options;
|
|
2049
|
+
const options = generatorContext.resolver.resolveOptions(transformedNode, {
|
|
2050
|
+
options: plugin.options,
|
|
2051
|
+
exclude,
|
|
2052
|
+
include,
|
|
2053
|
+
override
|
|
2054
|
+
});
|
|
2055
|
+
if (options === null) return null;
|
|
2056
|
+
return {
|
|
2057
|
+
transformedNode,
|
|
2058
|
+
options
|
|
2059
|
+
};
|
|
2060
|
+
};
|
|
2061
|
+
const dispatchNode = async (state, node, dispatch) => {
|
|
2062
|
+
if (state.failed) return;
|
|
2063
|
+
try {
|
|
2064
|
+
const resolved = resolveForPlugin(state, node);
|
|
2065
|
+
if (!resolved) return;
|
|
2066
|
+
const { transformedNode, options } = resolved;
|
|
2067
|
+
if (dispatch.checkAllowedNames && state.allowedSchemaNames !== null && "name" in transformedNode && transformedNode.name && !state.allowedSchemaNames.has(transformedNode.name)) return;
|
|
2068
|
+
const ctx = {
|
|
2069
|
+
...state.generatorContext,
|
|
2070
|
+
options
|
|
2071
|
+
};
|
|
2072
|
+
for (const gen of state.generators) {
|
|
2073
|
+
const run = gen[dispatch.method];
|
|
2074
|
+
if (!run) continue;
|
|
2075
|
+
const raw = run(transformedNode, ctx);
|
|
2076
|
+
const result = isPromise(raw) ? await raw : raw;
|
|
2077
|
+
const applied = this.dispatch({
|
|
2078
|
+
result,
|
|
2079
|
+
renderer: gen.renderer
|
|
2080
|
+
});
|
|
2081
|
+
if (isPromise(applied)) await applied;
|
|
2082
|
+
}
|
|
2083
|
+
if (dispatch.emit) await dispatch.emit(transformedNode, ctx);
|
|
2084
|
+
} catch (caughtError) {
|
|
2085
|
+
state.failed = true;
|
|
2086
|
+
state.error = caughtError;
|
|
2087
|
+
}
|
|
2088
|
+
};
|
|
2089
|
+
const schemaDispatch = {
|
|
2090
|
+
method: "schema",
|
|
2091
|
+
checkAllowedNames: true,
|
|
2092
|
+
emit: emitsSchemaHook ? (node, ctx) => this.hooks.emit("kubb:generate:schema", node, ctx) : null
|
|
2093
|
+
};
|
|
2094
|
+
const operationDispatch = {
|
|
2095
|
+
method: "operation",
|
|
2096
|
+
checkAllowedNames: false,
|
|
2097
|
+
emit: emitsOperationHook ? (node, ctx) => this.hooks.emit("kubb:generate:operation", node, ctx) : null
|
|
2098
|
+
};
|
|
2099
|
+
for (const state of states) {
|
|
2100
|
+
const needsCollectedOperations = emitsOperationsHook || state.generators.some((gen) => !!gen.operations);
|
|
2101
|
+
const collectedOperations = needsCollectedOperations ? [] : void 0;
|
|
2102
|
+
await forBatches(schemasBuffer, (nodes) => Promise.all(nodes.map((node) => dispatchNode(state, node, schemaDispatch))), {
|
|
2103
|
+
concurrency: 8,
|
|
2104
|
+
flush: flushPending
|
|
2105
|
+
});
|
|
2106
|
+
await forBatches(operationsBuffer, (nodes) => {
|
|
2107
|
+
if (needsCollectedOperations) collectedOperations?.push(...nodes);
|
|
2108
|
+
return Promise.all(nodes.map((node) => dispatchNode(state, node, operationDispatch)));
|
|
2109
|
+
}, {
|
|
2110
|
+
concurrency: 8,
|
|
2111
|
+
flush: flushPending
|
|
2112
|
+
});
|
|
2113
|
+
if (!state.failed && needsCollectedOperations) try {
|
|
2114
|
+
const { plugin, generatorContext, generators } = state;
|
|
2115
|
+
const ctx = {
|
|
2116
|
+
...generatorContext,
|
|
2117
|
+
options: plugin.options
|
|
2118
|
+
};
|
|
2119
|
+
const pluginOperations = (collectedOperations ?? []).reduce((acc, node) => {
|
|
2120
|
+
const resolved = resolveForPlugin(state, node);
|
|
2121
|
+
if (resolved) acc.push(resolved.transformedNode);
|
|
2122
|
+
return acc;
|
|
2123
|
+
}, []);
|
|
2124
|
+
for (const gen of generators) {
|
|
2125
|
+
if (!gen.operations) continue;
|
|
2126
|
+
const result = await gen.operations(pluginOperations, ctx);
|
|
2127
|
+
await this.dispatch({
|
|
2128
|
+
result,
|
|
2129
|
+
renderer: gen.renderer
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
await this.hooks.emit("kubb:generate:operations", pluginOperations, ctx);
|
|
2133
|
+
} catch (caughtError) {
|
|
2134
|
+
state.failed = true;
|
|
2135
|
+
state.error = caughtError;
|
|
2136
|
+
}
|
|
2137
|
+
const duration = getElapsedMs(state.hrStart);
|
|
2138
|
+
await this.#emitPluginEnd({
|
|
2139
|
+
plugin: state.plugin,
|
|
2140
|
+
duration,
|
|
2141
|
+
success: !state.failed,
|
|
2142
|
+
error: state.failed && state.error ? state.error : void 0
|
|
2143
|
+
});
|
|
2144
|
+
if (state.failed && state.error) diagnostics.push({
|
|
2145
|
+
...Diagnostics.from(state.error),
|
|
2146
|
+
plugin: state.plugin.name
|
|
2147
|
+
});
|
|
2148
|
+
diagnostics.push(Diagnostics.performance({
|
|
2149
|
+
plugin: state.plugin.name,
|
|
2150
|
+
duration
|
|
2151
|
+
}));
|
|
2152
|
+
}
|
|
2153
|
+
return diagnostics;
|
|
2154
|
+
}
|
|
2155
|
+
/**
|
|
2156
|
+
* Stores whatever a generator method or `kubb:generate:*` hook returned.
|
|
2157
|
+
*
|
|
2158
|
+
* - An `Array<FileNode>` goes straight into `fileManager` via `upsert`.
|
|
2159
|
+
* - A renderer element runs through `renderer` (the renderer factory, e.g. JSX) and the
|
|
2160
|
+
* produced files go to `fileManager.upsert`.
|
|
2161
|
+
* - A falsy result is treated as a no-op. The generator wrote files itself via
|
|
2162
|
+
* `ctx.upsertFile`.
|
|
2163
|
+
*
|
|
2164
|
+
* Pass `renderer` when the result may be a renderer element. Generators that only return
|
|
2165
|
+
* `Array<FileNode>` do not need one.
|
|
2166
|
+
*/
|
|
2167
|
+
async dispatch({ result, renderer }) {
|
|
2168
|
+
try {
|
|
2169
|
+
var _usingCtx$2 = require_memoryStorage._usingCtx();
|
|
2170
|
+
if (!result) return;
|
|
2171
|
+
if (Array.isArray(result)) {
|
|
2172
|
+
this.fileManager.upsert(...result);
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
if (!renderer) return;
|
|
2176
|
+
const instance = _usingCtx$2.u(renderer());
|
|
2177
|
+
if (instance.stream) {
|
|
2178
|
+
for (const file of instance.stream(result)) this.fileManager.upsert(file);
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
await instance.render(result);
|
|
2182
|
+
this.fileManager.upsert(...instance.files);
|
|
2183
|
+
} catch (_) {
|
|
2184
|
+
_usingCtx$2.e = _;
|
|
2185
|
+
} finally {
|
|
2186
|
+
_usingCtx$2.d();
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
/**
|
|
2190
|
+
* Removes every listener the driver added. Listeners attached directly to `hooks` from outside
|
|
2191
|
+
* the driver survive. Called at the end of a build to prevent leaks across repeated builds.
|
|
2192
|
+
*
|
|
2193
|
+
* @internal
|
|
2194
|
+
*/
|
|
2195
|
+
dispose() {
|
|
2196
|
+
for (const [event, handler] of this.#listeners) this.hooks.off(event, handler);
|
|
2197
|
+
this.#listeners.length = 0;
|
|
2198
|
+
this.#eventGeneratorPlugins.clear();
|
|
2199
|
+
this.#transforms.dispose();
|
|
2200
|
+
this.#resolvers.clear();
|
|
2201
|
+
this.#defaultResolvers.clear();
|
|
2202
|
+
this.fileManager.dispose();
|
|
2203
|
+
this.inputNode = null;
|
|
2204
|
+
this.#adapterSource = null;
|
|
2205
|
+
}
|
|
2206
|
+
[Symbol.dispose]() {
|
|
2207
|
+
this.dispose();
|
|
2208
|
+
}
|
|
2209
|
+
#getDefaultResolver = memoize(this.#defaultResolvers, (pluginName) => defineResolver(() => ({
|
|
2210
|
+
name: "default",
|
|
2211
|
+
pluginName
|
|
2212
|
+
})));
|
|
2213
|
+
/**
|
|
2214
|
+
* Merges `partial` with the plugin's default resolver and stores the result.
|
|
2215
|
+
* Also mirrors it onto `plugin.resolver` so callers using `getPlugin(name).resolver`
|
|
2216
|
+
* get the up-to-date resolver without going through `getResolver()`.
|
|
2217
|
+
*/
|
|
2218
|
+
setPluginResolver(pluginName, partial) {
|
|
2219
|
+
const merged = {
|
|
2220
|
+
...this.#getDefaultResolver(pluginName),
|
|
2221
|
+
...partial
|
|
2222
|
+
};
|
|
2223
|
+
this.#resolvers.set(pluginName, merged);
|
|
2224
|
+
const plugin = this.plugins.get(pluginName);
|
|
2225
|
+
if (plugin) plugin.resolver = merged;
|
|
2226
|
+
}
|
|
2227
|
+
getResolver(pluginName) {
|
|
2228
|
+
return this.#resolvers.get(pluginName) ?? this.plugins.get(pluginName)?.resolver ?? this.#getDefaultResolver(pluginName);
|
|
2229
|
+
}
|
|
2230
|
+
getContext(plugin) {
|
|
2231
|
+
const driver = this;
|
|
2232
|
+
const report = (diagnostic) => {
|
|
2233
|
+
Diagnostics.report({
|
|
2234
|
+
...diagnostic,
|
|
2235
|
+
plugin: plugin.name
|
|
2236
|
+
});
|
|
1162
2237
|
};
|
|
1163
|
-
} catch (error) {
|
|
1164
2238
|
return {
|
|
1165
|
-
|
|
1166
|
-
|
|
2239
|
+
config: driver.config,
|
|
2240
|
+
get root() {
|
|
2241
|
+
return (0, node_path.resolve)(driver.config.root, driver.config.output.path);
|
|
2242
|
+
},
|
|
2243
|
+
hooks: driver.hooks,
|
|
2244
|
+
plugin,
|
|
2245
|
+
getPlugin: driver.getPlugin.bind(driver),
|
|
2246
|
+
requirePlugin: ((name) => driver.requirePlugin(name, { requiredBy: plugin.name })),
|
|
2247
|
+
getResolver: driver.getResolver.bind(driver),
|
|
1167
2248
|
driver,
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
2249
|
+
addFile: async (...files) => {
|
|
2250
|
+
driver.fileManager.add(...files);
|
|
2251
|
+
},
|
|
2252
|
+
upsertFile: async (...files) => {
|
|
2253
|
+
driver.fileManager.upsert(...files);
|
|
2254
|
+
},
|
|
2255
|
+
get meta() {
|
|
2256
|
+
return driver.inputNode?.meta ?? {
|
|
2257
|
+
circularNames: [],
|
|
2258
|
+
enumNames: []
|
|
2259
|
+
};
|
|
2260
|
+
},
|
|
2261
|
+
get adapter() {
|
|
2262
|
+
return driver.adapter;
|
|
2263
|
+
},
|
|
2264
|
+
get resolver() {
|
|
2265
|
+
return driver.getResolver(plugin.name);
|
|
2266
|
+
},
|
|
2267
|
+
warn(message) {
|
|
2268
|
+
report({
|
|
2269
|
+
code: Diagnostics.code.pluginWarning,
|
|
2270
|
+
severity: "warning",
|
|
2271
|
+
message
|
|
2272
|
+
});
|
|
2273
|
+
},
|
|
2274
|
+
error(error) {
|
|
2275
|
+
const cause = typeof error === "string" ? void 0 : error;
|
|
2276
|
+
report({
|
|
2277
|
+
code: Diagnostics.code.pluginFailed,
|
|
2278
|
+
severity: "error",
|
|
2279
|
+
message: typeof error === "string" ? error : error.message,
|
|
2280
|
+
cause
|
|
2281
|
+
});
|
|
2282
|
+
},
|
|
2283
|
+
info(message) {
|
|
2284
|
+
report({
|
|
2285
|
+
code: Diagnostics.code.pluginInfo,
|
|
2286
|
+
severity: "info",
|
|
2287
|
+
message
|
|
2288
|
+
});
|
|
2289
|
+
}
|
|
1171
2290
|
};
|
|
1172
|
-
} finally {
|
|
1173
|
-
driver.dispose();
|
|
1174
2291
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
const { files, driver, failedPlugins, pluginTimings, error, sources } = await safeBuild(setupResult);
|
|
1178
|
-
if (error) throw error;
|
|
1179
|
-
if (failedPlugins.size > 0) {
|
|
1180
|
-
const errors = [...failedPlugins].map(({ error }) => error);
|
|
1181
|
-
throw new BuildError(`Build Error with ${failedPlugins.size} failed plugins`, { errors });
|
|
2292
|
+
getPlugin(pluginName) {
|
|
2293
|
+
return this.plugins.get(pluginName);
|
|
1182
2294
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
}
|
|
2295
|
+
requirePlugin(pluginName, context) {
|
|
2296
|
+
const plugin = this.plugins.get(pluginName);
|
|
2297
|
+
if (!plugin) {
|
|
2298
|
+
const requiredBy = context?.requiredBy;
|
|
2299
|
+
throw new Diagnostics.Error({
|
|
2300
|
+
code: Diagnostics.code.pluginNotFound,
|
|
2301
|
+
severity: "error",
|
|
2302
|
+
message: requiredBy ? `Plugin "${pluginName}" is required by "${requiredBy}" but not found. Make sure it is included in your Kubb config.` : `Plugin "${pluginName}" is required but not found. Make sure it is included in your Kubb config.`,
|
|
2303
|
+
help: requiredBy ? `Add "${pluginName}" to the \`plugins\` array in kubb.config.ts (required by "${requiredBy}"), or remove the dependency on it.` : `Add "${pluginName}" to the \`plugins\` array in kubb.config.ts, or remove the dependency on it.`,
|
|
2304
|
+
location: { kind: "config" }
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
return plugin;
|
|
2308
|
+
}
|
|
2309
|
+
};
|
|
1192
2310
|
function inputToAdapterSource(config) {
|
|
1193
2311
|
const input = config.input;
|
|
1194
|
-
if (!input) throw new Error(
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
2312
|
+
if (!input) throw new Diagnostics.Error({
|
|
2313
|
+
code: Diagnostics.code.inputRequired,
|
|
2314
|
+
severity: "error",
|
|
2315
|
+
message: "An adapter is configured without an input.",
|
|
2316
|
+
help: "Provide `input.path` (a file or URL) or `input.data` (an inline spec) in your Kubb config.",
|
|
2317
|
+
location: { kind: "config" }
|
|
2318
|
+
});
|
|
1199
2319
|
if ("data" in input) return {
|
|
1200
2320
|
type: "data",
|
|
1201
2321
|
data: input.data
|
|
1202
2322
|
};
|
|
1203
|
-
if (
|
|
2323
|
+
if (Url.canParse(input.path)) return {
|
|
1204
2324
|
type: "path",
|
|
1205
2325
|
path: input.path
|
|
1206
2326
|
};
|
|
@@ -1209,285 +2329,617 @@ function inputToAdapterSource(config) {
|
|
|
1209
2329
|
path: (0, node_path.resolve)(config.root, input.path)
|
|
1210
2330
|
};
|
|
1211
2331
|
}
|
|
2332
|
+
//#endregion
|
|
2333
|
+
//#region src/storages/fsStorage.ts
|
|
1212
2334
|
/**
|
|
1213
|
-
*
|
|
2335
|
+
* Built-in filesystem storage driver.
|
|
2336
|
+
*
|
|
2337
|
+
* This is the default storage when no `storage` option is configured in the root config.
|
|
2338
|
+
* Keys are resolved against `process.cwd()`, so root-relative paths such as
|
|
2339
|
+
* `src/gen/api/getPets.ts` are written to the correct location without extra configuration.
|
|
1214
2340
|
*
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
2341
|
+
* Writes are deduplicated and directory-safe:
|
|
2342
|
+
* - leading and trailing whitespace is trimmed before writing
|
|
2343
|
+
* - the write is skipped when the file content is already identical
|
|
2344
|
+
* - missing parent directories are created automatically
|
|
2345
|
+
* - Bun's native file API is used when running under Bun
|
|
1219
2346
|
*
|
|
1220
2347
|
* @example
|
|
1221
2348
|
* ```ts
|
|
1222
|
-
*
|
|
2349
|
+
* import { fsStorage } from '@kubb/core'
|
|
2350
|
+
* import { defineConfig } from 'kubb'
|
|
1223
2351
|
*
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
2352
|
+
* export default defineConfig({
|
|
2353
|
+
* input: { path: './petStore.yaml' },
|
|
2354
|
+
* output: { path: './src/gen' },
|
|
2355
|
+
* storage: fsStorage(),
|
|
1226
2356
|
* })
|
|
1227
|
-
*
|
|
1228
|
-
* const { files, failedPlugins } = await kubb.safeBuild()
|
|
1229
2357
|
* ```
|
|
1230
2358
|
*/
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
return
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
|
|
2359
|
+
const fsStorage = require_memoryStorage.createStorage(() => ({
|
|
2360
|
+
name: "fs",
|
|
2361
|
+
async hasItem(key) {
|
|
2362
|
+
try {
|
|
2363
|
+
await (0, node_fs_promises.access)((0, node_path.resolve)(key));
|
|
2364
|
+
return true;
|
|
2365
|
+
} catch (_error) {
|
|
2366
|
+
return false;
|
|
2367
|
+
}
|
|
2368
|
+
},
|
|
2369
|
+
async getItem(key) {
|
|
2370
|
+
try {
|
|
2371
|
+
return await (0, node_fs_promises.readFile)((0, node_path.resolve)(key), "utf8");
|
|
2372
|
+
} catch (_error) {
|
|
2373
|
+
return null;
|
|
2374
|
+
}
|
|
2375
|
+
},
|
|
2376
|
+
async setItem(key, value) {
|
|
2377
|
+
await write((0, node_path.resolve)(key), value, { sanity: false });
|
|
2378
|
+
},
|
|
2379
|
+
async removeItem(key) {
|
|
2380
|
+
await (0, node_fs_promises.rm)((0, node_path.resolve)(key), { force: true });
|
|
2381
|
+
},
|
|
2382
|
+
async getKeys(base) {
|
|
2383
|
+
const resolvedBase = (0, node_path.resolve)(base ?? process.cwd());
|
|
2384
|
+
if (runtime.isBun) {
|
|
2385
|
+
const bunGlob = new Bun.Glob("**/*");
|
|
2386
|
+
return Array.fromAsync(bunGlob.scan({
|
|
2387
|
+
cwd: resolvedBase,
|
|
2388
|
+
onlyFiles: true,
|
|
2389
|
+
dot: true
|
|
2390
|
+
}));
|
|
2391
|
+
}
|
|
2392
|
+
const keys = [];
|
|
2393
|
+
try {
|
|
2394
|
+
for await (const entry of (0, node_fs_promises.glob)("**/*", {
|
|
2395
|
+
cwd: resolvedBase,
|
|
2396
|
+
withFileTypes: true
|
|
2397
|
+
})) if (entry.isFile()) keys.push(toPosixPath((0, node_path.relative)(resolvedBase, (0, node_path.join)(entry.parentPath, entry.name))));
|
|
2398
|
+
} catch (_error) {}
|
|
2399
|
+
return keys;
|
|
2400
|
+
},
|
|
2401
|
+
async clear(base) {
|
|
2402
|
+
if (!base) return;
|
|
2403
|
+
await clean((0, node_path.resolve)(base));
|
|
2404
|
+
}
|
|
2405
|
+
}));
|
|
2406
|
+
//#endregion
|
|
2407
|
+
//#region src/createKubb.ts
|
|
2408
|
+
/**
|
|
2409
|
+
* Builds a `Storage` view scoped to the file paths produced by the current build.
|
|
2410
|
+
* Reads delegate to the underlying `storage` so source bytes stay where they were
|
|
2411
|
+
* written. Writes register the key so subsequent reads and `getKeys` are scoped
|
|
2412
|
+
* to this build's output.
|
|
2413
|
+
*/
|
|
2414
|
+
function createSourcesView(storage) {
|
|
2415
|
+
const paths = /* @__PURE__ */ new Set();
|
|
2416
|
+
return require_memoryStorage.createStorage(() => ({
|
|
2417
|
+
name: `${storage.name}:sources`,
|
|
2418
|
+
async hasItem(key) {
|
|
2419
|
+
return paths.has(key) && await storage.hasItem(key);
|
|
1240
2420
|
},
|
|
1241
|
-
|
|
1242
|
-
return
|
|
2421
|
+
async getItem(key) {
|
|
2422
|
+
return paths.has(key) ? storage.getItem(key) : null;
|
|
1243
2423
|
},
|
|
1244
|
-
|
|
1245
|
-
|
|
2424
|
+
async setItem(key, value) {
|
|
2425
|
+
paths.add(key);
|
|
2426
|
+
await storage.setItem(key, value);
|
|
1246
2427
|
},
|
|
1247
|
-
async
|
|
1248
|
-
|
|
2428
|
+
async removeItem(key) {
|
|
2429
|
+
paths.delete(key);
|
|
2430
|
+
await storage.removeItem(key);
|
|
1249
2431
|
},
|
|
1250
|
-
async
|
|
1251
|
-
if (!
|
|
1252
|
-
|
|
2432
|
+
async getKeys(base) {
|
|
2433
|
+
if (!base) return [...paths];
|
|
2434
|
+
const result = [];
|
|
2435
|
+
for (const key of paths) if (key.startsWith(base)) result.push(key);
|
|
2436
|
+
return result;
|
|
1253
2437
|
},
|
|
1254
|
-
async
|
|
1255
|
-
|
|
1256
|
-
|
|
2438
|
+
async clear() {
|
|
2439
|
+
paths.clear();
|
|
2440
|
+
await storage.clear();
|
|
1257
2441
|
}
|
|
2442
|
+
}))();
|
|
2443
|
+
}
|
|
2444
|
+
function resolveConfig(userConfig) {
|
|
2445
|
+
return {
|
|
2446
|
+
...userConfig,
|
|
2447
|
+
root: userConfig.root || process.cwd(),
|
|
2448
|
+
parsers: userConfig.parsers ?? [],
|
|
2449
|
+
output: {
|
|
2450
|
+
format: false,
|
|
2451
|
+
lint: false,
|
|
2452
|
+
extension: { ".ts": ".ts" },
|
|
2453
|
+
defaultBanner: "simple",
|
|
2454
|
+
...userConfig.output
|
|
2455
|
+
},
|
|
2456
|
+
storage: userConfig.storage ?? fsStorage(),
|
|
2457
|
+
reporters: userConfig.reporters ?? [],
|
|
2458
|
+
plugins: userConfig.plugins ?? []
|
|
1258
2459
|
};
|
|
1259
|
-
return instance;
|
|
1260
2460
|
}
|
|
1261
|
-
//#endregion
|
|
1262
|
-
//#region src/createRenderer.ts
|
|
1263
2461
|
/**
|
|
1264
|
-
*
|
|
2462
|
+
* Kubb code-generation instance bound to a single config entry. Resolves the user
|
|
2463
|
+
* config in the constructor, so `config` is available right away, and shares `hooks`,
|
|
2464
|
+
* `storage`, and `driver` across the `setup → build` lifecycle.
|
|
2465
|
+
*
|
|
2466
|
+
* `createKubb` takes a plain, serializable config object (the shape `defineConfig`
|
|
2467
|
+
* produces), not a fluent builder. Config stays plain data so it can be cache
|
|
2468
|
+
* fingerprinted and validated against the shipped JSON schema.
|
|
1265
2469
|
*
|
|
1266
|
-
*
|
|
1267
|
-
* renderer for a generator. Core will call this factory once per render cycle
|
|
1268
|
-
* to obtain a fresh renderer instance.
|
|
2470
|
+
* Attach event listeners to `.hooks` before calling `setup()` or `build()`.
|
|
1269
2471
|
*
|
|
1270
2472
|
* @example
|
|
1271
2473
|
* ```ts
|
|
1272
|
-
*
|
|
1273
|
-
*
|
|
1274
|
-
*
|
|
1275
|
-
*
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
2474
|
+
* const kubb = createKubb(userConfig)
|
|
2475
|
+
* kubb.hooks.on('kubb:plugin:end', ({ plugin, duration }) => console.log(plugin.name, duration))
|
|
2476
|
+
* const { files, diagnostics } = await kubb.safeBuild()
|
|
2477
|
+
* ```
|
|
2478
|
+
*/
|
|
2479
|
+
var Kubb = class {
|
|
2480
|
+
hooks;
|
|
2481
|
+
config;
|
|
2482
|
+
#driver = null;
|
|
2483
|
+
#storage = null;
|
|
2484
|
+
constructor(userConfig, options = {}) {
|
|
2485
|
+
this.config = resolveConfig(userConfig);
|
|
2486
|
+
this.hooks = options.hooks ?? new require_memoryStorage.AsyncEventEmitter();
|
|
2487
|
+
}
|
|
2488
|
+
get storage() {
|
|
2489
|
+
if (!this.#storage) throw new Error("[kubb] setup() must be called before accessing storage");
|
|
2490
|
+
return this.#storage;
|
|
2491
|
+
}
|
|
2492
|
+
get driver() {
|
|
2493
|
+
if (!this.#driver) throw new Error("[kubb] setup() must be called before accessing driver");
|
|
2494
|
+
return this.#driver;
|
|
2495
|
+
}
|
|
2496
|
+
/**
|
|
2497
|
+
* Initializes the driver and storage. `build()` calls this automatically.
|
|
2498
|
+
*/
|
|
2499
|
+
async setup() {
|
|
2500
|
+
const config = this.config;
|
|
2501
|
+
const driver = new KubbDriver(config, { hooks: this.hooks });
|
|
2502
|
+
const storage = createSourcesView(config.storage);
|
|
2503
|
+
this.hooks.setMaxListeners(Math.max(10, config.plugins.length * 4));
|
|
2504
|
+
if (config.output.clean) await config.storage.clear((0, node_path.resolve)(config.root, config.output.path));
|
|
2505
|
+
await driver.setup();
|
|
2506
|
+
this.#driver = driver;
|
|
2507
|
+
this.#storage = storage;
|
|
2508
|
+
}
|
|
2509
|
+
/**
|
|
2510
|
+
* Runs the full pipeline and throws on any plugin error.
|
|
2511
|
+
* Automatically calls `setup()` if needed.
|
|
2512
|
+
*/
|
|
2513
|
+
async build() {
|
|
2514
|
+
const out = await this.safeBuild();
|
|
2515
|
+
if (Diagnostics.hasError(out.diagnostics)) {
|
|
2516
|
+
const errors = out.diagnostics.filter(Diagnostics.isProblem).filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.cause ?? new Diagnostics.Error(diagnostic));
|
|
2517
|
+
throw new require_memoryStorage.BuildError(`Build failed with ${errors.length} ${errors.length === 1 ? "error" : "errors"}`, { errors });
|
|
2518
|
+
}
|
|
2519
|
+
return out;
|
|
2520
|
+
}
|
|
2521
|
+
/**
|
|
2522
|
+
* Runs the full pipeline and captures errors in `BuildOutput` instead of throwing.
|
|
2523
|
+
* Automatically calls `setup()` if needed. This is the canonical call: it never throws on
|
|
2524
|
+
* plugin errors, so callers stay in control of how failures surface.
|
|
2525
|
+
*/
|
|
2526
|
+
async safeBuild() {
|
|
2527
|
+
try {
|
|
2528
|
+
var _usingCtx$1 = require_memoryStorage._usingCtx();
|
|
2529
|
+
if (!this.#driver) await this.setup();
|
|
2530
|
+
const cleanup = _usingCtx$1.u(this);
|
|
2531
|
+
const driver = cleanup.driver;
|
|
2532
|
+
const storage = cleanup.storage;
|
|
2533
|
+
const { diagnostics } = await driver.run({ storage });
|
|
2534
|
+
return {
|
|
2535
|
+
diagnostics,
|
|
2536
|
+
files: driver.fileManager.files,
|
|
2537
|
+
driver,
|
|
2538
|
+
storage
|
|
2539
|
+
};
|
|
2540
|
+
} catch (_) {
|
|
2541
|
+
_usingCtx$1.e = _;
|
|
2542
|
+
} finally {
|
|
2543
|
+
_usingCtx$1.d();
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
dispose() {
|
|
2547
|
+
this.#driver?.dispose();
|
|
2548
|
+
}
|
|
2549
|
+
[Symbol.dispose]() {
|
|
2550
|
+
this.dispose();
|
|
2551
|
+
}
|
|
2552
|
+
};
|
|
2553
|
+
/**
|
|
2554
|
+
* Constructs a {@link Kubb} build orchestrator from a user config. Equivalent
|
|
2555
|
+
* to `new Kubb(userConfig, options)` and the canonical public entry point.
|
|
1281
2556
|
*
|
|
1282
|
-
*
|
|
1283
|
-
*
|
|
1284
|
-
*
|
|
1285
|
-
*
|
|
1286
|
-
*
|
|
1287
|
-
*
|
|
2557
|
+
* @example
|
|
2558
|
+
* ```ts
|
|
2559
|
+
* import { createKubb } from '@kubb/core'
|
|
2560
|
+
* import { adapterOas } from '@kubb/adapter-oas'
|
|
2561
|
+
* import { pluginTs } from '@kubb/plugin-ts'
|
|
2562
|
+
*
|
|
2563
|
+
* const kubb = createKubb({
|
|
2564
|
+
* input: { path: './petStore.yaml' },
|
|
2565
|
+
* output: { path: './src/gen' },
|
|
2566
|
+
* adapter: adapterOas(),
|
|
2567
|
+
* plugins: [pluginTs()],
|
|
1288
2568
|
* })
|
|
2569
|
+
*
|
|
2570
|
+
* await kubb.build()
|
|
1289
2571
|
* ```
|
|
1290
2572
|
*/
|
|
1291
|
-
function
|
|
1292
|
-
return
|
|
2573
|
+
function createKubb(userConfig, options = {}) {
|
|
2574
|
+
return new Kubb(userConfig, options);
|
|
1293
2575
|
}
|
|
1294
2576
|
//#endregion
|
|
1295
|
-
//#region src/
|
|
2577
|
+
//#region src/createReporter.ts
|
|
1296
2578
|
/**
|
|
1297
|
-
*
|
|
1298
|
-
*
|
|
1299
|
-
*
|
|
2579
|
+
* Numeric log-level thresholds used internally to compare verbosity.
|
|
2580
|
+
*
|
|
2581
|
+
* Higher numbers are more verbose.
|
|
1300
2582
|
*/
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
2583
|
+
const logLevel = {
|
|
2584
|
+
silent: Number.NEGATIVE_INFINITY,
|
|
2585
|
+
error: 0,
|
|
2586
|
+
warn: 1,
|
|
2587
|
+
info: 3,
|
|
2588
|
+
verbose: 4
|
|
2589
|
+
};
|
|
1306
2590
|
/**
|
|
1307
|
-
*
|
|
2591
|
+
* Defines a reporter. When the definition has a `drain`, the returned reporter buffers each value
|
|
2592
|
+
* `report` returns and hands the array to `drain` once, then clears it. Without a `drain`, nothing
|
|
2593
|
+
* is buffered. Wiring the reporter onto the run's events is the host's job, so the reporter only
|
|
2594
|
+
* ever deals with a {@link GenerationResult}.
|
|
1308
2595
|
*
|
|
1309
2596
|
* @example
|
|
1310
2597
|
* ```ts
|
|
1311
|
-
*
|
|
1312
|
-
*
|
|
1313
|
-
*
|
|
1314
|
-
*
|
|
1315
|
-
*
|
|
2598
|
+
* import { createReporter, Diagnostics } from '@kubb/core'
|
|
2599
|
+
*
|
|
2600
|
+
* export const jsonReporter = createReporter({
|
|
2601
|
+
* name: 'json',
|
|
2602
|
+
* report(result) {
|
|
2603
|
+
* return { status: Diagnostics.hasError(result.diagnostics) ? 'failed' : 'success', diagnostics: result.diagnostics }
|
|
2604
|
+
* },
|
|
2605
|
+
* drain(context, reports) {
|
|
2606
|
+
* process.stdout.write(`${JSON.stringify(reports, null, 2)}\n`)
|
|
1316
2607
|
* },
|
|
1317
2608
|
* })
|
|
1318
2609
|
* ```
|
|
1319
2610
|
*/
|
|
1320
|
-
function
|
|
1321
|
-
|
|
2611
|
+
function createReporter(reporter) {
|
|
2612
|
+
const drain = reporter.drain;
|
|
2613
|
+
if (!drain) return {
|
|
2614
|
+
name: reporter.name,
|
|
2615
|
+
async report(result, context) {
|
|
2616
|
+
await reporter.report(result, context);
|
|
2617
|
+
}
|
|
2618
|
+
};
|
|
2619
|
+
const reports = [];
|
|
2620
|
+
return {
|
|
2621
|
+
name: reporter.name,
|
|
2622
|
+
async report(result, context) {
|
|
2623
|
+
reports.push(await reporter.report(result, context));
|
|
2624
|
+
},
|
|
2625
|
+
async drain(context) {
|
|
2626
|
+
await drain(context, reports);
|
|
2627
|
+
reports.length = 0;
|
|
2628
|
+
}
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
//#endregion
|
|
2632
|
+
//#region src/reporters/report.ts
|
|
2633
|
+
/**
|
|
2634
|
+
* Builds the normalized {@link Report} for one config from its {@link GenerationResult}. Splits the
|
|
2635
|
+
* diagnostics into problems and per-plugin timings (slowest first) and derives the plugin and issue
|
|
2636
|
+
* counts, so every reporter renders the same data.
|
|
2637
|
+
*/
|
|
2638
|
+
function buildReport(result) {
|
|
2639
|
+
const { config, diagnostics, filesCreated, status, hrStart } = result;
|
|
2640
|
+
const failed = Diagnostics.failedPlugins(diagnostics);
|
|
2641
|
+
const total = config.plugins?.length ?? 0;
|
|
2642
|
+
const counts = Diagnostics.count(diagnostics);
|
|
2643
|
+
const problems = diagnostics.filter(Diagnostics.isProblem);
|
|
2644
|
+
const timings = diagnostics.filter(Diagnostics.isPerformance).sort((a, b) => b.duration - a.duration).map((diagnostic) => ({
|
|
2645
|
+
plugin: diagnostic.plugin,
|
|
2646
|
+
durationMs: diagnostic.duration
|
|
2647
|
+
}));
|
|
2648
|
+
return {
|
|
2649
|
+
name: config.name ?? "",
|
|
2650
|
+
status,
|
|
2651
|
+
plugins: {
|
|
2652
|
+
passed: total - failed.length,
|
|
2653
|
+
failed,
|
|
2654
|
+
total
|
|
2655
|
+
},
|
|
2656
|
+
counts,
|
|
2657
|
+
filesCreated,
|
|
2658
|
+
durationMs: getElapsedMs(hrStart),
|
|
2659
|
+
output: (0, node_path.resolve)(config.root, config.output.path),
|
|
2660
|
+
timings,
|
|
2661
|
+
diagnostics: problems.map((diagnostic) => Diagnostics.serialize(diagnostic))
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
//#endregion
|
|
2665
|
+
//#region src/reporters/cliReporter.ts
|
|
2666
|
+
/**
|
|
2667
|
+
* Builds the vitest/jest-style summary for one {@link Report}: right-aligned dim labels with
|
|
2668
|
+
* `N passed (total)` counts, and a per-plugin `Timings` section when `showTimings`.
|
|
2669
|
+
*/
|
|
2670
|
+
function buildSummaryLines(report, { showTimings }) {
|
|
2671
|
+
const { status, plugins, counts, filesCreated, durationMs, output, timings } = report;
|
|
2672
|
+
const rows = [];
|
|
2673
|
+
rows.push(["Plugins", status === "success" ? `${(0, node_util.styleText)("green", `${plugins.passed} passed`)} (${plugins.total})` : `${(0, node_util.styleText)("green", `${plugins.passed} passed`)} | ${(0, node_util.styleText)("red", `${plugins.failed.length} failed`)} (${plugins.total})`]);
|
|
2674
|
+
if (status === "failed" && plugins.failed.length > 0) rows.push(["Failed", plugins.failed.map((name) => randomCliColor(name)).join(", ")]);
|
|
2675
|
+
if (counts.errors > 0 || counts.warnings > 0) {
|
|
2676
|
+
const issues = [counts.errors > 0 ? (0, node_util.styleText)("red", `${counts.errors} ${counts.errors === 1 ? "error" : "errors"}`) : void 0, counts.warnings > 0 ? (0, node_util.styleText)("yellow", `${counts.warnings} ${counts.warnings === 1 ? "warning" : "warnings"}`) : void 0].filter(Boolean).join(" | ");
|
|
2677
|
+
rows.push(["Issues", issues]);
|
|
2678
|
+
}
|
|
2679
|
+
rows.push(["Files", `${(0, node_util.styleText)("green", String(filesCreated))} generated`]);
|
|
2680
|
+
rows.push(["Duration", (0, node_util.styleText)("green", formatMs(durationMs))]);
|
|
2681
|
+
rows.push(["Output", output]);
|
|
2682
|
+
const labelWidth = Math.max(...rows.map(([label]) => label.length), timings.length > 0 ? 7 : 0);
|
|
2683
|
+
const lines = rows.map(([label, value]) => `${(0, node_util.styleText)("dim", label.padStart(labelWidth))} ${value}`);
|
|
2684
|
+
if (showTimings && timings.length > 0) {
|
|
2685
|
+
const nameWidth = Math.max(0, ...timings.map((timing) => timing.plugin.length));
|
|
2686
|
+
const indent = " ".repeat(labelWidth + 2);
|
|
2687
|
+
lines.push((0, node_util.styleText)("dim", "Timings".padStart(labelWidth)));
|
|
2688
|
+
for (const timing of timings) {
|
|
2689
|
+
const timeStr = formatMs(timing.durationMs);
|
|
2690
|
+
const barLength = Math.min(Math.ceil(timing.durationMs / 100), 10);
|
|
2691
|
+
const bar = (0, node_util.styleText)("dim", "█".repeat(barLength));
|
|
2692
|
+
lines.push(`${indent}${(0, node_util.styleText)("dim", "•")} ${timing.plugin.padEnd(nameWidth)} ${bar} ${timeStr}`);
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
return lines;
|
|
1322
2696
|
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Renders the summary as plain `console.log` lines so it works in every CLI (no clack/TTY
|
|
2699
|
+
* dependency): a blank line, the config name colored by status, then the summary rows.
|
|
2700
|
+
*/
|
|
2701
|
+
function renderSummary(lines, { title, status }) {
|
|
2702
|
+
console.log("");
|
|
2703
|
+
if (title) console.log((0, node_util.styleText)(status === "failed" ? "red" : "green", title));
|
|
2704
|
+
for (const line of lines) console.log(line);
|
|
2705
|
+
}
|
|
2706
|
+
/**
|
|
2707
|
+
* The default `cli` reporter. Renders the {@link Report} for each config as it finishes, independent
|
|
2708
|
+
* of the live logger view. Suppressed at `silent`. The `verbose` level adds the per-plugin timings.
|
|
2709
|
+
*/
|
|
2710
|
+
const cliReporter = createReporter({
|
|
2711
|
+
name: "cli",
|
|
2712
|
+
report(result, { logLevel: logLevel$1 }) {
|
|
2713
|
+
if (logLevel$1 <= logLevel.silent) return;
|
|
2714
|
+
const report = buildReport(result);
|
|
2715
|
+
renderSummary(buildSummaryLines(report, { showTimings: logLevel$1 >= logLevel.verbose }), {
|
|
2716
|
+
title: report.name,
|
|
2717
|
+
status: report.status
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2720
|
+
});
|
|
1323
2721
|
//#endregion
|
|
1324
|
-
//#region src/
|
|
2722
|
+
//#region src/reporters/fileReporter.ts
|
|
2723
|
+
/**
|
|
2724
|
+
* Builds the `## Summary` section: the same counts the cli and json reporters expose, as a list of
|
|
2725
|
+
* `label value` rows with the labels padded to a common width.
|
|
2726
|
+
*/
|
|
2727
|
+
function buildSummarySection(report) {
|
|
2728
|
+
const { status, plugins, counts, filesCreated, durationMs, output } = report;
|
|
2729
|
+
const rows = [["Status", status], ["Plugins", status === "success" ? `${plugins.passed} passed (${plugins.total})` : `${plugins.passed} passed | ${plugins.failed.length} failed (${plugins.total})`]];
|
|
2730
|
+
if (plugins.failed.length > 0) rows.push(["Failed", plugins.failed.join(", ")]);
|
|
2731
|
+
rows.push(["Issues", `${counts.errors} errors | ${counts.warnings} warnings | ${counts.infos} infos`]);
|
|
2732
|
+
rows.push(["Files", `${filesCreated} generated`]);
|
|
2733
|
+
rows.push(["Duration", formatMs(durationMs)]);
|
|
2734
|
+
rows.push(["Output", output]);
|
|
2735
|
+
const labelWidth = Math.max(...rows.map(([label]) => label.length));
|
|
2736
|
+
return [
|
|
2737
|
+
"## Summary",
|
|
2738
|
+
"",
|
|
2739
|
+
...rows.map(([label, value]) => ` ${label.padEnd(labelWidth)} ${value}`)
|
|
2740
|
+
];
|
|
2741
|
+
}
|
|
2742
|
+
/**
|
|
2743
|
+
* Builds the `## Problems` section: each problem rendered in the miette block format, blocks
|
|
2744
|
+
* separated by a blank line. Returns an empty array when there are no problems, so the caller
|
|
2745
|
+
* can drop the heading.
|
|
2746
|
+
*/
|
|
2747
|
+
function buildProblemSection(diagnostics) {
|
|
2748
|
+
const problems = diagnostics.filter(Diagnostics.isProblem);
|
|
2749
|
+
if (problems.length === 0) return [];
|
|
2750
|
+
return [
|
|
2751
|
+
"## Problems",
|
|
2752
|
+
"",
|
|
2753
|
+
problems.map((diagnostic) => Diagnostics.formatLines(diagnostic).join("\n")).join("\n\n")
|
|
2754
|
+
];
|
|
2755
|
+
}
|
|
2756
|
+
/**
|
|
2757
|
+
* Builds the `## Timings` section from a {@link Report}: one `plugin duration` row per record,
|
|
2758
|
+
* slowest first with the plugin names left-aligned and the durations right-aligned. Returns an
|
|
2759
|
+
* empty array when there are no timings.
|
|
2760
|
+
*/
|
|
2761
|
+
function buildTimingSection(report) {
|
|
2762
|
+
const { timings } = report;
|
|
2763
|
+
if (timings.length === 0) return [];
|
|
2764
|
+
const nameWidth = Math.max(...timings.map((timing) => timing.plugin.length));
|
|
2765
|
+
const durations = timings.map((timing) => formatMs(timing.durationMs));
|
|
2766
|
+
const durationWidth = Math.max(...durations.map((duration) => duration.length));
|
|
2767
|
+
return [
|
|
2768
|
+
"## Timings",
|
|
2769
|
+
"",
|
|
2770
|
+
...timings.map((timing, index) => ` ${timing.plugin.padEnd(nameWidth)} ${durations[index].padStart(durationWidth)}`)
|
|
2771
|
+
];
|
|
2772
|
+
}
|
|
1325
2773
|
/**
|
|
1326
|
-
*
|
|
2774
|
+
* The `file` reporter. Writes a config's {@link Report} to `.kubb/kubb-<name>-<timestamp>.log` as a
|
|
2775
|
+
* plain-text document: a `# <name> — <timestamp>` header, a `## Summary` with the same counts the
|
|
2776
|
+
* cli and json reporters expose, a `## Problems` section in the miette block format, and a
|
|
2777
|
+
* `## Timings` section. Selected with `--reporter file` (or `reporters: ['file']`), replacing the
|
|
2778
|
+
* old `--debug` flag.
|
|
1327
2779
|
*
|
|
1328
|
-
*
|
|
1329
|
-
*
|
|
2780
|
+
* @note Unlike the streaming logger it replaced, it captures the collected diagnostics once a
|
|
2781
|
+
* config finishes, not the live `kubb:info`/`kubb:plugin` event stream. Color is stripped so the
|
|
2782
|
+
* file stays plain text even when the run is attached to a TTY.
|
|
2783
|
+
*/
|
|
2784
|
+
const fileReporter = createReporter({
|
|
2785
|
+
name: "file",
|
|
2786
|
+
async report(result) {
|
|
2787
|
+
const { diagnostics, config } = result;
|
|
2788
|
+
if (diagnostics.length === 0) return;
|
|
2789
|
+
const report = buildReport(result);
|
|
2790
|
+
const content = (0, node_util.stripVTControlCharacters)([config.name ? `# ${config.name} — ${(/* @__PURE__ */ new Date()).toISOString()}` : `# ${(/* @__PURE__ */ new Date()).toISOString()}`, ...[
|
|
2791
|
+
buildSummarySection(report),
|
|
2792
|
+
buildProblemSection(diagnostics),
|
|
2793
|
+
buildTimingSection(report)
|
|
2794
|
+
].filter((section) => section.length > 0).map((section) => section.join("\n"))].join("\n\n"));
|
|
2795
|
+
const baseName = `${[
|
|
2796
|
+
"kubb",
|
|
2797
|
+
config.name,
|
|
2798
|
+
Date.now()
|
|
2799
|
+
].filter(Boolean).join("-")}.log`;
|
|
2800
|
+
const pathName = (0, node_path.resolve)(node_process.default.cwd(), ".kubb", baseName);
|
|
2801
|
+
await write(pathName, `${content}\n`);
|
|
2802
|
+
console.error(`Debug log written to ${(0, node_path.relative)(node_process.default.cwd(), pathName)}`);
|
|
2803
|
+
}
|
|
2804
|
+
});
|
|
2805
|
+
//#endregion
|
|
2806
|
+
//#region src/reporters/jsonReporter.ts
|
|
2807
|
+
/**
|
|
2808
|
+
* The `json` reporter. `report` returns one config's {@link Report}, which {@link createReporter}
|
|
2809
|
+
* buffers, and `drain` writes them as a single pretty-printed JSON array on `kubb:lifecycle:end`.
|
|
2810
|
+
* Buffering keeps a multi-config run one valid JSON document on stdout instead of concatenated
|
|
2811
|
+
* objects that would break `jq .`. The terminal reporter is suppressed while `json` is active so
|
|
2812
|
+
* stdout stays valid JSON.
|
|
2813
|
+
*/
|
|
2814
|
+
const jsonReporter = createReporter({
|
|
2815
|
+
name: "json",
|
|
2816
|
+
report(result) {
|
|
2817
|
+
return buildReport(result);
|
|
2818
|
+
},
|
|
2819
|
+
drain(_context, reports) {
|
|
2820
|
+
node_process.default.stdout.write(`${JSON.stringify(reports, null, 2)}\n`);
|
|
2821
|
+
}
|
|
2822
|
+
});
|
|
2823
|
+
//#endregion
|
|
2824
|
+
//#region src/createRenderer.ts
|
|
2825
|
+
/**
|
|
2826
|
+
* Defines a renderer factory. Renderers turn the generator's return value
|
|
2827
|
+
* (JSX, a template string, a tree of any shape) into `FileNode`s that get
|
|
2828
|
+
* written to disk.
|
|
1330
2829
|
*
|
|
1331
|
-
*
|
|
2830
|
+
* A renderer can target output formats beyond JSX, for instance a Handlebars
|
|
2831
|
+
* renderer or one that writes binary files. Plugins and generators pick the
|
|
2832
|
+
* renderer to use via the `renderer` field on `defineGenerator`.
|
|
1332
2833
|
*
|
|
1333
|
-
* @example
|
|
2834
|
+
* @example A minimal renderer that wraps a custom runtime
|
|
1334
2835
|
* ```ts
|
|
1335
|
-
* import {
|
|
1336
|
-
*
|
|
1337
|
-
* // Stateless middleware
|
|
1338
|
-
* export const logMiddleware = defineMiddleware(() => ({
|
|
1339
|
-
* name: 'log-middleware',
|
|
1340
|
-
* hooks: {
|
|
1341
|
-
* 'kubb:build:end'({ files }) {
|
|
1342
|
-
* console.log(`Build complete with ${files.length} files`)
|
|
1343
|
-
* },
|
|
1344
|
-
* },
|
|
1345
|
-
* }))
|
|
2836
|
+
* import { createRenderer } from '@kubb/core'
|
|
1346
2837
|
*
|
|
1347
|
-
*
|
|
1348
|
-
*
|
|
1349
|
-
* const seen = new Set<string>()
|
|
2838
|
+
* export const myRenderer = createRenderer(() => {
|
|
2839
|
+
* const runtime = new MyRuntime()
|
|
1350
2840
|
* return {
|
|
1351
|
-
*
|
|
1352
|
-
*
|
|
1353
|
-
*
|
|
1354
|
-
*
|
|
1355
|
-
*
|
|
2841
|
+
* async render(element) {
|
|
2842
|
+
* await runtime.render(element)
|
|
2843
|
+
* },
|
|
2844
|
+
* get files() {
|
|
2845
|
+
* return runtime.files
|
|
2846
|
+
* },
|
|
2847
|
+
* [Symbol.dispose]() {
|
|
2848
|
+
* runtime.dispose()
|
|
1356
2849
|
* },
|
|
1357
2850
|
* }
|
|
1358
2851
|
* })
|
|
1359
2852
|
* ```
|
|
1360
2853
|
*/
|
|
1361
|
-
function
|
|
1362
|
-
return
|
|
2854
|
+
function createRenderer(factory) {
|
|
2855
|
+
return factory;
|
|
1363
2856
|
}
|
|
1364
2857
|
//#endregion
|
|
1365
|
-
//#region src/
|
|
2858
|
+
//#region src/defineGenerator.ts
|
|
1366
2859
|
/**
|
|
1367
|
-
* Defines a
|
|
2860
|
+
* Defines a generator: a unit of work that runs during the plugin's AST walk
|
|
2861
|
+
* and produces files. Plugins register generators via `ctx.addGenerator()`
|
|
2862
|
+
* inside `kubb:plugin:setup`.
|
|
1368
2863
|
*
|
|
1369
|
-
*
|
|
2864
|
+
* The returned object is the input as-is, but with `this` types preserved so
|
|
2865
|
+
* `schema`/`operation`/`operations` methods are correctly typed against the
|
|
2866
|
+
* plugin's `PluginFactoryOptions`. Renderer elements and `FileNode[]` returns
|
|
2867
|
+
* are both handled by the runtime, so pick whichever style fits.
|
|
1370
2868
|
*
|
|
1371
|
-
* @example
|
|
1372
|
-
* ```
|
|
1373
|
-
* import {
|
|
2869
|
+
* @example JSX-based schema generator
|
|
2870
|
+
* ```tsx
|
|
2871
|
+
* import { defineGenerator } from '@kubb/core'
|
|
2872
|
+
* import { jsxRenderer } from '@kubb/renderer-jsx'
|
|
1374
2873
|
*
|
|
1375
|
-
* export const
|
|
1376
|
-
* name: '
|
|
1377
|
-
*
|
|
1378
|
-
*
|
|
1379
|
-
*
|
|
1380
|
-
*
|
|
2874
|
+
* export const typeGenerator = defineGenerator({
|
|
2875
|
+
* name: 'typescript',
|
|
2876
|
+
* renderer: jsxRenderer,
|
|
2877
|
+
* schema(node, ctx) {
|
|
2878
|
+
* return (
|
|
2879
|
+
* <File path={`${ctx.root}/${node.name}.ts`}>
|
|
2880
|
+
* <Type node={node} resolver={ctx.resolver} />
|
|
2881
|
+
* </File>
|
|
2882
|
+
* )
|
|
1381
2883
|
* },
|
|
1382
2884
|
* })
|
|
1383
2885
|
* ```
|
|
1384
2886
|
*/
|
|
1385
|
-
function
|
|
1386
|
-
return
|
|
2887
|
+
function defineGenerator(generator) {
|
|
2888
|
+
return generator;
|
|
1387
2889
|
}
|
|
1388
2890
|
//#endregion
|
|
1389
|
-
//#region src/
|
|
2891
|
+
//#region src/defineParser.ts
|
|
1390
2892
|
/**
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1393
|
-
* Handlers live in a single `hooks` object (inspired by Astro integrations).
|
|
1394
|
-
* All lifecycle events from `KubbHooks` are available for subscription.
|
|
1395
|
-
*
|
|
1396
|
-
* @note For real plugins, use a `PluginFactoryOptions` type parameter to get type-safe context in `kubb:plugin:setup`.
|
|
1397
|
-
* Plugin names should follow the convention `plugin-<feature>` (e.g., `plugin-react-query`, `plugin-zod`).
|
|
2893
|
+
* Defines a parser with type-safe `this`. Used to register handlers for new
|
|
2894
|
+
* file extensions or to plug a non-TypeScript output into the build.
|
|
1398
2895
|
*
|
|
1399
2896
|
* @example
|
|
1400
2897
|
* ```ts
|
|
1401
|
-
* import {
|
|
2898
|
+
* import { defineParser } from '@kubb/core'
|
|
2899
|
+
* import { extractStringsFromNodes } from '@kubb/ast/utils'
|
|
1402
2900
|
*
|
|
1403
|
-
* export const
|
|
1404
|
-
* name: '
|
|
1405
|
-
*
|
|
1406
|
-
*
|
|
1407
|
-
*
|
|
1408
|
-
*
|
|
2901
|
+
* export const jsonParser = defineParser({
|
|
2902
|
+
* name: 'json',
|
|
2903
|
+
* extNames: ['.json'],
|
|
2904
|
+
* parse(file) {
|
|
2905
|
+
* return file.sources
|
|
2906
|
+
* .map((source) => extractStringsFromNodes(source.nodes ?? []))
|
|
2907
|
+
* .join('\n')
|
|
2908
|
+
* },
|
|
2909
|
+
* print(...nodes) {
|
|
2910
|
+
* return nodes.map(String).join('\n')
|
|
1409
2911
|
* },
|
|
1410
|
-
* }))
|
|
1411
|
-
* ```
|
|
1412
|
-
*/
|
|
1413
|
-
function definePlugin(factory) {
|
|
1414
|
-
return (options) => factory(options ?? {});
|
|
1415
|
-
}
|
|
1416
|
-
//#endregion
|
|
1417
|
-
//#region src/storages/memoryStorage.ts
|
|
1418
|
-
/**
|
|
1419
|
-
* In-memory storage driver. Useful for testing and dry-run scenarios where
|
|
1420
|
-
* generated output should be captured without touching the filesystem.
|
|
1421
|
-
*
|
|
1422
|
-
* All data lives in a `Map` scoped to the storage instance and is discarded
|
|
1423
|
-
* when the instance is garbage-collected.
|
|
1424
|
-
*
|
|
1425
|
-
* @example
|
|
1426
|
-
* ```ts
|
|
1427
|
-
* import { memoryStorage } from '@kubb/core'
|
|
1428
|
-
* import { defineConfig } from 'kubb'
|
|
1429
|
-
*
|
|
1430
|
-
* export default defineConfig({
|
|
1431
|
-
* input: { path: './petStore.yaml' },
|
|
1432
|
-
* output: { path: './src/gen' },
|
|
1433
|
-
* storage: memoryStorage(),
|
|
1434
2912
|
* })
|
|
1435
2913
|
* ```
|
|
1436
2914
|
*/
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
name: "memory",
|
|
1441
|
-
async hasItem(key) {
|
|
1442
|
-
return store.has(key);
|
|
1443
|
-
},
|
|
1444
|
-
async getItem(key) {
|
|
1445
|
-
return store.get(key) ?? null;
|
|
1446
|
-
},
|
|
1447
|
-
async setItem(key, value) {
|
|
1448
|
-
store.set(key, value);
|
|
1449
|
-
},
|
|
1450
|
-
async removeItem(key) {
|
|
1451
|
-
store.delete(key);
|
|
1452
|
-
},
|
|
1453
|
-
async getKeys(base) {
|
|
1454
|
-
const keys = [...store.keys()];
|
|
1455
|
-
return base ? keys.filter((k) => k.startsWith(base)) : keys;
|
|
1456
|
-
},
|
|
1457
|
-
async clear(base) {
|
|
1458
|
-
if (!base) {
|
|
1459
|
-
store.clear();
|
|
1460
|
-
return;
|
|
1461
|
-
}
|
|
1462
|
-
for (const key of store.keys()) if (key.startsWith(base)) store.delete(key);
|
|
1463
|
-
}
|
|
1464
|
-
};
|
|
1465
|
-
});
|
|
2915
|
+
function defineParser(parser) {
|
|
2916
|
+
return parser;
|
|
2917
|
+
}
|
|
1466
2918
|
//#endregion
|
|
1467
|
-
exports.AsyncEventEmitter = AsyncEventEmitter;
|
|
1468
|
-
exports.
|
|
1469
|
-
exports.
|
|
1470
|
-
exports.
|
|
1471
|
-
exports.URLPath = URLPath;
|
|
2919
|
+
exports.AsyncEventEmitter = require_memoryStorage.AsyncEventEmitter;
|
|
2920
|
+
exports.Diagnostics = Diagnostics;
|
|
2921
|
+
exports.KubbDriver = KubbDriver;
|
|
2922
|
+
exports.Url = Url;
|
|
1472
2923
|
Object.defineProperty(exports, "ast", {
|
|
1473
2924
|
enumerable: true,
|
|
1474
2925
|
get: function() {
|
|
1475
2926
|
return _kubb_ast;
|
|
1476
2927
|
}
|
|
1477
2928
|
});
|
|
2929
|
+
exports.cliReporter = cliReporter;
|
|
1478
2930
|
exports.createAdapter = createAdapter;
|
|
1479
2931
|
exports.createKubb = createKubb;
|
|
1480
2932
|
exports.createRenderer = createRenderer;
|
|
1481
|
-
exports.
|
|
2933
|
+
exports.createReporter = createReporter;
|
|
2934
|
+
exports.createStorage = require_memoryStorage.createStorage;
|
|
1482
2935
|
exports.defineGenerator = defineGenerator;
|
|
1483
|
-
exports.defineLogger = defineLogger;
|
|
1484
|
-
exports.defineMiddleware = defineMiddleware;
|
|
1485
2936
|
exports.defineParser = defineParser;
|
|
1486
2937
|
exports.definePlugin = definePlugin;
|
|
1487
|
-
exports.defineResolver =
|
|
2938
|
+
exports.defineResolver = defineResolver;
|
|
2939
|
+
exports.fileReporter = fileReporter;
|
|
1488
2940
|
exports.fsStorage = fsStorage;
|
|
1489
|
-
exports.
|
|
1490
|
-
exports.logLevel =
|
|
1491
|
-
exports.memoryStorage = memoryStorage;
|
|
2941
|
+
exports.jsonReporter = jsonReporter;
|
|
2942
|
+
exports.logLevel = logLevel;
|
|
2943
|
+
exports.memoryStorage = require_memoryStorage.memoryStorage;
|
|
1492
2944
|
|
|
1493
2945
|
//# sourceMappingURL=index.cjs.map
|