@kubb/core 5.0.0-beta.11 → 5.0.0-beta.110

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.
Files changed (44) hide show
  1. package/LICENSE +17 -10
  2. package/README.md +20 -123
  3. package/dist/index.cjs +2430 -1184
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.ts +136 -140
  6. package/dist/index.js +2419 -1173
  7. package/dist/index.js.map +1 -1
  8. package/dist/mocks.cjs +86 -32
  9. package/dist/mocks.cjs.map +1 -1
  10. package/dist/mocks.d.ts +37 -14
  11. package/dist/mocks.js +88 -36
  12. package/dist/mocks.js.map +1 -1
  13. package/dist/types-Ba5Mo-G8.d.ts +3016 -0
  14. package/dist/usingCtx-BdYw7ICK.cjs +954 -0
  15. package/dist/usingCtx-BdYw7ICK.cjs.map +1 -0
  16. package/dist/usingCtx-njZUKKsY.js +822 -0
  17. package/dist/usingCtx-njZUKKsY.js.map +1 -0
  18. package/package.json +7 -28
  19. package/dist/PluginDriver-C1OsqGBJ.cjs +0 -1086
  20. package/dist/PluginDriver-C1OsqGBJ.cjs.map +0 -1
  21. package/dist/PluginDriver-CGypdXHg.js +0 -989
  22. package/dist/PluginDriver-CGypdXHg.js.map +0 -1
  23. package/dist/createKubb-BSfMDBwR.d.ts +0 -2176
  24. package/src/FileManager.ts +0 -123
  25. package/src/FileProcessor.ts +0 -91
  26. package/src/PluginDriver.ts +0 -466
  27. package/src/constants.ts +0 -39
  28. package/src/createAdapter.ts +0 -108
  29. package/src/createKubb.ts +0 -1380
  30. package/src/createRenderer.ts +0 -57
  31. package/src/createStorage.ts +0 -70
  32. package/src/defineGenerator.ts +0 -175
  33. package/src/defineLogger.ts +0 -58
  34. package/src/defineMiddleware.ts +0 -62
  35. package/src/defineParser.ts +0 -44
  36. package/src/definePlugin.ts +0 -379
  37. package/src/defineResolver.ts +0 -654
  38. package/src/devtools.ts +0 -66
  39. package/src/index.ts +0 -20
  40. package/src/mocks.ts +0 -177
  41. package/src/storages/fsStorage.ts +0 -89
  42. package/src/storages/memoryStorage.ts +0 -55
  43. package/src/types.ts +0 -42
  44. /package/dist/{chunk--u3MIqq1.js → rolldown-runtime-C0LytTxp.js} +0 -0
@@ -0,0 +1,822 @@
1
+ import "./rolldown-runtime-C0LytTxp.js";
2
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
4
+ import { ast, extractStringsFromNodes } from "@kubb/ast";
5
+ import { EventEmitter } from "node:events";
6
+ //#region ../../internals/utils/src/casing.ts
7
+ /**
8
+ * Shared implementation for camelCase and PascalCase conversion.
9
+ * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
10
+ * and capitalizes each word according to `pascal`.
11
+ *
12
+ * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
13
+ */
14
+ function toCamelOrPascal(text, pascal) {
15
+ return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
16
+ if (word.length > 1 && word === word.toUpperCase()) return word;
17
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
18
+ }).join("").replace(/[^a-zA-Z0-9]/g, "");
19
+ }
20
+ /**
21
+ * Converts `text` to camelCase.
22
+ *
23
+ * @example Word boundaries
24
+ * `camelCase('hello-world') // 'helloWorld'`
25
+ *
26
+ * @example With a prefix
27
+ * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
28
+ */
29
+ function camelCase(text, { prefix = "", suffix = "" } = {}) {
30
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
31
+ }
32
+ //#endregion
33
+ //#region ../../internals/utils/src/errors.ts
34
+ /**
35
+ * Thrown when one or more errors occur during a Kubb build.
36
+ * Carries the full list of underlying errors on `errors`.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * throw new BuildError('Build failed', { errors: [err1, err2] })
41
+ * ```
42
+ */
43
+ var BuildError = class extends Error {
44
+ errors;
45
+ constructor(message, options) {
46
+ super(message, { cause: options.cause });
47
+ this.name = "BuildError";
48
+ this.errors = options.errors;
49
+ }
50
+ };
51
+ /**
52
+ * Coerces an unknown thrown value to an `Error` instance.
53
+ * Returns the value as-is when it is already an `Error`; otherwise wraps it with `String(value)`.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * try { ... } catch(err) {
58
+ * throw new BuildError('Build failed', { cause: toError(err), errors: [] })
59
+ * }
60
+ * ```
61
+ */
62
+ function toError(value) {
63
+ return value instanceof Error ? value : new Error(String(value));
64
+ }
65
+ /**
66
+ * Extracts a human-readable message from any thrown value.
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * getErrorMessage(new Error('oops')) // 'oops'
71
+ * getErrorMessage('plain string') // 'plain string'
72
+ * ```
73
+ */
74
+ function getErrorMessage(value) {
75
+ return value instanceof Error ? value.message : String(value);
76
+ }
77
+ //#endregion
78
+ //#region ../../internals/utils/src/runtime.ts
79
+ /**
80
+ * Detects the JavaScript runtime executing the current process and exposes its name and version.
81
+ *
82
+ * Prefer the shared {@link runtime} instance over constructing your own.
83
+ */
84
+ var Runtime = class {
85
+ /**
86
+ * `true` when the current process is running under Bun.
87
+ *
88
+ * Detection keys off the global `Bun` object rather than `process.versions`,
89
+ * because Bun polyfills `process.versions.node` for Node compatibility and would
90
+ * otherwise look like Node.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * if (runtime.isBun) {
95
+ * await Bun.write(path, data)
96
+ * }
97
+ * ```
98
+ */
99
+ get isBun() {
100
+ return typeof Bun !== "undefined";
101
+ }
102
+ /**
103
+ * `true` when the current process is running under Deno.
104
+ */
105
+ get isDeno() {
106
+ return typeof globalThis.Deno !== "undefined";
107
+ }
108
+ /**
109
+ * `true` when the current process is running under Node.
110
+ *
111
+ * Bun and Deno are excluded first so a polyfilled `process` does not register as Node.
112
+ */
113
+ get isNode() {
114
+ return !this.isBun && !this.isDeno && typeof process !== "undefined" && process.versions?.node != null;
115
+ }
116
+ /**
117
+ * Name of the runtime executing the current process.
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * runtime.name // 'bun' when run with `bun kubb`, 'node' otherwise
122
+ * ```
123
+ */
124
+ get name() {
125
+ if (this.isBun) return "bun";
126
+ if (this.isDeno) return "deno";
127
+ return "node";
128
+ }
129
+ /**
130
+ * Version of the active runtime, or an empty string when it cannot be read.
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * runtime.version // '1.3.11' under Bun, '22.22.2' under Node
135
+ * ```
136
+ */
137
+ get version() {
138
+ if (this.isBun) return process.versions.bun ?? "";
139
+ if (this.isDeno) return globalThis.Deno?.version?.deno ?? "";
140
+ return process.versions?.node ?? "";
141
+ }
142
+ };
143
+ /**
144
+ * Shared {@link Runtime} instance describing the JavaScript runtime executing the current process.
145
+ */
146
+ const runtime = new Runtime();
147
+ //#endregion
148
+ //#region ../../internals/utils/src/fs.ts
149
+ /**
150
+ * Reads the file at `path` as a UTF-8 string.
151
+ * Uses `Bun.file().text()` when running under Bun, `fs.readFile` otherwise.
152
+ *
153
+ * @example
154
+ * ```ts
155
+ * const source = await read('./src/Pet.ts')
156
+ * ```
157
+ */
158
+ async function read(path) {
159
+ if (runtime.isBun) return Bun.file(path).text();
160
+ return readFile(path, { encoding: "utf8" });
161
+ }
162
+ /**
163
+ * Whether `stored` already holds `source`, comparing on the trimmed text rather than the exact
164
+ * bytes. Surrounding whitespace is what a formatter adds and what editors strip, and neither is a
165
+ * reason to rewrite the file.
166
+ *
167
+ * Both sides are trimmed, so a storage that keeps bytes verbatim settles on the same answer as one
168
+ * that normalizes what it stores. Trimming only `stored` would leave a source with leading
169
+ * whitespace rewritten on every build, since the stored copy keeps the whitespace the comparison
170
+ * has already dropped.
171
+ */
172
+ function matchesStored({ stored, source }) {
173
+ return stored.trim() === source.trim();
174
+ }
175
+ /**
176
+ * Writes `data` to `path`, trimming surrounding whitespace and ending the file with a single newline
177
+ * the way prettier, biome, and oxfmt all do.
178
+ * Skips the write when the trimmed content is empty, or when the file already holds that content.
179
+ * Creates any missing parent directories automatically.
180
+ * When `sanity` is `true`, re-reads the file after writing and throws if the content does not match.
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * await write('./src/Pet.ts', source) // writes and returns the trimmed content plus a newline
185
+ * await write('./src/Pet.ts', source) // null — file unchanged
186
+ * await write('./src/Pet.ts', ' ') // null — empty content skipped
187
+ * ```
188
+ */
189
+ async function write(path, data, options = {}) {
190
+ const trimmed = data.trim();
191
+ if (trimmed === "") return null;
192
+ const content = `${trimmed}\n`;
193
+ const resolved = resolve(path);
194
+ if (runtime.isBun) {
195
+ const file = Bun.file(resolved);
196
+ if (matchesStored({
197
+ stored: await file.exists() ? await file.text() : "",
198
+ source: trimmed
199
+ })) return null;
200
+ await Bun.write(resolved, content);
201
+ return content;
202
+ }
203
+ try {
204
+ if (matchesStored({
205
+ stored: await readFile(resolved, { encoding: "utf-8" }),
206
+ source: trimmed
207
+ })) return null;
208
+ } catch {}
209
+ try {
210
+ await writeFile(resolved, content, { encoding: "utf-8" });
211
+ } catch (error) {
212
+ if (error.code !== "ENOENT") throw error;
213
+ await mkdir(dirname(resolved), { recursive: true });
214
+ await writeFile(resolved, content, { encoding: "utf-8" });
215
+ }
216
+ if (options.sanity) {
217
+ const savedData = await readFile(resolved, { encoding: "utf-8" });
218
+ if (savedData !== content) throw new Error(`Sanity check failed for ${path}\n\nData[${data.length}]:\n${data}\n\nSaved[${savedData.length}]:\n${savedData}\n`);
219
+ return savedData;
220
+ }
221
+ return content;
222
+ }
223
+ /**
224
+ * Recursively removes `path`. Silently succeeds when `path` does not exist.
225
+ *
226
+ * @example
227
+ * ```ts
228
+ * await clean('./dist')
229
+ * ```
230
+ */
231
+ async function clean(path) {
232
+ return rm(path, {
233
+ recursive: true,
234
+ force: true
235
+ });
236
+ }
237
+ /**
238
+ * Resolves to `true` when `path` is `parent` itself or nested inside it. Both sides are resolved
239
+ * to absolute paths first, so relative and `..`-containing inputs compare correctly.
240
+ *
241
+ * Guards destructive operations: before wiping an output directory, check that it does not contain
242
+ * the project root, otherwise a `clean` would delete `kubb.config` and every source file.
243
+ *
244
+ * @example
245
+ * isPathInside('./src/gen', '.') // true — nested inside the root
246
+ * isPathInside('.', '.') // true — the same directory counts as inside
247
+ * isPathInside('.', './src/gen') // false — the root is not inside its own output
248
+ * isPathInside('../other', '.') // false — escapes the root
249
+ */
250
+ function isPathInside(path, parent) {
251
+ const resolvedPath = resolve(path);
252
+ const resolvedParent = resolve(parent);
253
+ if (resolvedPath === resolvedParent) return true;
254
+ const rel = relative(resolvedParent, resolvedPath);
255
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
256
+ }
257
+ /**
258
+ * Converts a filesystem path to use POSIX (`/`) separators.
259
+ *
260
+ * Most of the codebase compares and composes paths as strings (prefix matching, joining for
261
+ * import specifiers, splitting on `/`). On POSIX `path.resolve` already returns `/`-separated
262
+ * paths, but on Windows it returns `\`-separated paths, which breaks every such comparison.
263
+ *
264
+ * Routing every path that crosses a module boundary through `toPosixPath` keeps the rest of the
265
+ * code platform-agnostic. The conversion runs unconditionally so Windows-specific behavior is
266
+ * exercisable from POSIX CI.
267
+ *
268
+ * @example
269
+ * toPosixPath('C:\\repo\\src\\pet.ts') // 'C:/repo/src/pet.ts'
270
+ */
271
+ function toPosixPath(filePath) {
272
+ return filePath.replaceAll("\\", "/");
273
+ }
274
+ /**
275
+ * Builds a nested file path from a dotted name. Splits on dots that precede a letter
276
+ * (so version numbers embedded in operationIds like `v2025.0` stay intact), camelCases
277
+ * every earlier segment, applies `caseLast` to the final segment, and joins with `/`.
278
+ *
279
+ * Empty segments are dropped before joining. They arise when the name starts with a dot
280
+ * followed by a letter (e.g. `..Schema` splits into `['..', 'Schema']` and `'..'` cases to
281
+ * an empty string). Without this a leading `/` would form, which `path.resolve` reads as an
282
+ * absolute path, letting generated files escape the configured output directory.
283
+ *
284
+ * @example Nested path from a dotted name
285
+ * `toFilePath('pet.petId') // 'pet/petId'`
286
+ *
287
+ * @example PascalCase the final segment
288
+ * `toFilePath('pet.Pet', pascalCase) // 'pet/Pet'`
289
+ *
290
+ * @example Suffix applied to the final segment only
291
+ * `toFilePath('tag.tag', (part) => camelCase(part, { suffix: 'schema' })) // 'tag/tagSchema'`
292
+ */
293
+ function toFilePath(name, caseLast = camelCase) {
294
+ const parts = name.split(/\.(?=[a-zA-Z])/);
295
+ return parts.map((part, i) => i === parts.length - 1 ? caseLast(part) : camelCase(part)).filter(Boolean).join("/");
296
+ }
297
+ //#endregion
298
+ //#region ../../internals/utils/src/promise.ts
299
+ /**
300
+ * Runs `run` over every item with at most `limit` in flight. Workers share one iterator, so each
301
+ * takes the next item the moment it frees up instead of waiting for a batch to drain.
302
+ *
303
+ * @example
304
+ * ```ts
305
+ * await inParallel({ items: files, limit: 50, run: (file) => storage.writeItem(file.path, file.source) })
306
+ * ```
307
+ */
308
+ async function inParallel({ items, limit, run }) {
309
+ const queue = items.entries();
310
+ const worker = async () => {
311
+ for (const [index, item] of queue) await run(item, index);
312
+ };
313
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
314
+ }
315
+ //#endregion
316
+ //#region src/constants.ts
317
+ /**
318
+ * Plugin `include` filter types that select operations directly. When one of these is set
319
+ * without a `schemaName` include, the generate phase pre-scans operations to compute the set
320
+ * of schemas they reach, so unreachable schemas can be pruned for that plugin.
321
+ */
322
+ const OPERATION_FILTER_TYPES = /* @__PURE__ */ new Set([
323
+ "tag",
324
+ "operationId",
325
+ "path",
326
+ "method",
327
+ "contentType"
328
+ ]);
329
+ /**
330
+ * Stable codes Kubb attaches to a `Diagnostic`. Each maps to a known failure mode
331
+ * and stays stable so it can be referenced in tooling and (later) docs. Reference
332
+ * these instead of inlining the string at a throw site.
333
+ */
334
+ const diagnosticCode = {
335
+ /**
336
+ * Fallback for an unstructured error with no specific code.
337
+ */
338
+ unknown: "KUBB_UNKNOWN",
339
+ /**
340
+ * The file or URL set as `input` could not be read.
341
+ */
342
+ inputNotFound: "KUBB_INPUT_NOT_FOUND",
343
+ /**
344
+ * A URL set as `input` (or referenced by a `$ref`) answered with a 4xx or 5xx status
345
+ * instead of the document.
346
+ */
347
+ inputRequestFailed: "KUBB_INPUT_REQUEST_FAILED",
348
+ /**
349
+ * A URL set as `input` (or referenced by a `$ref`) never answered, so the request failed
350
+ * before a status was returned.
351
+ */
352
+ inputUnreachable: "KUBB_INPUT_UNREACHABLE",
353
+ /**
354
+ * An adapter was configured without an `input`.
355
+ */
356
+ inputRequired: "KUBB_INPUT_REQUIRED",
357
+ /**
358
+ * `input` uses the v4 `{ path }` / `{ data }` wrapper, which v5 reads as a parsed
359
+ * document instead of a pointer to one.
360
+ */
361
+ legacyInput: "KUBB_LEGACY_INPUT",
362
+ /**
363
+ * The parsed `input` carries no `openapi` or `swagger` version, so it is not a
364
+ * document the adapter can read.
365
+ */
366
+ invalidDocument: "KUBB_INVALID_DOCUMENT",
367
+ /**
368
+ * A `$ref` (or equivalent reference) could not be resolved in the source document.
369
+ */
370
+ refNotFound: "KUBB_REF_NOT_FOUND",
371
+ /**
372
+ * A server variable value is not allowed by its `enum`.
373
+ */
374
+ invalidServerVariable: "KUBB_INVALID_SERVER_VARIABLE",
375
+ /**
376
+ * A required plugin is missing from the config.
377
+ */
378
+ pluginNotFound: "KUBB_PLUGIN_NOT_FOUND",
379
+ /**
380
+ * A plugin threw while generating.
381
+ */
382
+ pluginFailed: "KUBB_PLUGIN_FAILED",
383
+ /**
384
+ * A plugin reported a non-fatal warning through `ctx.warn`.
385
+ */
386
+ pluginWarning: "KUBB_PLUGIN_WARNING",
387
+ /**
388
+ * A plugin reported an informational message through `ctx.info`.
389
+ */
390
+ pluginInfo: "KUBB_PLUGIN_INFO",
391
+ /**
392
+ * A schema uses a `format` Kubb does not map to a specific type. Reserved for
393
+ * adapters to emit as a `warning`.
394
+ */
395
+ unsupportedFormat: "KUBB_UNSUPPORTED_FORMAT",
396
+ /**
397
+ * A referenced schema or operation is marked `deprecated`. Reserved for adapters
398
+ * to emit as an `info`.
399
+ */
400
+ deprecated: "KUBB_DEPRECATED",
401
+ /**
402
+ * An adapter is required but the config has none. The build cannot read the input
403
+ * without one.
404
+ */
405
+ adapterRequired: "KUBB_ADAPTER_REQUIRED",
406
+ /**
407
+ * A resolved output path escapes the output directory, which can stem from a path
408
+ * traversal in the spec or a misconfigured `group.name`.
409
+ */
410
+ pathTraversal: "KUBB_PATH_TRAVERSAL",
411
+ /**
412
+ * `output.clean` is enabled but `output.path` resolves to the project root or a parent of it,
413
+ * so cleaning would delete kubb.config and every source file.
414
+ */
415
+ cleanRoot: "KUBB_CLEAN_ROOT",
416
+ /**
417
+ * A plugin's options are invalid, for example `output.mode: 'file'` paired with a `group` option.
418
+ */
419
+ invalidPluginOptions: "KUBB_INVALID_PLUGIN_OPTIONS",
420
+ /**
421
+ * A post-generate command (`output.postGenerate`) exited with a failure.
422
+ */
423
+ postGenerateFailed: "KUBB_POST_GENERATE_FAILED",
424
+ /**
425
+ * The formatter pass over the generated files failed.
426
+ */
427
+ formatFailed: "KUBB_FORMAT_FAILED",
428
+ /**
429
+ * The linter pass over the generated files failed.
430
+ */
431
+ lintFailed: "KUBB_LINT_FAILED",
432
+ /**
433
+ * Not a failure. Carries a plugin's elapsed time, summed into the run total.
434
+ */
435
+ performance: "KUBB_PERFORMANCE",
436
+ /**
437
+ * Not a failure. A newer Kubb version is available on npm.
438
+ */
439
+ updateAvailable: "KUBB_UPDATE_AVAILABLE"
440
+ };
441
+ //#endregion
442
+ //#region src/Hookable.ts
443
+ /**
444
+ * Typed hook emitter that awaits all async listeners before resolving.
445
+ * Wraps Node's `EventEmitter` with full TypeScript hook-map inference.
446
+ *
447
+ * @example
448
+ * ```ts
449
+ * const hooks = new Hookable<{ build: [name: string] }>()
450
+ * hooks.hook('build', async (name) => { console.log(name) })
451
+ * await hooks.callHook('build', 'petstore') // all listeners awaited
452
+ * ```
453
+ */
454
+ var Hookable = class {
455
+ /**
456
+ * Maximum number of listeners per hook before Node emits a memory-leak warning.
457
+ * @default 10
458
+ */
459
+ constructor(maxListener = 10) {
460
+ this.#emitter.setMaxListeners(maxListener);
461
+ }
462
+ #emitter = new EventEmitter();
463
+ /**
464
+ * Calls `hookName` and awaits all registered listeners sequentially.
465
+ * Throws if any listener rejects, wrapping the cause with the hook name and serialized arguments.
466
+ *
467
+ * @example
468
+ * ```ts
469
+ * await hooks.callHook('build', 'petstore')
470
+ * ```
471
+ */
472
+ callHook(hookName, ...hookArgs) {
473
+ const listeners = this.#emitter.listeners(hookName);
474
+ if (listeners.length === 0) return;
475
+ return this.#emitAll(hookName, listeners, hookArgs);
476
+ }
477
+ async #emitAll(hookName, listeners, hookArgs) {
478
+ for (const listener of listeners) try {
479
+ await listener(...hookArgs);
480
+ } catch (err) {
481
+ let serializedArgs;
482
+ try {
483
+ serializedArgs = JSON.stringify(hookArgs);
484
+ } catch {
485
+ serializedArgs = String(hookArgs);
486
+ }
487
+ throw new Error(`Error in async listener for "${hookName}" with hookArgs ${serializedArgs}`, { cause: toError(err) });
488
+ }
489
+ }
490
+ /**
491
+ * Registers a persistent listener for `hookName` and returns a function that removes it.
492
+ *
493
+ * @example
494
+ * ```ts
495
+ * const unhook = hooks.hook('build', async (name) => { console.log(name) })
496
+ * unhook() // removes it
497
+ * ```
498
+ */
499
+ hook(hookName, handler) {
500
+ this.#emitter.on(hookName, handler);
501
+ return () => this.removeHook(hookName, handler);
502
+ }
503
+ /**
504
+ * Registers every handler in `configHooks` at once and returns a function that removes them
505
+ * all. Undefined entries are skipped, so a partial hook object registers only its present keys.
506
+ *
507
+ * @example
508
+ * ```ts
509
+ * const unhook = hooks.addHooks({ build: onBuild, done: onDone })
510
+ * unhook() // removes both
511
+ * ```
512
+ */
513
+ addHooks(configHooks) {
514
+ const unhooks = Object.keys(configHooks).filter((name) => configHooks[name]).map((name) => this.hook(name, configHooks[name]));
515
+ return () => {
516
+ for (const unhook of unhooks) unhook();
517
+ };
518
+ }
519
+ /**
520
+ * Removes a previously registered listener.
521
+ *
522
+ * @example
523
+ * ```ts
524
+ * hooks.removeHook('build', handler)
525
+ * ```
526
+ */
527
+ removeHook(hookName, handler) {
528
+ this.#emitter.off(hookName, handler);
529
+ }
530
+ /**
531
+ * Returns the number of listeners registered for `hookName`.
532
+ *
533
+ * @example
534
+ * ```ts
535
+ * hooks.hook('build', handler)
536
+ * hooks.listenerCount('build') // 1
537
+ * ```
538
+ */
539
+ listenerCount(hookName) {
540
+ return this.#emitter.listenerCount(hookName);
541
+ }
542
+ /**
543
+ * Raises or lowers the per-hook listener ceiling before Node warns about a memory leak.
544
+ * Set this above the expected listener count when many listeners attach by design.
545
+ *
546
+ * @example
547
+ * ```ts
548
+ * hooks.setMaxListeners(40)
549
+ * ```
550
+ */
551
+ setMaxListeners(max) {
552
+ this.#emitter.setMaxListeners(max);
553
+ }
554
+ /**
555
+ * Removes all listeners from every hook channel.
556
+ *
557
+ * @example
558
+ * ```ts
559
+ * hooks.removeAllHooks()
560
+ * ```
561
+ */
562
+ removeAllHooks() {
563
+ this.#emitter.removeAllListeners();
564
+ }
565
+ };
566
+ //#endregion
567
+ //#region src/FileManager.ts
568
+ function joinSources(file) {
569
+ return file.sources.map((source) => extractStringsFromNodes(source.nodes)).filter(Boolean).join("\n\n");
570
+ }
571
+ async function parseCopy(file) {
572
+ let content;
573
+ try {
574
+ content = await read(file.copy);
575
+ } catch (err) {
576
+ throw new Error(`[kubb] Could not copy file into output: ${file.copy}`, { cause: err });
577
+ }
578
+ return [
579
+ file.banner,
580
+ content,
581
+ file.footer
582
+ ].filter((segment) => Boolean(segment)).map((segment) => segment.trimEnd()).join("\n");
583
+ }
584
+ function mergeFile(a, b) {
585
+ return {
586
+ ...a,
587
+ banner: b.banner,
588
+ footer: b.footer,
589
+ copy: b.copy ?? a.copy,
590
+ sources: a.sources.length ? b.sources.length ? [...a.sources, ...b.sources] : a.sources : b.sources,
591
+ imports: a.imports.length ? b.imports.length ? [...a.imports, ...b.imports] : a.imports : b.imports,
592
+ exports: a.exports.length ? b.exports.length ? [...a.exports, ...b.exports] : a.exports : b.exports
593
+ };
594
+ }
595
+ function isIndexPath(path) {
596
+ return path.endsWith("/index.ts") || path === "index.ts";
597
+ }
598
+ function compareFiles(a, b) {
599
+ const lenDiff = a.path.length - b.path.length;
600
+ if (lenDiff !== 0) return lenDiff;
601
+ const aIsIndex = isIndexPath(a.path);
602
+ const bIsIndex = isIndexPath(b.path);
603
+ if (aIsIndex && !bIsIndex) return 1;
604
+ if (!aIsIndex && bIsIndex) return -1;
605
+ return 0;
606
+ }
607
+ function isUnchanged({ stored, source, key, manifest }) {
608
+ if (stored === null) return false;
609
+ if (matchesStored({
610
+ stored,
611
+ source
612
+ })) return true;
613
+ return manifest?.isUpToDate({
614
+ key,
615
+ source,
616
+ disk: stored
617
+ }) ?? false;
618
+ }
619
+ /**
620
+ * In-memory file store for generated files, and the writer that turns them into source
621
+ * strings on `storage`. Files sharing a `path` are merged (sources/imports/exports
622
+ * concatenated). The `files` getter is sorted by path length (barrel `index.ts` last
623
+ * within a bucket).
624
+ *
625
+ * @example
626
+ * ```ts
627
+ * const manager = new FileManager()
628
+ * manager.upsert(myFile)
629
+ * manager.files // sorted view
630
+ * await manager.write(manager.files, { storage: fsStorage() })
631
+ * ```
632
+ */
633
+ var FileManager = class {
634
+ hooks = new Hookable();
635
+ #cache = /* @__PURE__ */ new Map();
636
+ #sorted = null;
637
+ add(...files) {
638
+ return this.#store(files, false);
639
+ }
640
+ upsert(...files) {
641
+ return this.#store(files, true);
642
+ }
643
+ #store(files, mergeExisting) {
644
+ const batch = files.length > 1 ? this.#dedupe(files) : files;
645
+ const resolved = [];
646
+ for (const file of batch) {
647
+ const existing = this.#cache.get(file.path);
648
+ const merged = existing && mergeExisting ? ast.factory.createFile(mergeFile(existing, file)) : ast.factory.createFile(file);
649
+ this.#cache.set(merged.path, merged);
650
+ resolved.push(merged);
651
+ }
652
+ if (resolved.length > 0) this.#sorted = null;
653
+ return resolved;
654
+ }
655
+ #dedupe(files) {
656
+ const seen = /* @__PURE__ */ new Map();
657
+ for (const file of files) {
658
+ const prev = seen.get(file.path);
659
+ seen.set(file.path, prev ? mergeFile(prev, file) : file);
660
+ }
661
+ return [...seen.values()];
662
+ }
663
+ clear() {
664
+ this.#cache.clear();
665
+ this.#sorted = null;
666
+ }
667
+ /**
668
+ * Releases all stored files and clears every `hooks` listener. Called by the core after
669
+ * `kubb:build:end`.
670
+ */
671
+ dispose() {
672
+ this.clear();
673
+ this.hooks.removeAllHooks();
674
+ }
675
+ /**
676
+ * All stored files in stable sort order (shortest path first, barrel files
677
+ * last within a length bucket). Returns a cached view, do not mutate.
678
+ */
679
+ get files() {
680
+ return this.#sorted ??= [...this.#cache.values()].sort(compareFiles);
681
+ }
682
+ /**
683
+ * Converts a file's AST sources (or its `copy` source) into the final on-disk string.
684
+ */
685
+ async parse(file, { parsers } = {}) {
686
+ if (file.copy) return parseCopy(file);
687
+ if (!parsers || !file.extname) return joinSources(file);
688
+ const parser = parsers.get(file.extname);
689
+ if (!parser) return joinSources(file);
690
+ return parser.parse(file);
691
+ }
692
+ /**
693
+ * Parses and writes every file through a bounded pool of workers. A small spec runs all its files
694
+ * at once; a spec with thousands of files keeps at most {@link FILE_CONCURRENCY} parsed sources in
695
+ * memory rather than holding every source, while still overlapping each file's write with the
696
+ * next file's parse. Each `update` carries the file's input position, so a consumer can present
697
+ * the files in generation order even though they finish in whatever order they parse.
698
+ *
699
+ * A file the storage already holds is skipped, so a rebuild that generates identical output
700
+ * writes nothing and leaves every mtime where it was.
701
+ */
702
+ async write(files, { storage, parsers, manifest }) {
703
+ if (files.length === 0) return;
704
+ await this.hooks.callHook("start", files);
705
+ const total = files.length;
706
+ await inParallel({
707
+ items: files,
708
+ limit: 50,
709
+ run: async (file, index) => {
710
+ const source = await this.parse(file, { parsers });
711
+ await this.hooks.callHook("update", {
712
+ file,
713
+ source,
714
+ processed: index + 1,
715
+ total,
716
+ percentage: (index + 1) / total * 100
717
+ });
718
+ if (!source) return;
719
+ if (isUnchanged({
720
+ stored: await storage.readItem(file.path),
721
+ source,
722
+ key: file.path,
723
+ manifest
724
+ })) return;
725
+ await storage.writeItem(file.path, source);
726
+ manifest?.track({
727
+ key: file.path,
728
+ source
729
+ });
730
+ }
731
+ });
732
+ await this.hooks.callHook("end", files);
733
+ }
734
+ };
735
+ //#endregion
736
+ //#region src/nodeCache.ts
737
+ /**
738
+ * Builds an empty {@link NodeCache} backed by a plain `Map`. Called once per node in the generate
739
+ * walk.
740
+ */
741
+ function createNodeCache() {
742
+ const store = /* @__PURE__ */ new Map();
743
+ return {
744
+ readItem(key) {
745
+ return store.get(key);
746
+ },
747
+ writeItem(key, value) {
748
+ store.set(key, value);
749
+ return value;
750
+ },
751
+ ensureItem(key, factory) {
752
+ if (store.has(key)) return store.get(key);
753
+ const value = factory();
754
+ store.set(key, value);
755
+ return value;
756
+ }
757
+ };
758
+ }
759
+ //#endregion
760
+ //#region \0@oxc-project+runtime@0.142.0/helpers/esm/usingCtx.js
761
+ function _usingCtx() {
762
+ var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
763
+ var n = Error();
764
+ return n.name = "SuppressedError", n.error = r, n.suppressed = e, n;
765
+ };
766
+ var e = {};
767
+ var n = [];
768
+ function using(r, e) {
769
+ if (null != e) {
770
+ if (Object(e) !== e) throw new TypeError("using declarations can only be used with objects, functions, null, or undefined.");
771
+ if (r) var o = e[Symbol.asyncDispose || Symbol["for"]("Symbol.asyncDispose")];
772
+ if (void 0 === o && (o = e[Symbol.dispose || Symbol["for"]("Symbol.dispose")], r)) var t = o;
773
+ if ("function" != typeof o) throw new TypeError("Object is not disposable.");
774
+ t && (o = function o() {
775
+ try {
776
+ t.call(e);
777
+ } catch (r) {
778
+ return Promise.reject(r);
779
+ }
780
+ }), n.push({
781
+ v: e,
782
+ d: o,
783
+ a: r
784
+ });
785
+ } else r && n.push({
786
+ d: e,
787
+ a: r
788
+ });
789
+ return e;
790
+ }
791
+ return {
792
+ e,
793
+ u: using.bind(null, !1),
794
+ a: using.bind(null, !0),
795
+ d: function d() {
796
+ var o;
797
+ var t = this.e;
798
+ var s = 0;
799
+ function next() {
800
+ for (; o = n.pop();) try {
801
+ if (!o.a && 1 === s) return s = 0, n.push(o), Promise.resolve().then(next);
802
+ if (o.d) {
803
+ var r = o.d.call(o.v);
804
+ if (o.a) return s |= 2, Promise.resolve(r).then(next, err);
805
+ } else s |= 1;
806
+ } catch (r) {
807
+ return err(r);
808
+ }
809
+ if (1 === s) return t !== e ? Promise.reject(t) : Promise.resolve();
810
+ if (t !== e) throw t;
811
+ }
812
+ function err(n) {
813
+ return t = t !== e ? new r(n, t) : n, next();
814
+ }
815
+ return next();
816
+ }
817
+ };
818
+ }
819
+ //#endregion
820
+ export { OPERATION_FILTER_TYPES as a, clean as c, toPosixPath as d, write as f, camelCase as g, toError as h, Hookable as i, isPathInside as l, getErrorMessage as m, createNodeCache as n, diagnosticCode as o, BuildError as p, FileManager as r, inParallel as s, _usingCtx as t, toFilePath as u };
821
+
822
+ //# sourceMappingURL=usingCtx-njZUKKsY.js.map