@kubb/core
Advanced tools
Sorry, the diff of this file is too big to display
| //#region \0rolldown/runtime.js | ||
| var __create = Object.create; | ||
| var __defProp = Object.defineProperty; | ||
| var __name = (target, value) => __defProp(target, "name", { | ||
| value, | ||
| configurable: true | ||
| }); | ||
| var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | ||
| var __getOwnPropNames = Object.getOwnPropertyNames; | ||
| var __getProtoOf = Object.getPrototypeOf; | ||
| var __hasOwnProp = Object.prototype.hasOwnProperty; | ||
| var __copyProps = (to, from, except, desc) => { | ||
| if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { | ||
| key = keys[i]; | ||
| if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { | ||
| get: ((k) => from[k]).bind(null, key), | ||
| enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable | ||
| }); | ||
| } | ||
| return to; | ||
| }; | ||
| var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { | ||
| value: mod, | ||
| enumerable: true | ||
| }) : target, mod)); | ||
| //#endregion | ||
| let node_events = require("node:events"); | ||
| let node_fs_promises = require("node:fs/promises"); | ||
| let node_path = require("node:path"); | ||
| let _kubb_ast = require("@kubb/ast"); | ||
| let _kubb_ast_utils = require("@kubb/ast/utils"); | ||
| //#region ../../internals/utils/src/errors.ts | ||
| /** | ||
| * Thrown when one or more errors occur during a Kubb build. | ||
| * Carries the full list of underlying errors on `errors`. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * throw new BuildError('Build failed', { errors: [err1, err2] }) | ||
| * ``` | ||
| */ | ||
| var BuildError = class extends Error { | ||
| errors; | ||
| constructor(message, options) { | ||
| super(message, { cause: options.cause }); | ||
| this.name = "BuildError"; | ||
| this.errors = options.errors; | ||
| } | ||
| }; | ||
| /** | ||
| * Coerces an unknown thrown value to an `Error` instance. | ||
| * Returns the value as-is when it is already an `Error`; otherwise wraps it with `String(value)`. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * try { ... } catch(err) { | ||
| * throw new BuildError('Build failed', { cause: toError(err), errors: [] }) | ||
| * } | ||
| * ``` | ||
| */ | ||
| function toError(value) { | ||
| return value instanceof Error ? value : new Error(String(value)); | ||
| } | ||
| /** | ||
| * Extracts a human-readable message from any thrown value. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * getErrorMessage(new Error('oops')) // 'oops' | ||
| * getErrorMessage('plain string') // 'plain string' | ||
| * ``` | ||
| */ | ||
| function getErrorMessage(value) { | ||
| return value instanceof Error ? value.message : String(value); | ||
| } | ||
| //#endregion | ||
| //#region ../../internals/utils/src/asyncEventEmitter.ts | ||
| /** | ||
| * Typed `EventEmitter` that awaits all async listeners before resolving. | ||
| * Wraps Node's `EventEmitter` with full TypeScript event-map inference. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const emitter = new AsyncEventEmitter<{ build: [name: string] }>() | ||
| * emitter.on('build', async (name) => { console.log(name) }) | ||
| * await emitter.emit('build', 'petstore') // all listeners awaited | ||
| * ``` | ||
| */ | ||
| var AsyncEventEmitter = class { | ||
| /** | ||
| * Maximum number of listeners per event before Node emits a memory-leak warning. | ||
| * @default 10 | ||
| */ | ||
| constructor(maxListener = 10) { | ||
| this.#emitter.setMaxListeners(maxListener); | ||
| } | ||
| #emitter = new node_events.EventEmitter(); | ||
| /** | ||
| * Emits `eventName` and awaits all registered listeners sequentially. | ||
| * Throws if any listener rejects, wrapping the cause with the event name and serialized arguments. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * await emitter.emit('build', 'petstore') | ||
| * ``` | ||
| */ | ||
| emit(eventName, ...eventArgs) { | ||
| const listeners = this.#emitter.listeners(eventName); | ||
| if (listeners.length === 0) return; | ||
| return this.#emitAll(eventName, listeners, eventArgs); | ||
| } | ||
| async #emitAll(eventName, listeners, eventArgs) { | ||
| for (const listener of listeners) try { | ||
| await listener(...eventArgs); | ||
| } catch (err) { | ||
| let serializedArgs; | ||
| try { | ||
| serializedArgs = JSON.stringify(eventArgs); | ||
| } catch { | ||
| serializedArgs = String(eventArgs); | ||
| } | ||
| throw new Error(`Error in async listener for "${eventName}" with eventArgs ${serializedArgs}`, { cause: toError(err) }); | ||
| } | ||
| } | ||
| /** | ||
| * Registers a persistent listener for `eventName`. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * emitter.on('build', async (name) => { console.log(name) }) | ||
| * ``` | ||
| */ | ||
| on(eventName, handler) { | ||
| this.#emitter.on(eventName, handler); | ||
| } | ||
| /** | ||
| * Removes a previously registered listener. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * emitter.off('build', handler) | ||
| * ``` | ||
| */ | ||
| off(eventName, handler) { | ||
| this.#emitter.off(eventName, handler); | ||
| } | ||
| /** | ||
| * Returns the number of listeners registered for `eventName`. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * emitter.on('build', handler) | ||
| * emitter.listenerCount('build') // 1 | ||
| * ``` | ||
| */ | ||
| listenerCount(eventName) { | ||
| return this.#emitter.listenerCount(eventName); | ||
| } | ||
| /** | ||
| * Raises or lowers the per-event listener ceiling before Node warns about a memory leak. | ||
| * Set this above the expected listener count when many listeners attach by design. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * emitter.setMaxListeners(40) | ||
| * ``` | ||
| */ | ||
| setMaxListeners(max) { | ||
| this.#emitter.setMaxListeners(max); | ||
| } | ||
| /** | ||
| * Removes all listeners from every event channel. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * emitter.removeAll() | ||
| * ``` | ||
| */ | ||
| removeAll() { | ||
| this.#emitter.removeAllListeners(); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region ../../internals/utils/src/casing.ts | ||
| /** | ||
| * Shared implementation for camelCase and PascalCase conversion. | ||
| * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons) | ||
| * and capitalizes each word according to `pascal`. | ||
| * | ||
| * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are. | ||
| */ | ||
| function toCamelOrPascal(text, pascal) { | ||
| 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) => { | ||
| if (word.length > 1 && word === word.toUpperCase()) return word; | ||
| return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1); | ||
| }).join("").replace(/[^a-zA-Z0-9]/g, ""); | ||
| } | ||
| /** | ||
| * Converts `text` to camelCase. | ||
| * | ||
| * @example Word boundaries | ||
| * `camelCase('hello-world') // 'helloWorld'` | ||
| * | ||
| * @example With a prefix | ||
| * `camelCase('tag', { prefix: 'create' }) // 'createTag'` | ||
| */ | ||
| function camelCase(text, { prefix = "", suffix = "" } = {}) { | ||
| return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false); | ||
| } | ||
| /** | ||
| * Converts `text` to PascalCase. | ||
| * | ||
| * @example Word boundaries | ||
| * `pascalCase('hello-world') // 'HelloWorld'` | ||
| * | ||
| * @example With a suffix | ||
| * `pascalCase('tag', { suffix: 'schema' }) // 'TagSchema'` | ||
| */ | ||
| function pascalCase(text, { prefix = "", suffix = "" } = {}) { | ||
| return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true); | ||
| } | ||
| //#endregion | ||
| //#region ../../internals/utils/src/runtime.ts | ||
| /** | ||
| * Detects the JavaScript runtime executing the current process and exposes its name and version. | ||
| * | ||
| * Prefer the shared {@link runtime} instance over constructing your own. | ||
| */ | ||
| var Runtime = class { | ||
| /** | ||
| * `true` when the current process is running under Bun. | ||
| * | ||
| * Detection keys off the global `Bun` object rather than `process.versions`, | ||
| * because Bun polyfills `process.versions.node` for Node compatibility and would | ||
| * otherwise look like Node. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * if (runtime.isBun) { | ||
| * await Bun.write(path, data) | ||
| * } | ||
| * ``` | ||
| */ | ||
| get isBun() { | ||
| return typeof Bun !== "undefined"; | ||
| } | ||
| /** | ||
| * `true` when the current process is running under Deno. | ||
| */ | ||
| get isDeno() { | ||
| return typeof globalThis.Deno !== "undefined"; | ||
| } | ||
| /** | ||
| * `true` when the current process is running under Node. | ||
| * | ||
| * Bun and Deno are excluded first so a polyfilled `process` does not register as Node. | ||
| */ | ||
| get isNode() { | ||
| return !this.isBun && !this.isDeno && typeof process !== "undefined" && process.versions?.node != null; | ||
| } | ||
| /** | ||
| * Name of the runtime executing the current process. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * runtime.name // 'bun' when run with `bun kubb`, 'node' otherwise | ||
| * ``` | ||
| */ | ||
| get name() { | ||
| if (this.isBun) return "bun"; | ||
| if (this.isDeno) return "deno"; | ||
| return "node"; | ||
| } | ||
| /** | ||
| * Version of the active runtime, or an empty string when it cannot be read. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * runtime.version // '1.3.11' under Bun, '22.22.2' under Node | ||
| * ``` | ||
| */ | ||
| get version() { | ||
| if (this.isBun) return process.versions.bun ?? ""; | ||
| if (this.isDeno) return globalThis.Deno?.version?.deno ?? ""; | ||
| return process.versions?.node ?? ""; | ||
| } | ||
| }; | ||
| /** | ||
| * Shared {@link Runtime} instance describing the JavaScript runtime executing the current process. | ||
| */ | ||
| const runtime = new Runtime(); | ||
| //#endregion | ||
| //#region ../../internals/utils/src/fs.ts | ||
| /** | ||
| * Reads the file at `path` as a UTF-8 string. | ||
| * Uses `Bun.file().text()` when running under Bun, `fs.readFile` otherwise. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const source = await read('./src/Pet.ts') | ||
| * ``` | ||
| */ | ||
| async function read(path) { | ||
| if (runtime.isBun) return Bun.file(path).text(); | ||
| return (0, node_fs_promises.readFile)(path, { encoding: "utf8" }); | ||
| } | ||
| /** | ||
| * Writes `data` to `path`, trimming leading/trailing whitespace before saving. | ||
| * Skips the write when the trimmed content is empty or identical to what is already on disk. | ||
| * Creates any missing parent directories automatically. | ||
| * When `sanity` is `true`, re-reads the file after writing and throws if the content does not match. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * await write('./src/Pet.ts', source) // writes and returns trimmed content | ||
| * await write('./src/Pet.ts', source) // null — file unchanged | ||
| * await write('./src/Pet.ts', ' ') // null — empty content skipped | ||
| * ``` | ||
| */ | ||
| async function write(path, data, options = {}) { | ||
| const trimmed = data.trim(); | ||
| if (trimmed === "") return null; | ||
| const resolved = (0, node_path.resolve)(path); | ||
| if (runtime.isBun) { | ||
| const file = Bun.file(resolved); | ||
| if ((await file.exists() ? await file.text() : null) === trimmed) return null; | ||
| await Bun.write(resolved, trimmed); | ||
| return trimmed; | ||
| } | ||
| try { | ||
| if (await (0, node_fs_promises.readFile)(resolved, { encoding: "utf-8" }) === trimmed) return null; | ||
| } catch {} | ||
| await (0, node_fs_promises.mkdir)((0, node_path.dirname)(resolved), { recursive: true }); | ||
| await (0, node_fs_promises.writeFile)(resolved, trimmed, { encoding: "utf-8" }); | ||
| if (options.sanity) { | ||
| const savedData = await (0, node_fs_promises.readFile)(resolved, { encoding: "utf-8" }); | ||
| if (savedData !== trimmed) throw new Error(`Sanity check failed for ${path}\n\nData[${data.length}]:\n${data}\n\nSaved[${savedData.length}]:\n${savedData}\n`); | ||
| return savedData; | ||
| } | ||
| return trimmed; | ||
| } | ||
| /** | ||
| * Recursively removes `path`. Silently succeeds when `path` does not exist. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * await clean('./dist') | ||
| * ``` | ||
| */ | ||
| async function clean(path) { | ||
| return (0, node_fs_promises.rm)(path, { | ||
| recursive: true, | ||
| force: true | ||
| }); | ||
| } | ||
| /** | ||
| * Converts a filesystem path to use POSIX (`/`) separators. | ||
| * | ||
| * Most of the codebase compares and composes paths as strings (prefix matching, joining for | ||
| * import specifiers, splitting on `/`). On POSIX `path.resolve` already returns `/`-separated | ||
| * paths, but on Windows it returns `\`-separated paths, which breaks every such comparison. | ||
| * | ||
| * Routing every path that crosses a module boundary through `toPosixPath` keeps the rest of the | ||
| * code platform-agnostic. The conversion runs unconditionally so Windows-specific behavior is | ||
| * exercisable from POSIX CI. | ||
| * | ||
| * @example | ||
| * toPosixPath('C:\\repo\\src\\pet.ts') // 'C:/repo/src/pet.ts' | ||
| */ | ||
| function toPosixPath(filePath) { | ||
| return filePath.replaceAll("\\", "/"); | ||
| } | ||
| /** | ||
| * Builds a nested file path from a dotted name. Splits on dots that precede a letter | ||
| * (so version numbers embedded in operationIds like `v2025.0` stay intact), camelCases | ||
| * every earlier segment, applies `caseLast` to the final segment, and joins with `/`. | ||
| * | ||
| * Empty segments are dropped before joining. They arise when the name starts with a dot | ||
| * followed by a letter (e.g. `..Schema` splits into `['..', 'Schema']` and `'..'` cases to | ||
| * an empty string). Without this a leading `/` would form, which `path.resolve` reads as an | ||
| * absolute path, letting generated files escape the configured output directory. | ||
| * | ||
| * @example Nested path from a dotted name | ||
| * `toFilePath('pet.petId') // 'pet/petId'` | ||
| * | ||
| * @example PascalCase the final segment | ||
| * `toFilePath('pet.Pet', pascalCase) // 'pet/Pet'` | ||
| * | ||
| * @example Suffix applied to the final segment only | ||
| * `toFilePath('tag.tag', (part) => camelCase(part, { suffix: 'schema' })) // 'tag/tagSchema'` | ||
| */ | ||
| function toFilePath(name, caseLast = camelCase) { | ||
| const parts = name.split(/\.(?=[a-zA-Z])/); | ||
| return parts.map((part, i) => i === parts.length - 1 ? caseLast(part) : camelCase(part)).filter(Boolean).join("/"); | ||
| } | ||
| //#endregion | ||
| //#region src/constants.ts | ||
| /** | ||
| * Plugin `include` filter types that select operations directly. When one of these is set | ||
| * without a `schemaName` include, the generate phase pre-scans operations to compute the set | ||
| * of schemas they reach, so unreachable schemas can be pruned for that plugin. | ||
| */ | ||
| const OPERATION_FILTER_TYPES = new Set([ | ||
| "tag", | ||
| "operationId", | ||
| "path", | ||
| "method", | ||
| "contentType" | ||
| ]); | ||
| /** | ||
| * Stable codes Kubb attaches to a `Diagnostic`. Each maps to a known failure mode | ||
| * and stays stable so it can be referenced in tooling and (later) docs. Reference | ||
| * these instead of inlining the string at a throw site. | ||
| */ | ||
| const diagnosticCode = { | ||
| /** | ||
| * Fallback for an unstructured error with no specific code. | ||
| */ | ||
| unknown: "KUBB_UNKNOWN", | ||
| /** | ||
| * The `input.path` file or URL could not be read. | ||
| */ | ||
| inputNotFound: "KUBB_INPUT_NOT_FOUND", | ||
| /** | ||
| * An adapter was configured without an `input`. | ||
| */ | ||
| inputRequired: "KUBB_INPUT_REQUIRED", | ||
| /** | ||
| * A `$ref` (or equivalent reference) could not be resolved in the source document. | ||
| */ | ||
| refNotFound: "KUBB_REF_NOT_FOUND", | ||
| /** | ||
| * A server variable value is not allowed by its `enum`. | ||
| */ | ||
| invalidServerVariable: "KUBB_INVALID_SERVER_VARIABLE", | ||
| /** | ||
| * A required plugin is missing from the config. | ||
| */ | ||
| pluginNotFound: "KUBB_PLUGIN_NOT_FOUND", | ||
| /** | ||
| * A plugin threw while generating. | ||
| */ | ||
| pluginFailed: "KUBB_PLUGIN_FAILED", | ||
| /** | ||
| * A plugin reported a non-fatal warning through `ctx.warn`. | ||
| */ | ||
| pluginWarning: "KUBB_PLUGIN_WARNING", | ||
| /** | ||
| * A plugin reported an informational message through `ctx.info`. | ||
| */ | ||
| pluginInfo: "KUBB_PLUGIN_INFO", | ||
| /** | ||
| * A schema uses a `format` Kubb does not map to a specific type. Reserved for | ||
| * adapters to emit as a `warning`. | ||
| */ | ||
| unsupportedFormat: "KUBB_UNSUPPORTED_FORMAT", | ||
| /** | ||
| * A referenced schema or operation is marked `deprecated`. Reserved for adapters | ||
| * to emit as an `info`. | ||
| */ | ||
| deprecated: "KUBB_DEPRECATED", | ||
| /** | ||
| * An adapter is required but the config has none. The build cannot read the input | ||
| * without one. | ||
| */ | ||
| adapterRequired: "KUBB_ADAPTER_REQUIRED", | ||
| /** | ||
| * A resolved output path escapes the output directory, which can stem from a path | ||
| * traversal in the spec or a misconfigured `group.name`. | ||
| */ | ||
| pathTraversal: "KUBB_PATH_TRAVERSAL", | ||
| /** | ||
| * A plugin's options are invalid, for example `output.mode: 'file'` paired with a `group` option. | ||
| */ | ||
| invalidPluginOptions: "KUBB_INVALID_PLUGIN_OPTIONS", | ||
| /** | ||
| * A post-generate shell hook (`hooks.done`) exited with a failure. | ||
| */ | ||
| hookFailed: "KUBB_HOOK_FAILED", | ||
| /** | ||
| * The formatter pass over the generated files failed. | ||
| */ | ||
| formatFailed: "KUBB_FORMAT_FAILED", | ||
| /** | ||
| * The linter pass over the generated files failed. | ||
| */ | ||
| lintFailed: "KUBB_LINT_FAILED", | ||
| /** | ||
| * Not a failure. Carries a plugin's elapsed time, summed into the run total. | ||
| */ | ||
| performance: "KUBB_PERFORMANCE", | ||
| /** | ||
| * Not a failure. A newer Kubb version is available on npm. | ||
| */ | ||
| updateAvailable: "KUBB_UPDATE_AVAILABLE" | ||
| }; | ||
| //#endregion | ||
| //#region src/createStorage.ts | ||
| /** | ||
| * Defines a custom storage backend. The builder receives user options and | ||
| * returns a `Storage` implementation. Kubb ships with filesystem and in-memory | ||
| * storages. A custom backend writes generated files elsewhere, such as cloud | ||
| * storage or a database. | ||
| * | ||
| * @example In-memory storage (the built-in implementation) | ||
| * ```ts | ||
| * import { createStorage } from '@kubb/core' | ||
| * | ||
| * export const memoryStorage = createStorage(() => { | ||
| * const store = new Map<string, string>() | ||
| * | ||
| * return { | ||
| * name: 'memory', | ||
| * async hasItem(key) { | ||
| * return store.has(key) | ||
| * }, | ||
| * async getItem(key) { | ||
| * return store.get(key) ?? null | ||
| * }, | ||
| * async setItem(key, value) { | ||
| * store.set(key, value) | ||
| * }, | ||
| * async removeItem(key) { | ||
| * store.delete(key) | ||
| * }, | ||
| * async getKeys(base) { | ||
| * const keys = [...store.keys()] | ||
| * return base ? keys.filter((k) => k.startsWith(base)) : keys | ||
| * }, | ||
| * async clear(base) { | ||
| * if (!base) store.clear() | ||
| * }, | ||
| * } | ||
| * }) | ||
| * ``` | ||
| */ | ||
| function createStorage(build) { | ||
| return (options) => build(options ?? {}); | ||
| } | ||
| //#endregion | ||
| //#region src/FileManager.ts | ||
| function mergeFile(a, b) { | ||
| return { | ||
| ...a, | ||
| banner: b.banner, | ||
| footer: b.footer, | ||
| copy: b.copy ?? a.copy, | ||
| sources: a.sources.length ? b.sources.length ? [...a.sources, ...b.sources] : a.sources : b.sources, | ||
| imports: a.imports.length ? b.imports.length ? [...a.imports, ...b.imports] : a.imports : b.imports, | ||
| exports: a.exports.length ? b.exports.length ? [...a.exports, ...b.exports] : a.exports : b.exports | ||
| }; | ||
| } | ||
| function isIndexPath(path) { | ||
| return path.endsWith("/index.ts") || path === "index.ts"; | ||
| } | ||
| function compareFiles(a, b) { | ||
| const lenDiff = a.path.length - b.path.length; | ||
| if (lenDiff !== 0) return lenDiff; | ||
| const aIsIndex = isIndexPath(a.path); | ||
| const bIsIndex = isIndexPath(b.path); | ||
| if (aIsIndex && !bIsIndex) return 1; | ||
| if (!aIsIndex && bIsIndex) return -1; | ||
| return 0; | ||
| } | ||
| /** | ||
| * In-memory file store for generated files. Files sharing a `path` are merged | ||
| * (sources/imports/exports concatenated). The `files` getter is sorted by | ||
| * path length (barrel `index.ts` last within a bucket). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const manager = new FileManager() | ||
| * manager.upsert(myFile) | ||
| * manager.files // sorted view | ||
| * ``` | ||
| */ | ||
| var FileManager = class { | ||
| /** | ||
| * Subscribe to file-store changes. Listeners on `upsert` see each resolved file as it lands | ||
| * through `add` or `upsert`. | ||
| */ | ||
| hooks = new AsyncEventEmitter(); | ||
| #cache = /* @__PURE__ */ new Map(); | ||
| #sorted = null; | ||
| add(...files) { | ||
| return this.#store(files, false); | ||
| } | ||
| upsert(...files) { | ||
| return this.#store(files, true); | ||
| } | ||
| #store(files, mergeExisting) { | ||
| const batch = files.length > 1 ? this.#dedupe(files) : files; | ||
| const resolved = []; | ||
| for (const file of batch) { | ||
| const existing = this.#cache.get(file.path); | ||
| const merged = existing && mergeExisting ? _kubb_ast.ast.factory.createFile(mergeFile(existing, file)) : _kubb_ast.ast.factory.createFile(file); | ||
| this.#cache.set(merged.path, merged); | ||
| resolved.push(merged); | ||
| this.hooks.emit("upsert", merged); | ||
| } | ||
| if (resolved.length > 0) this.#sorted = null; | ||
| return resolved; | ||
| } | ||
| #dedupe(files) { | ||
| const seen = /* @__PURE__ */ new Map(); | ||
| for (const file of files) { | ||
| const prev = seen.get(file.path); | ||
| seen.set(file.path, prev ? mergeFile(prev, file) : file); | ||
| } | ||
| return [...seen.values()]; | ||
| } | ||
| getByPath(path) { | ||
| return this.#cache.get(path) ?? null; | ||
| } | ||
| deleteByPath(path) { | ||
| if (!this.#cache.delete(path)) return; | ||
| this.#sorted = null; | ||
| } | ||
| clear() { | ||
| this.#cache.clear(); | ||
| this.#sorted = null; | ||
| } | ||
| /** | ||
| * Releases all stored files and clears every `hooks` listener. Called by the core after | ||
| * `kubb:build:end`. | ||
| */ | ||
| dispose() { | ||
| this.clear(); | ||
| this.hooks.removeAll(); | ||
| } | ||
| [Symbol.dispose]() { | ||
| this.dispose(); | ||
| } | ||
| /** | ||
| * All stored files in stable sort order (shortest path first, barrel files | ||
| * last within a length bucket). Returns a cached view, do not mutate. | ||
| */ | ||
| get files() { | ||
| return this.#sorted ??= [...this.#cache.values()].sort(compareFiles); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/FileProcessor.ts | ||
| function joinSources(file) { | ||
| const sources = file.sources; | ||
| if (sources.length === 0) return ""; | ||
| const parts = []; | ||
| for (const source of sources) { | ||
| const text = (0, _kubb_ast_utils.extractStringsFromNodes)(source.nodes); | ||
| if (text) parts.push(text); | ||
| } | ||
| return parts.join("\n\n"); | ||
| } | ||
| async function parseCopy(file) { | ||
| let content; | ||
| try { | ||
| content = await read(file.copy); | ||
| } catch (err) { | ||
| throw new Error(`[kubb] Could not copy file into output: ${file.copy}`, { cause: err }); | ||
| } | ||
| return [ | ||
| file.banner, | ||
| content, | ||
| file.footer | ||
| ].filter((segment) => Boolean(segment)).map((segment) => segment.trimEnd()).join("\n"); | ||
| } | ||
| /** | ||
| * Turns `FileNode`s into source strings and writes them to storage. | ||
| * | ||
| * Two modes share the same instance. Stateless mode (`parse`, `stream`, `run`) just runs the | ||
| * conversion. Queue mode (`enqueue`, `flush`, `drain`) buffers files deduped by path and | ||
| * writes each batch through storage with up to `STREAM_FLUSH_EVERY` requests in flight. | ||
| * | ||
| * `flush` does not wait for its batch to finish, so dispatch can overlap with IO. The next | ||
| * `flush` or `drain` picks the in-flight batch up. `drain` blocks until everything has been | ||
| * written and is meant for the end of a build. | ||
| * | ||
| * To surface build-level hook signals (`kubb:files:processing:*` and friends) subscribe to | ||
| * `hooks` and re-emit on the kubb bus. | ||
| */ | ||
| var FileProcessor = class { | ||
| hooks = new AsyncEventEmitter(); | ||
| #parsers; | ||
| #storage; | ||
| #extension; | ||
| #pending = /* @__PURE__ */ new Map(); | ||
| #runningFlush = null; | ||
| constructor(options) { | ||
| this.#parsers = options.parsers ?? null; | ||
| this.#storage = options.storage; | ||
| this.#extension = options.extension ?? null; | ||
| } | ||
| /** | ||
| * Files waiting in the queue. | ||
| */ | ||
| get size() { | ||
| return this.#pending.size; | ||
| } | ||
| async parse(file) { | ||
| if (file.copy) return parseCopy(file); | ||
| const parsers = this.#parsers; | ||
| const parseExtName = this.#extension?.[file.extname] || void 0; | ||
| if (!parsers || !file.extname) return joinSources(file); | ||
| const parser = parsers.get(file.extname); | ||
| if (!parser) return joinSources(file); | ||
| return parser.parse(file, { extname: parseExtName }); | ||
| } | ||
| async *stream(files) { | ||
| const total = files.length; | ||
| if (total === 0) return; | ||
| let processed = 0; | ||
| for (const file of files) { | ||
| const source = await this.parse(file); | ||
| processed++; | ||
| yield { | ||
| file, | ||
| source, | ||
| processed, | ||
| total, | ||
| percentage: processed / total * 100 | ||
| }; | ||
| } | ||
| } | ||
| async run(files) { | ||
| await this.hooks.emit("start", files); | ||
| for await (const { file, source, processed, total, percentage } of this.stream(files)) await this.hooks.emit("update", { | ||
| file, | ||
| source, | ||
| processed, | ||
| percentage, | ||
| total | ||
| }); | ||
| await this.hooks.emit("end", files); | ||
| return files; | ||
| } | ||
| /** | ||
| * Adds a file to the next flush. A later `enqueue` for the same path replaces the previous | ||
| * entry, matching `FileManager.upsert`. Fires the `enqueue` event. | ||
| */ | ||
| enqueue(file) { | ||
| this.#pending.set(file.path, file); | ||
| this.hooks.emit("enqueue", file); | ||
| } | ||
| /** | ||
| * Starts processing the queued files. Waits for any previous flush to finish (so two | ||
| * batches never run together) and then returns without waiting for the new one. The next | ||
| * `flush` or `drain` picks up the in-flight task. | ||
| */ | ||
| async flush() { | ||
| if (this.#runningFlush) await this.#runningFlush; | ||
| if (this.#pending.size === 0) return; | ||
| const batch = [...this.#pending.values()]; | ||
| this.#pending.clear(); | ||
| this.#runningFlush = this.#processAndWrite(batch).finally(() => { | ||
| this.#runningFlush = null; | ||
| }); | ||
| } | ||
| /** | ||
| * Waits for the in-flight flush and writes any files still queued. Fires the `drain` event | ||
| * when both are done. | ||
| */ | ||
| async drain() { | ||
| if (this.#runningFlush) await this.#runningFlush; | ||
| if (this.#pending.size > 0) { | ||
| const batch = [...this.#pending.values()]; | ||
| this.#pending.clear(); | ||
| await this.#processAndWrite(batch); | ||
| } | ||
| await this.hooks.emit("drain"); | ||
| } | ||
| async #processAndWrite(files) { | ||
| const storage = this.#storage; | ||
| await this.hooks.emit("start", files); | ||
| const queue = []; | ||
| for await (const item of this.stream(files)) { | ||
| await this.hooks.emit("update", item); | ||
| if (item.source) { | ||
| queue.push(storage.setItem(item.file.path, item.source)); | ||
| if (queue.length >= 50) await Promise.all(queue.splice(0)); | ||
| } | ||
| } | ||
| await Promise.all(queue); | ||
| await this.hooks.emit("end", files); | ||
| } | ||
| /** | ||
| * Clears every listener and the pending queue. | ||
| */ | ||
| dispose() { | ||
| this.hooks.removeAll(); | ||
| this.#pending.clear(); | ||
| } | ||
| [Symbol.dispose]() { | ||
| this.dispose(); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region \0@oxc-project+runtime@0.135.0/helpers/esm/usingCtx.js | ||
| function _usingCtx() { | ||
| var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) { | ||
| var n = Error(); | ||
| return n.name = "SuppressedError", n.error = r, n.suppressed = e, n; | ||
| }; | ||
| var e = {}; | ||
| var n = []; | ||
| function using(r, e) { | ||
| if (null != e) { | ||
| if (Object(e) !== e) throw new TypeError("using declarations can only be used with objects, functions, null, or undefined."); | ||
| if (r) var o = e[Symbol.asyncDispose || Symbol["for"]("Symbol.asyncDispose")]; | ||
| if (void 0 === o && (o = e[Symbol.dispose || Symbol["for"]("Symbol.dispose")], r)) var t = o; | ||
| if ("function" != typeof o) throw new TypeError("Object is not disposable."); | ||
| t && (o = function o() { | ||
| try { | ||
| t.call(e); | ||
| } catch (r) { | ||
| return Promise.reject(r); | ||
| } | ||
| }), n.push({ | ||
| v: e, | ||
| d: o, | ||
| a: r | ||
| }); | ||
| } else r && n.push({ | ||
| d: e, | ||
| a: r | ||
| }); | ||
| return e; | ||
| } | ||
| return { | ||
| e, | ||
| u: using.bind(null, !1), | ||
| a: using.bind(null, !0), | ||
| d: function d() { | ||
| var o; | ||
| var t = this.e; | ||
| var s = 0; | ||
| function next() { | ||
| for (; o = n.pop();) try { | ||
| if (!o.a && 1 === s) return s = 0, n.push(o), Promise.resolve().then(next); | ||
| if (o.d) { | ||
| var r = o.d.call(o.v); | ||
| if (o.a) return s |= 2, Promise.resolve(r).then(next, err); | ||
| } else s |= 1; | ||
| } catch (r) { | ||
| return err(r); | ||
| } | ||
| if (1 === s) return t !== e ? Promise.reject(t) : Promise.resolve(); | ||
| if (t !== e) throw t; | ||
| } | ||
| function err(n) { | ||
| return t = t !== e ? new r(n, t) : n, next(); | ||
| } | ||
| return next(); | ||
| } | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region src/storages/memoryStorage.ts | ||
| /** | ||
| * In-memory storage driver. Useful for testing and dry-run scenarios where | ||
| * generated output should be captured without touching the filesystem. | ||
| * | ||
| * All data lives in a `Map` scoped to the storage instance and is discarded | ||
| * when the instance is garbage-collected. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { memoryStorage } from '@kubb/core' | ||
| * import { defineConfig } from 'kubb' | ||
| * | ||
| * export default defineConfig({ | ||
| * input: { path: './petStore.yaml' }, | ||
| * output: { path: './src/gen' }, | ||
| * storage: memoryStorage(), | ||
| * }) | ||
| * ``` | ||
| */ | ||
| const memoryStorage = createStorage(() => { | ||
| const store = /* @__PURE__ */ new Map(); | ||
| return { | ||
| name: "memory", | ||
| async hasItem(key) { | ||
| return store.has(key); | ||
| }, | ||
| async getItem(key) { | ||
| return store.get(key) ?? null; | ||
| }, | ||
| async setItem(key, value) { | ||
| store.set(key, value); | ||
| }, | ||
| async removeItem(key) { | ||
| store.delete(key); | ||
| }, | ||
| async getKeys(base) { | ||
| const keys = [...store.keys()]; | ||
| return base ? keys.filter((k) => k.startsWith(base)) : keys; | ||
| }, | ||
| async clear(base) { | ||
| if (!base) { | ||
| store.clear(); | ||
| return; | ||
| } | ||
| for (const key of store.keys()) if (key.startsWith(base)) store.delete(key); | ||
| } | ||
| }; | ||
| }); | ||
| //#endregion | ||
| Object.defineProperty(exports, "AsyncEventEmitter", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return AsyncEventEmitter; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "BuildError", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return BuildError; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "FileManager", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return FileManager; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "FileProcessor", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return FileProcessor; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "OPERATION_FILTER_TYPES", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return OPERATION_FILTER_TYPES; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "__name", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return __name; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "__toESM", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return __toESM; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "_usingCtx", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return _usingCtx; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "camelCase", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return camelCase; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "clean", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return clean; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "createStorage", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return createStorage; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "diagnosticCode", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return diagnosticCode; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "getErrorMessage", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return getErrorMessage; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "memoryStorage", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return memoryStorage; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "pascalCase", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return pascalCase; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "runtime", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return runtime; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "toFilePath", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return toFilePath; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "toPosixPath", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return toPosixPath; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "write", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return write; | ||
| } | ||
| }); | ||
| //# sourceMappingURL=memoryStorage-Bk7RHgGj.cjs.map |
| {"version":3,"file":"memoryStorage-Bk7RHgGj.cjs","names":["#emitter","NodeEventEmitter","#emitAll","#cache","#store","#dedupe","ast","#sorted","#parsers","#storage","#extension","#pending","#runningFlush","#processAndWrite"],"sources":["../../../internals/utils/src/errors.ts","../../../internals/utils/src/asyncEventEmitter.ts","../../../internals/utils/src/casing.ts","../../../internals/utils/src/runtime.ts","../../../internals/utils/src/fs.ts","../src/constants.ts","../src/createStorage.ts","../src/FileManager.ts","../src/FileProcessor.ts","../src/storages/memoryStorage.ts"],"sourcesContent":["/**\n * Thrown when one or more errors occur during a Kubb build.\n * Carries the full list of underlying errors on `errors`.\n *\n * @example\n * ```ts\n * throw new BuildError('Build failed', { errors: [err1, err2] })\n * ```\n */\nexport class BuildError extends Error {\n errors: Array<Error>\n\n constructor(message: string, options: { cause?: Error; errors: Array<Error> }) {\n super(message, { cause: options.cause })\n this.name = 'BuildError'\n this.errors = options.errors\n }\n}\n\n/**\n * Coerces an unknown thrown value to an `Error` instance.\n * Returns the value as-is when it is already an `Error`; otherwise wraps it with `String(value)`.\n *\n * @example\n * ```ts\n * try { ... } catch(err) {\n * throw new BuildError('Build failed', { cause: toError(err), errors: [] })\n * }\n * ```\n */\nexport function toError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value))\n}\n\n/**\n * Extracts a human-readable message from any thrown value.\n *\n * @example\n * ```ts\n * getErrorMessage(new Error('oops')) // 'oops'\n * getErrorMessage('plain string') // 'plain string'\n * ```\n */\nexport function getErrorMessage(value: unknown): string {\n return value instanceof Error ? value.message : String(value)\n}\n\n/**\n * Extracts the `.cause` of an `Error` as an `Error`, or `undefined` when absent or not an `Error`.\n *\n * @example\n * ```ts\n * const cause = toCause(buildError) // Error | undefined\n * ```\n */\nexport function toCause(error: Error): Error | undefined {\n return error.cause instanceof Error ? error.cause : undefined\n}\n","import { EventEmitter as NodeEventEmitter } from 'node:events'\nimport { toError } from './errors.ts'\n\n/**\n * A function that can be registered as an event listener, synchronous or async.\n */\ntype AsyncListener<TArgs extends Array<unknown>> = (...args: TArgs) => void | Promise<void>\n\n/**\n * Typed `EventEmitter` that awaits all async listeners before resolving.\n * Wraps Node's `EventEmitter` with full TypeScript event-map inference.\n *\n * @example\n * ```ts\n * const emitter = new AsyncEventEmitter<{ build: [name: string] }>()\n * emitter.on('build', async (name) => { console.log(name) })\n * await emitter.emit('build', 'petstore') // all listeners awaited\n * ```\n */\nexport class AsyncEventEmitter<TEvents extends { [K in keyof TEvents]: Array<unknown> }> {\n /**\n * Maximum number of listeners per event before Node emits a memory-leak warning.\n * @default 10\n */\n constructor(maxListener = 10) {\n this.#emitter.setMaxListeners(maxListener)\n }\n\n #emitter = new NodeEventEmitter()\n\n /**\n * Emits `eventName` and awaits all registered listeners sequentially.\n * Throws if any listener rejects, wrapping the cause with the event name and serialized arguments.\n *\n * @example\n * ```ts\n * await emitter.emit('build', 'petstore')\n * ```\n */\n emit<TEventName extends keyof TEvents & string>(eventName: TEventName, ...eventArgs: TEvents[TEventName]): Promise<void> | void {\n const listeners = this.#emitter.listeners(eventName) as Array<AsyncListener<TEvents[TEventName]>>\n\n if (listeners.length === 0) {\n return\n }\n\n return this.#emitAll(eventName, listeners, eventArgs)\n }\n\n async #emitAll<TEventName extends keyof TEvents & string>(\n eventName: TEventName,\n listeners: Array<AsyncListener<TEvents[TEventName]>>,\n eventArgs: TEvents[TEventName],\n ): Promise<void> {\n for (const listener of listeners) {\n try {\n await listener(...eventArgs)\n } catch (err) {\n let serializedArgs: string\n try {\n serializedArgs = JSON.stringify(eventArgs)\n } catch {\n serializedArgs = String(eventArgs)\n }\n throw new Error(`Error in async listener for \"${eventName}\" with eventArgs ${serializedArgs}`, { cause: toError(err) })\n }\n }\n }\n\n /**\n * Registers a persistent listener for `eventName`.\n *\n * @example\n * ```ts\n * emitter.on('build', async (name) => { console.log(name) })\n * ```\n */\n on<TEventName extends keyof TEvents & string>(eventName: TEventName, handler: AsyncListener<TEvents[TEventName]>): void {\n this.#emitter.on(eventName, handler as AsyncListener<Array<unknown>>)\n }\n\n /**\n * Removes a previously registered listener.\n *\n * @example\n * ```ts\n * emitter.off('build', handler)\n * ```\n */\n off<TEventName extends keyof TEvents & string>(eventName: TEventName, handler: AsyncListener<TEvents[TEventName]>): void {\n this.#emitter.off(eventName, handler as AsyncListener<Array<unknown>>)\n }\n\n /**\n * Returns the number of listeners registered for `eventName`.\n *\n * @example\n * ```ts\n * emitter.on('build', handler)\n * emitter.listenerCount('build') // 1\n * ```\n */\n listenerCount<TEventName extends keyof TEvents & string>(eventName: TEventName): number {\n return this.#emitter.listenerCount(eventName)\n }\n\n /**\n * Raises or lowers the per-event listener ceiling before Node warns about a memory leak.\n * Set this above the expected listener count when many listeners attach by design.\n *\n * @example\n * ```ts\n * emitter.setMaxListeners(40)\n * ```\n */\n setMaxListeners(max: number): void {\n this.#emitter.setMaxListeners(max)\n }\n\n /**\n * Removes all listeners from every event channel.\n *\n * @example\n * ```ts\n * emitter.removeAll()\n * ```\n */\n removeAll(): void {\n this.#emitter.removeAllListeners()\n }\n}\n","type Options = {\n /**\n * Text prepended before casing is applied.\n */\n prefix?: string\n /**\n * Text appended before casing is applied.\n */\n suffix?: string\n}\n\n/**\n * Shared implementation for camelCase and PascalCase conversion.\n * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)\n * and capitalizes each word according to `pascal`.\n *\n * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.\n */\nfunction toCamelOrPascal(text: string, pascal: boolean): string {\n return text\n .trim()\n .replace(/([a-z\\d])([A-Z])/g, '$1 $2')\n .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')\n .replace(/(\\d)([a-z])/g, '$1 $2')\n .split(/[\\s\\-_./\\\\:]+/)\n .filter(Boolean)\n .map((word, i) => {\n if (word.length > 1 && word === word.toUpperCase()) return word\n const head = i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()\n return head + word.slice(1)\n })\n .join('')\n .replace(/[^a-zA-Z0-9]/g, '')\n}\n\n/**\n * Converts `text` to camelCase.\n *\n * @example Word boundaries\n * `camelCase('hello-world') // 'helloWorld'`\n *\n * @example With a prefix\n * `camelCase('tag', { prefix: 'create' }) // 'createTag'`\n */\nexport function camelCase(text: string, { prefix = '', suffix = '' }: Options = {}): string {\n return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false)\n}\n\n/**\n * Converts `text` to PascalCase.\n *\n * @example Word boundaries\n * `pascalCase('hello-world') // 'HelloWorld'`\n *\n * @example With a suffix\n * `pascalCase('tag', { suffix: 'schema' }) // 'TagSchema'`\n */\nexport function pascalCase(text: string, { prefix = '', suffix = '' }: Options = {}): string {\n return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true)\n}\n","/**\n * Name of the JavaScript runtime executing the current process.\n */\ntype RuntimeName = 'bun' | 'deno' | 'node'\n\n/**\n * Detects the JavaScript runtime executing the current process and exposes its name and version.\n *\n * Prefer the shared {@link runtime} instance over constructing your own.\n */\nclass Runtime {\n /**\n * `true` when the current process is running under Bun.\n *\n * Detection keys off the global `Bun` object rather than `process.versions`,\n * because Bun polyfills `process.versions.node` for Node compatibility and would\n * otherwise look like Node.\n *\n * @example\n * ```ts\n * if (runtime.isBun) {\n * await Bun.write(path, data)\n * }\n * ```\n */\n get isBun(): boolean {\n return typeof Bun !== 'undefined'\n }\n\n /**\n * `true` when the current process is running under Deno.\n */\n get isDeno(): boolean {\n return typeof (globalThis as { Deno?: unknown }).Deno !== 'undefined'\n }\n\n /**\n * `true` when the current process is running under Node.\n *\n * Bun and Deno are excluded first so a polyfilled `process` does not register as Node.\n */\n get isNode(): boolean {\n return !this.isBun && !this.isDeno && typeof process !== 'undefined' && process.versions?.node != null\n }\n\n /**\n * Name of the runtime executing the current process.\n *\n * @example\n * ```ts\n * runtime.name // 'bun' when run with `bun kubb`, 'node' otherwise\n * ```\n */\n get name(): RuntimeName {\n if (this.isBun) return 'bun'\n if (this.isDeno) return 'deno'\n\n return 'node'\n }\n\n /**\n * Version of the active runtime, or an empty string when it cannot be read.\n *\n * @example\n * ```ts\n * runtime.version // '1.3.11' under Bun, '22.22.2' under Node\n * ```\n */\n get version(): string {\n if (this.isBun) return process.versions.bun ?? ''\n if (this.isDeno) return (globalThis as { Deno?: { version?: { deno?: string } } }).Deno?.version?.deno ?? ''\n\n return process.versions?.node ?? ''\n }\n}\n\n/**\n * Shared {@link Runtime} instance describing the JavaScript runtime executing the current process.\n */\nexport const runtime = new Runtime()\n","import { existsSync } from 'node:fs'\nimport { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises'\nimport { dirname, join, posix, resolve } from 'node:path'\nimport { camelCase } from './casing.ts'\nimport { runtime } from './runtime.ts'\n\n/**\n * Walks up the directory tree from `cwd` (defaults to `process.cwd()`) and\n * returns the absolute path of the nearest `package.json`, or `null` when none\n * is found before reaching the filesystem root.\n *\n * @example\n * ```ts\n * const pkgPath = findPackageJSON('/home/user/project/src') // '/home/user/project/package.json'\n * ```\n */\nexport function findPackageJSON(cwd?: string): string | null {\n let dir = cwd ? resolve(cwd) : process.cwd()\n while (true) {\n const pkgPath = join(dir, 'package.json')\n if (existsSync(pkgPath)) return pkgPath\n const parent = dirname(dir)\n if (parent === dir) return null\n dir = parent\n }\n}\n\n/**\n * Converts all backslashes to forward slashes.\n * Extended-length Windows paths (`\\\\?\\...`) are left unchanged.\n */\nfunction toSlash(p: string): string {\n if (p.startsWith('\\\\\\\\?\\\\')) return p\n\n return p.replaceAll('\\\\', '/')\n}\n\n/**\n * Returns the relative path from `rootDir` to `filePath`, always using forward slashes\n * and prefixed with `./` when not already traversing upward.\n *\n * @example\n * ```ts\n * getRelativePath('/src/components', '/src/components/Button.tsx') // './Button.tsx'\n * getRelativePath('/src/components', '/src/utils/helpers.ts') // '../utils/helpers.ts'\n * ```\n */\nexport function getRelativePath(rootDir?: string | null, filePath?: string | null): string {\n if (!rootDir || !filePath) {\n throw new Error(`Root and file should be filled in when retrieving the relativePath, ${rootDir || ''} ${filePath || ''}`)\n }\n\n const relativePath = posix.relative(toSlash(rootDir), toSlash(filePath))\n\n return relativePath.startsWith('../') ? relativePath : `./${relativePath}`\n}\n\n/**\n * Resolves to `true` when the file or directory at `path` exists.\n * Uses `Bun.file().exists()` when running under Bun, `fs.access` otherwise.\n *\n * @example\n * ```ts\n * if (await exists('./kubb.config.ts')) {\n * const content = await read('./kubb.config.ts')\n * }\n * ```\n */\nexport async function exists(path: string): Promise<boolean> {\n if (runtime.isBun) {\n return Bun.file(path).exists()\n }\n return access(path).then(\n () => true,\n () => false,\n )\n}\n\n/**\n * Reads the file at `path` as a UTF-8 string.\n * Uses `Bun.file().text()` when running under Bun, `fs.readFile` otherwise.\n *\n * @example\n * ```ts\n * const source = await read('./src/Pet.ts')\n * ```\n */\nexport async function read(path: string): Promise<string> {\n if (runtime.isBun) {\n return Bun.file(path).text()\n }\n return readFile(path, { encoding: 'utf8' })\n}\n\ntype WriteOptions = {\n /**\n * When `true`, re-reads the file immediately after writing and throws if the\n * content does not match — useful for catching write failures on unreliable file systems.\n */\n sanity?: boolean\n}\n\n/**\n * Writes `data` to `path`, trimming leading/trailing whitespace before saving.\n * Skips the write when the trimmed content is empty or identical to what is already on disk.\n * Creates any missing parent directories automatically.\n * When `sanity` is `true`, re-reads the file after writing and throws if the content does not match.\n *\n * @example\n * ```ts\n * await write('./src/Pet.ts', source) // writes and returns trimmed content\n * await write('./src/Pet.ts', source) // null — file unchanged\n * await write('./src/Pet.ts', ' ') // null — empty content skipped\n * ```\n */\nexport async function write(path: string, data: string, options: WriteOptions = {}): Promise<string | null> {\n const trimmed = data.trim()\n if (trimmed === '') return null\n\n const resolved = resolve(path)\n\n if (runtime.isBun) {\n const file = Bun.file(resolved)\n const oldContent = (await file.exists()) ? await file.text() : null\n if (oldContent === trimmed) return null\n await Bun.write(resolved, trimmed)\n return trimmed\n }\n\n try {\n const oldContent = await readFile(resolved, { encoding: 'utf-8' })\n if (oldContent === trimmed) return null\n } catch {\n /* file doesn't exist yet */\n }\n\n await mkdir(dirname(resolved), { recursive: true })\n await writeFile(resolved, trimmed, { encoding: 'utf-8' })\n\n if (options.sanity) {\n const savedData = await readFile(resolved, { encoding: 'utf-8' })\n if (savedData !== trimmed) {\n throw new Error(`Sanity check failed for ${path}\\n\\nData[${data.length}]:\\n${data}\\n\\nSaved[${savedData.length}]:\\n${savedData}\\n`)\n }\n return savedData\n }\n\n return trimmed\n}\n\n/**\n * Recursively removes `path`. Silently succeeds when `path` does not exist.\n *\n * @example\n * ```ts\n * await clean('./dist')\n * ```\n */\nexport async function clean(path: string): Promise<void> {\n return rm(path, { recursive: true, force: true })\n}\n\n/**\n * Converts a filesystem path to use POSIX (`/`) separators.\n *\n * Most of the codebase compares and composes paths as strings (prefix matching, joining for\n * import specifiers, splitting on `/`). On POSIX `path.resolve` already returns `/`-separated\n * paths, but on Windows it returns `\\`-separated paths, which breaks every such comparison.\n *\n * Routing every path that crosses a module boundary through `toPosixPath` keeps the rest of the\n * code platform-agnostic. The conversion runs unconditionally so Windows-specific behavior is\n * exercisable from POSIX CI.\n *\n * @example\n * toPosixPath('C:\\\\repo\\\\src\\\\pet.ts') // 'C:/repo/src/pet.ts'\n */\nexport function toPosixPath(filePath: string): string {\n return filePath.replaceAll('\\\\', '/')\n}\n\n/**\n * Strips the file extension from a path or file name.\n * Only removes the last `.ext` segment when the dot is not part of a directory name.\n *\n * @example\n * trimExtName('petStore.ts') // 'petStore'\n * trimExtName('/src/models/pet.ts') // '/src/models/pet'\n * trimExtName('/project.v2/gen/pet.ts') // '/project.v2/gen/pet'\n * trimExtName('noExtension') // 'noExtension'\n */\nexport function trimExtName(text: string): string {\n const dotIndex = text.lastIndexOf('.')\n if (dotIndex > 0 && !text.includes('/', dotIndex)) {\n return text.slice(0, dotIndex)\n }\n return text\n}\n\n/**\n * Builds a nested file path from a dotted name. Splits on dots that precede a letter\n * (so version numbers embedded in operationIds like `v2025.0` stay intact), camelCases\n * every earlier segment, applies `caseLast` to the final segment, and joins with `/`.\n *\n * Empty segments are dropped before joining. They arise when the name starts with a dot\n * followed by a letter (e.g. `..Schema` splits into `['..', 'Schema']` and `'..'` cases to\n * an empty string). Without this a leading `/` would form, which `path.resolve` reads as an\n * absolute path, letting generated files escape the configured output directory.\n *\n * @example Nested path from a dotted name\n * `toFilePath('pet.petId') // 'pet/petId'`\n *\n * @example PascalCase the final segment\n * `toFilePath('pet.Pet', pascalCase) // 'pet/Pet'`\n *\n * @example Suffix applied to the final segment only\n * `toFilePath('tag.tag', (part) => camelCase(part, { suffix: 'schema' })) // 'tag/tagSchema'`\n */\nexport function toFilePath(name: string, caseLast: (part: string) => string = camelCase): string {\n const parts = name.split(/\\.(?=[a-zA-Z])/)\n return parts\n .map((part, i) => (i === parts.length - 1 ? caseLast(part) : camelCase(part)))\n .filter(Boolean)\n .join('/')\n}\n","/**\n * Number of file writes to batch in parallel during `flushPendingFiles`.\n */\nexport const STREAM_FLUSH_EVERY = 50\n\n/**\n * Maximum number of █ characters in a plugin timing bar.\n */\nexport const SUMMARY_MAX_BAR_LENGTH = 10 as const\n\n/**\n * Divides elapsed milliseconds into bar-length units (1 block per 100 ms).\n */\nexport const SUMMARY_TIME_SCALE_DIVISOR = 100 as const\n\n/**\n * How many schema/operation nodes to generate between write flushes. Flushing as generation runs\n * lets writes reach disk before the build finishes, rather than queuing them all to the end.\n */\nexport const GENERATE_FLUSH_EVERY = 8\n\n/**\n * Upper bound of hook listeners a single plugin can add to one event (its schema, operation,\n * and operations generators, plus lifecycle hooks). Used to size the hooks emitter's\n * max-listener ceiling so a multi-generator plugin set does not trip Node's leak warning.\n */\nexport const HOOK_LISTENERS_PER_PLUGIN = 4\n\n/**\n * Plugin `include` filter types that select operations directly. When one of these is set\n * without a `schemaName` include, the generate phase pre-scans operations to compute the set\n * of schemas they reach, so unreachable schemas can be pruned for that plugin.\n */\nexport const OPERATION_FILTER_TYPES: ReadonlySet<string> = new Set(['tag', 'operationId', 'path', 'method', 'contentType'])\n\n/**\n * Stable codes Kubb attaches to a `Diagnostic`. Each maps to a known failure mode\n * and stays stable so it can be referenced in tooling and (later) docs. Reference\n * these instead of inlining the string at a throw site.\n */\nexport const diagnosticCode = {\n /**\n * Fallback for an unstructured error with no specific code.\n */\n unknown: 'KUBB_UNKNOWN',\n /**\n * The `input.path` file or URL could not be read.\n */\n inputNotFound: 'KUBB_INPUT_NOT_FOUND',\n /**\n * An adapter was configured without an `input`.\n */\n inputRequired: 'KUBB_INPUT_REQUIRED',\n /**\n * A `$ref` (or equivalent reference) could not be resolved in the source document.\n */\n refNotFound: 'KUBB_REF_NOT_FOUND',\n /**\n * A server variable value is not allowed by its `enum`.\n */\n invalidServerVariable: 'KUBB_INVALID_SERVER_VARIABLE',\n /**\n * A required plugin is missing from the config.\n */\n pluginNotFound: 'KUBB_PLUGIN_NOT_FOUND',\n /**\n * A plugin threw while generating.\n */\n pluginFailed: 'KUBB_PLUGIN_FAILED',\n /**\n * A plugin reported a non-fatal warning through `ctx.warn`.\n */\n pluginWarning: 'KUBB_PLUGIN_WARNING',\n /**\n * A plugin reported an informational message through `ctx.info`.\n */\n pluginInfo: 'KUBB_PLUGIN_INFO',\n /**\n * A schema uses a `format` Kubb does not map to a specific type. Reserved for\n * adapters to emit as a `warning`.\n */\n unsupportedFormat: 'KUBB_UNSUPPORTED_FORMAT',\n /**\n * A referenced schema or operation is marked `deprecated`. Reserved for adapters\n * to emit as an `info`.\n */\n deprecated: 'KUBB_DEPRECATED',\n /**\n * An adapter is required but the config has none. The build cannot read the input\n * without one.\n */\n adapterRequired: 'KUBB_ADAPTER_REQUIRED',\n /**\n * A resolved output path escapes the output directory, which can stem from a path\n * traversal in the spec or a misconfigured `group.name`.\n */\n pathTraversal: 'KUBB_PATH_TRAVERSAL',\n /**\n * A plugin's options are invalid, for example `output.mode: 'file'` paired with a `group` option.\n */\n invalidPluginOptions: 'KUBB_INVALID_PLUGIN_OPTIONS',\n /**\n * A post-generate shell hook (`hooks.done`) exited with a failure.\n */\n hookFailed: 'KUBB_HOOK_FAILED',\n /**\n * The formatter pass over the generated files failed.\n */\n formatFailed: 'KUBB_FORMAT_FAILED',\n /**\n * The linter pass over the generated files failed.\n */\n lintFailed: 'KUBB_LINT_FAILED',\n /**\n * Not a failure. Carries a plugin's elapsed time, summed into the run total.\n */\n performance: 'KUBB_PERFORMANCE',\n /**\n * Not a failure. A newer Kubb version is available on npm.\n */\n updateAvailable: 'KUBB_UPDATE_AVAILABLE',\n} as const\n\n/**\n * Union of the stable {@link diagnosticCode} values.\n */\nexport type DiagnosticCode = (typeof diagnosticCode)[keyof typeof diagnosticCode]\n","/**\n * Backend that persists generated files. Kubb ships with `fsStorage` (writes\n * to disk) and `memoryStorage` (keeps everything in RAM). Implement this\n * interface to write somewhere else, such as S3 or a database.\n */\nexport type Storage = {\n /**\n * Identifier used in logs and diagnostics (`'fs'`, `'memory'`, `'s3'`).\n */\n readonly name: string\n /**\n * Returns `true` when an entry for `key` exists.\n */\n hasItem(key: string): Promise<boolean>\n /**\n * Reads the stored string. Returns `null` when the key is missing.\n */\n getItem(key: string): Promise<string | null>\n /**\n * Stores `value` under `key`, creating any required structure (directories,\n * buckets, ...).\n */\n setItem(key: string, value: string): Promise<void>\n /**\n * Deletes the entry for `key`. No-op when the key does not exist.\n */\n removeItem(key: string): Promise<void>\n /**\n * Returns every key. Pass `base` to filter to keys starting with that prefix.\n */\n getKeys(base?: string): Promise<Array<string>>\n /**\n * Removes every entry. Pass `base` to scope the wipe to a key prefix.\n */\n clear(base?: string): Promise<void>\n /**\n * Optional teardown hook for a backend to flush buffers, close connections,\n * or release file locks.\n */\n dispose?(): Promise<void>\n}\n\n/**\n * Defines a custom storage backend. The builder receives user options and\n * returns a `Storage` implementation. Kubb ships with filesystem and in-memory\n * storages. A custom backend writes generated files elsewhere, such as cloud\n * storage or a database.\n *\n * @example In-memory storage (the built-in implementation)\n * ```ts\n * import { createStorage } from '@kubb/core'\n *\n * export const memoryStorage = createStorage(() => {\n * const store = new Map<string, string>()\n *\n * return {\n * name: 'memory',\n * async hasItem(key) {\n * return store.has(key)\n * },\n * async getItem(key) {\n * return store.get(key) ?? null\n * },\n * async setItem(key, value) {\n * store.set(key, value)\n * },\n * async removeItem(key) {\n * store.delete(key)\n * },\n * async getKeys(base) {\n * const keys = [...store.keys()]\n * return base ? keys.filter((k) => k.startsWith(base)) : keys\n * },\n * async clear(base) {\n * if (!base) store.clear()\n * },\n * }\n * })\n * ```\n */\nexport function createStorage<TOptions = Record<string, never>>(build: (options: TOptions) => Storage): (options?: TOptions) => Storage {\n return (options) => build(options ?? ({} as TOptions))\n}\n","import { AsyncEventEmitter } from '@internals/utils'\nimport { ast, type FileNode } from '@kubb/ast'\n\n/**\n * Hooks fired by a `FileManager`.\n *\n * - `upsert` fires once per resolved file added through `add` or `upsert`.\n */\nexport type FileManagerHooks = {\n upsert: [file: FileNode]\n}\n\nfunction mergeFile<TMeta extends object = object>(a: FileNode<TMeta>, b: FileNode<TMeta>): FileNode<TMeta> {\n return {\n ...a,\n // Incoming file (b) takes precedence for banner/footer so a barrel file (whose\n // banner/footer the barrel plugin resolves last) wins over a plugin-generated\n // file at the same path.\n banner: b.banner,\n footer: b.footer,\n // A verbatim-copy file cannot be merged with rendered content; the incoming `copy` wins.\n copy: b.copy ?? a.copy,\n sources: a.sources.length ? (b.sources.length ? [...a.sources, ...b.sources] : a.sources) : b.sources,\n imports: a.imports.length ? (b.imports.length ? [...a.imports, ...b.imports] : a.imports) : b.imports,\n exports: a.exports.length ? (b.exports.length ? [...a.exports, ...b.exports] : a.exports) : b.exports,\n }\n}\n\nfunction isIndexPath(path: string): boolean {\n return path.endsWith('/index.ts') || path === 'index.ts'\n}\n\n// Sort order: shortest path first. Within a length bucket, index.ts barrels last.\nfunction compareFiles(a: FileNode, b: FileNode): number {\n const lenDiff = a.path.length - b.path.length\n if (lenDiff !== 0) return lenDiff\n const aIsIndex = isIndexPath(a.path)\n const bIsIndex = isIndexPath(b.path)\n if (aIsIndex && !bIsIndex) return 1\n if (!aIsIndex && bIsIndex) return -1\n return 0\n}\n\n/**\n * In-memory file store for generated files. Files sharing a `path` are merged\n * (sources/imports/exports concatenated). The `files` getter is sorted by\n * path length (barrel `index.ts` last within a bucket).\n *\n * @example\n * ```ts\n * const manager = new FileManager()\n * manager.upsert(myFile)\n * manager.files // sorted view\n * ```\n */\nexport class FileManager {\n /**\n * Subscribe to file-store changes. Listeners on `upsert` see each resolved file as it lands\n * through `add` or `upsert`.\n */\n readonly hooks = new AsyncEventEmitter<FileManagerHooks>()\n readonly #cache = new Map<string, FileNode>()\n // Cached sorted view. Null means stale and rebuilt lazily on next `files` read.\n // Nulled (not mutated) on every write so callers holding a prior reference keep\n // their snapshot. `dispose()` must not silently empty an array the consumer\n // already holds.\n #sorted: Array<FileNode> | null = null\n\n add(...files: Array<FileNode>): Array<FileNode> {\n return this.#store(files, false)\n }\n\n upsert(...files: Array<FileNode>): Array<FileNode> {\n return this.#store(files, true)\n }\n\n #store(files: ReadonlyArray<FileNode>, mergeExisting: boolean): Array<FileNode> {\n const batch = files.length > 1 ? this.#dedupe(files) : files\n const resolved: Array<FileNode> = []\n\n for (const file of batch) {\n const existing = this.#cache.get(file.path)\n const merged = existing && mergeExisting ? ast.factory.createFile(mergeFile(existing, file)) : ast.factory.createFile(file)\n this.#cache.set(merged.path, merged)\n resolved.push(merged)\n this.hooks.emit('upsert', merged)\n }\n\n if (resolved.length > 0) this.#sorted = null\n return resolved\n }\n\n // Merges same-path entries within a batch so the cache update loop stays\n // uniform. Only called for multi-file batches.\n #dedupe(files: ReadonlyArray<FileNode>): Array<FileNode> {\n const seen = new Map<string, FileNode>()\n for (const file of files) {\n const prev = seen.get(file.path)\n seen.set(file.path, prev ? mergeFile(prev, file) : file)\n }\n return [...seen.values()]\n }\n\n getByPath(path: string): FileNode | null {\n return this.#cache.get(path) ?? null\n }\n\n deleteByPath(path: string): void {\n if (!this.#cache.delete(path)) return\n this.#sorted = null\n }\n\n clear(): void {\n this.#cache.clear()\n this.#sorted = null\n }\n\n /**\n * Releases all stored files and clears every `hooks` listener. Called by the core after\n * `kubb:build:end`.\n */\n dispose(): void {\n this.clear()\n this.hooks.removeAll()\n }\n\n [Symbol.dispose](): void {\n this.dispose()\n }\n\n /**\n * All stored files in stable sort order (shortest path first, barrel files\n * last within a length bucket). Returns a cached view, do not mutate.\n */\n get files(): Array<FileNode> {\n return (this.#sorted ??= [...this.#cache.values()].sort(compareFiles))\n }\n}\n","import { AsyncEventEmitter, read } from '@internals/utils'\nimport type { CodeNode, FileNode } from '@kubb/ast'\nimport { extractStringsFromNodes } from '@kubb/ast/utils'\nimport { STREAM_FLUSH_EVERY } from './constants.ts'\nimport type { Storage } from './createStorage.ts'\nimport type { Parser } from './defineParser.ts'\n\n/**\n * Hooks fired by a `FileProcessor`.\n *\n * - `start` opens a batch, from `run` or a queue flush.\n * - `update` fires once per file as it is converted.\n * - `end` closes a batch.\n * - `enqueue` fires for every `enqueue` call.\n * - `drain` fires when `drain()` empties the queue with no in-flight batch left.\n */\nexport type FileProcessorHooks = {\n start: [files: Array<FileNode>]\n update: [params: { file: FileNode; source?: string; processed: number; total: number; percentage: number }]\n end: [files: Array<FileNode>]\n enqueue: [file: FileNode]\n drain: []\n}\n\n/**\n * Per-file progress record yielded by `stream` and surfaced through the `update` event.\n */\nexport type ParsedFile = {\n file: FileNode\n source: string\n processed: number\n total: number\n percentage: number\n}\n\ntype FileProcessorOptions = {\n /**\n * Storage destination for queued writes.\n */\n storage: Storage\n /**\n * Parsers indexed by file extension.\n */\n parsers?: Map<FileNode['extname'], Parser>\n /**\n * Output extname per source extname, applied during conversion.\n */\n extension?: Record<FileNode['extname'], FileNode['extname'] | ''>\n}\n\nfunction joinSources(file: FileNode): string {\n const sources = file.sources\n if (sources.length === 0) return ''\n const parts: Array<string> = []\n for (const source of sources) {\n const text = extractStringsFromNodes(source.nodes as Array<CodeNode>)\n if (text) parts.push(text)\n }\n return parts.join('\\n\\n')\n}\n\nasync function parseCopy(file: FileNode): Promise<string> {\n let content: string\n try {\n content = await read(file.copy as string)\n } catch (err) {\n throw new Error(`[kubb] Could not copy file into output: ${file.copy}`, { cause: err })\n }\n\n return [file.banner, content, file.footer]\n .filter((segment): segment is string => Boolean(segment))\n .map((segment) => segment.trimEnd())\n .join('\\n')\n}\n\n/**\n * Turns `FileNode`s into source strings and writes them to storage.\n *\n * Two modes share the same instance. Stateless mode (`parse`, `stream`, `run`) just runs the\n * conversion. Queue mode (`enqueue`, `flush`, `drain`) buffers files deduped by path and\n * writes each batch through storage with up to `STREAM_FLUSH_EVERY` requests in flight.\n *\n * `flush` does not wait for its batch to finish, so dispatch can overlap with IO. The next\n * `flush` or `drain` picks the in-flight batch up. `drain` blocks until everything has been\n * written and is meant for the end of a build.\n *\n * To surface build-level hook signals (`kubb:files:processing:*` and friends) subscribe to\n * `hooks` and re-emit on the kubb bus.\n */\nexport class FileProcessor {\n readonly hooks = new AsyncEventEmitter<FileProcessorHooks>()\n readonly #parsers: Map<FileNode['extname'], Parser> | null\n readonly #storage: Storage\n readonly #extension: Record<FileNode['extname'], FileNode['extname'] | ''> | null\n readonly #pending = new Map<string, FileNode>()\n #runningFlush: Promise<void> | null = null\n\n constructor(options: FileProcessorOptions) {\n this.#parsers = options.parsers ?? null\n this.#storage = options.storage\n this.#extension = options.extension ?? null\n }\n\n /**\n * Files waiting in the queue.\n */\n get size(): number {\n return this.#pending.size\n }\n\n async parse(file: FileNode): Promise<string> {\n if (file.copy) {\n return parseCopy(file)\n }\n\n const parsers = this.#parsers\n const parseExtName = this.#extension?.[file.extname] || undefined\n\n if (!parsers || !file.extname) {\n return joinSources(file)\n }\n\n const parser = parsers.get(file.extname)\n\n if (!parser) {\n return joinSources(file)\n }\n\n return parser.parse(file, { extname: parseExtName })\n }\n\n async *stream(files: ReadonlyArray<FileNode>): AsyncGenerator<ParsedFile> {\n const total = files.length\n if (total === 0) return\n\n let processed = 0\n for (const file of files) {\n const source = await this.parse(file)\n processed++\n\n yield { file, source, processed, total, percentage: (processed / total) * 100 }\n }\n }\n\n async run(files: Array<FileNode>): Promise<Array<FileNode>> {\n await this.hooks.emit('start', files)\n\n for await (const { file, source, processed, total, percentage } of this.stream(files)) {\n await this.hooks.emit('update', { file, source, processed, percentage, total })\n }\n\n await this.hooks.emit('end', files)\n\n return files\n }\n\n /**\n * Adds a file to the next flush. A later `enqueue` for the same path replaces the previous\n * entry, matching `FileManager.upsert`. Fires the `enqueue` event.\n */\n enqueue(file: FileNode): void {\n this.#pending.set(file.path, file)\n this.hooks.emit('enqueue', file)\n }\n\n /**\n * Starts processing the queued files. Waits for any previous flush to finish (so two\n * batches never run together) and then returns without waiting for the new one. The next\n * `flush` or `drain` picks up the in-flight task.\n */\n async flush(): Promise<void> {\n if (this.#runningFlush) await this.#runningFlush\n if (this.#pending.size === 0) return\n\n const batch = [...this.#pending.values()]\n this.#pending.clear()\n\n this.#runningFlush = this.#processAndWrite(batch).finally(() => {\n this.#runningFlush = null\n })\n }\n\n /**\n * Waits for the in-flight flush and writes any files still queued. Fires the `drain` event\n * when both are done.\n */\n async drain(): Promise<void> {\n if (this.#runningFlush) await this.#runningFlush\n\n if (this.#pending.size > 0) {\n const batch = [...this.#pending.values()]\n this.#pending.clear()\n await this.#processAndWrite(batch)\n }\n\n await this.hooks.emit('drain')\n }\n\n async #processAndWrite(files: Array<FileNode>): Promise<void> {\n const storage = this.#storage\n\n await this.hooks.emit('start', files)\n\n // Single pass: each file's write starts right after its `update` fires, so IO overlaps\n // parsing and the batch never holds every rendered source in memory at once.\n const queue: Array<Promise<void>> = []\n for await (const item of this.stream(files)) {\n await this.hooks.emit('update', item)\n if (item.source) {\n queue.push(storage.setItem(item.file.path, item.source))\n if (queue.length >= STREAM_FLUSH_EVERY) await Promise.all(queue.splice(0))\n }\n }\n await Promise.all(queue)\n\n await this.hooks.emit('end', files)\n }\n\n /**\n * Clears every listener and the pending queue.\n */\n dispose(): void {\n this.hooks.removeAll()\n this.#pending.clear()\n }\n\n [Symbol.dispose](): void {\n this.dispose()\n }\n}\n","import { createStorage } from '../createStorage.ts'\n\n/**\n * In-memory storage driver. Useful for testing and dry-run scenarios where\n * generated output should be captured without touching the filesystem.\n *\n * All data lives in a `Map` scoped to the storage instance and is discarded\n * when the instance is garbage-collected.\n *\n * @example\n * ```ts\n * import { memoryStorage } from '@kubb/core'\n * import { defineConfig } from 'kubb'\n *\n * export default defineConfig({\n * input: { path: './petStore.yaml' },\n * output: { path: './src/gen' },\n * storage: memoryStorage(),\n * })\n * ```\n */\nexport const memoryStorage = createStorage(() => {\n const store = new Map<string, string>()\n\n return {\n name: 'memory',\n async hasItem(key: string) {\n return store.has(key)\n },\n async getItem(key: string) {\n return store.get(key) ?? null\n },\n async setItem(key: string, value: string) {\n store.set(key, value)\n },\n async removeItem(key: string) {\n store.delete(key)\n },\n async getKeys(base?: string) {\n const keys = [...store.keys()]\n return base ? keys.filter((k) => k.startsWith(base)) : keys\n },\n async clear(base?: string) {\n if (!base) {\n store.clear()\n return\n }\n for (const key of store.keys()) {\n if (key.startsWith(base)) {\n store.delete(key)\n }\n }\n },\n }\n})\n"],"mappings":"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"} |
| import "./rolldown-runtime-C0LytTxp.js"; | ||
| import { EventEmitter } from "node:events"; | ||
| import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; | ||
| import { dirname, resolve } from "node:path"; | ||
| import { ast } from "@kubb/ast"; | ||
| import { extractStringsFromNodes } from "@kubb/ast/utils"; | ||
| //#region ../../internals/utils/src/errors.ts | ||
| /** | ||
| * Thrown when one or more errors occur during a Kubb build. | ||
| * Carries the full list of underlying errors on `errors`. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * throw new BuildError('Build failed', { errors: [err1, err2] }) | ||
| * ``` | ||
| */ | ||
| var BuildError = class extends Error { | ||
| errors; | ||
| constructor(message, options) { | ||
| super(message, { cause: options.cause }); | ||
| this.name = "BuildError"; | ||
| this.errors = options.errors; | ||
| } | ||
| }; | ||
| /** | ||
| * Coerces an unknown thrown value to an `Error` instance. | ||
| * Returns the value as-is when it is already an `Error`; otherwise wraps it with `String(value)`. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * try { ... } catch(err) { | ||
| * throw new BuildError('Build failed', { cause: toError(err), errors: [] }) | ||
| * } | ||
| * ``` | ||
| */ | ||
| function toError(value) { | ||
| return value instanceof Error ? value : new Error(String(value)); | ||
| } | ||
| /** | ||
| * Extracts a human-readable message from any thrown value. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * getErrorMessage(new Error('oops')) // 'oops' | ||
| * getErrorMessage('plain string') // 'plain string' | ||
| * ``` | ||
| */ | ||
| function getErrorMessage(value) { | ||
| return value instanceof Error ? value.message : String(value); | ||
| } | ||
| //#endregion | ||
| //#region ../../internals/utils/src/asyncEventEmitter.ts | ||
| /** | ||
| * Typed `EventEmitter` that awaits all async listeners before resolving. | ||
| * Wraps Node's `EventEmitter` with full TypeScript event-map inference. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const emitter = new AsyncEventEmitter<{ build: [name: string] }>() | ||
| * emitter.on('build', async (name) => { console.log(name) }) | ||
| * await emitter.emit('build', 'petstore') // all listeners awaited | ||
| * ``` | ||
| */ | ||
| var AsyncEventEmitter = class { | ||
| /** | ||
| * Maximum number of listeners per event before Node emits a memory-leak warning. | ||
| * @default 10 | ||
| */ | ||
| constructor(maxListener = 10) { | ||
| this.#emitter.setMaxListeners(maxListener); | ||
| } | ||
| #emitter = new EventEmitter(); | ||
| /** | ||
| * Emits `eventName` and awaits all registered listeners sequentially. | ||
| * Throws if any listener rejects, wrapping the cause with the event name and serialized arguments. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * await emitter.emit('build', 'petstore') | ||
| * ``` | ||
| */ | ||
| emit(eventName, ...eventArgs) { | ||
| const listeners = this.#emitter.listeners(eventName); | ||
| if (listeners.length === 0) return; | ||
| return this.#emitAll(eventName, listeners, eventArgs); | ||
| } | ||
| async #emitAll(eventName, listeners, eventArgs) { | ||
| for (const listener of listeners) try { | ||
| await listener(...eventArgs); | ||
| } catch (err) { | ||
| let serializedArgs; | ||
| try { | ||
| serializedArgs = JSON.stringify(eventArgs); | ||
| } catch { | ||
| serializedArgs = String(eventArgs); | ||
| } | ||
| throw new Error(`Error in async listener for "${eventName}" with eventArgs ${serializedArgs}`, { cause: toError(err) }); | ||
| } | ||
| } | ||
| /** | ||
| * Registers a persistent listener for `eventName`. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * emitter.on('build', async (name) => { console.log(name) }) | ||
| * ``` | ||
| */ | ||
| on(eventName, handler) { | ||
| this.#emitter.on(eventName, handler); | ||
| } | ||
| /** | ||
| * Removes a previously registered listener. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * emitter.off('build', handler) | ||
| * ``` | ||
| */ | ||
| off(eventName, handler) { | ||
| this.#emitter.off(eventName, handler); | ||
| } | ||
| /** | ||
| * Returns the number of listeners registered for `eventName`. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * emitter.on('build', handler) | ||
| * emitter.listenerCount('build') // 1 | ||
| * ``` | ||
| */ | ||
| listenerCount(eventName) { | ||
| return this.#emitter.listenerCount(eventName); | ||
| } | ||
| /** | ||
| * Raises or lowers the per-event listener ceiling before Node warns about a memory leak. | ||
| * Set this above the expected listener count when many listeners attach by design. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * emitter.setMaxListeners(40) | ||
| * ``` | ||
| */ | ||
| setMaxListeners(max) { | ||
| this.#emitter.setMaxListeners(max); | ||
| } | ||
| /** | ||
| * Removes all listeners from every event channel. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * emitter.removeAll() | ||
| * ``` | ||
| */ | ||
| removeAll() { | ||
| this.#emitter.removeAllListeners(); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region ../../internals/utils/src/casing.ts | ||
| /** | ||
| * Shared implementation for camelCase and PascalCase conversion. | ||
| * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons) | ||
| * and capitalizes each word according to `pascal`. | ||
| * | ||
| * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are. | ||
| */ | ||
| function toCamelOrPascal(text, pascal) { | ||
| 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) => { | ||
| if (word.length > 1 && word === word.toUpperCase()) return word; | ||
| return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1); | ||
| }).join("").replace(/[^a-zA-Z0-9]/g, ""); | ||
| } | ||
| /** | ||
| * Converts `text` to camelCase. | ||
| * | ||
| * @example Word boundaries | ||
| * `camelCase('hello-world') // 'helloWorld'` | ||
| * | ||
| * @example With a prefix | ||
| * `camelCase('tag', { prefix: 'create' }) // 'createTag'` | ||
| */ | ||
| function camelCase(text, { prefix = "", suffix = "" } = {}) { | ||
| return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false); | ||
| } | ||
| /** | ||
| * Converts `text` to PascalCase. | ||
| * | ||
| * @example Word boundaries | ||
| * `pascalCase('hello-world') // 'HelloWorld'` | ||
| * | ||
| * @example With a suffix | ||
| * `pascalCase('tag', { suffix: 'schema' }) // 'TagSchema'` | ||
| */ | ||
| function pascalCase(text, { prefix = "", suffix = "" } = {}) { | ||
| return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true); | ||
| } | ||
| //#endregion | ||
| //#region ../../internals/utils/src/runtime.ts | ||
| /** | ||
| * Detects the JavaScript runtime executing the current process and exposes its name and version. | ||
| * | ||
| * Prefer the shared {@link runtime} instance over constructing your own. | ||
| */ | ||
| var Runtime = class { | ||
| /** | ||
| * `true` when the current process is running under Bun. | ||
| * | ||
| * Detection keys off the global `Bun` object rather than `process.versions`, | ||
| * because Bun polyfills `process.versions.node` for Node compatibility and would | ||
| * otherwise look like Node. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * if (runtime.isBun) { | ||
| * await Bun.write(path, data) | ||
| * } | ||
| * ``` | ||
| */ | ||
| get isBun() { | ||
| return typeof Bun !== "undefined"; | ||
| } | ||
| /** | ||
| * `true` when the current process is running under Deno. | ||
| */ | ||
| get isDeno() { | ||
| return typeof globalThis.Deno !== "undefined"; | ||
| } | ||
| /** | ||
| * `true` when the current process is running under Node. | ||
| * | ||
| * Bun and Deno are excluded first so a polyfilled `process` does not register as Node. | ||
| */ | ||
| get isNode() { | ||
| return !this.isBun && !this.isDeno && typeof process !== "undefined" && process.versions?.node != null; | ||
| } | ||
| /** | ||
| * Name of the runtime executing the current process. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * runtime.name // 'bun' when run with `bun kubb`, 'node' otherwise | ||
| * ``` | ||
| */ | ||
| get name() { | ||
| if (this.isBun) return "bun"; | ||
| if (this.isDeno) return "deno"; | ||
| return "node"; | ||
| } | ||
| /** | ||
| * Version of the active runtime, or an empty string when it cannot be read. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * runtime.version // '1.3.11' under Bun, '22.22.2' under Node | ||
| * ``` | ||
| */ | ||
| get version() { | ||
| if (this.isBun) return process.versions.bun ?? ""; | ||
| if (this.isDeno) return globalThis.Deno?.version?.deno ?? ""; | ||
| return process.versions?.node ?? ""; | ||
| } | ||
| }; | ||
| /** | ||
| * Shared {@link Runtime} instance describing the JavaScript runtime executing the current process. | ||
| */ | ||
| const runtime = new Runtime(); | ||
| //#endregion | ||
| //#region ../../internals/utils/src/fs.ts | ||
| /** | ||
| * Reads the file at `path` as a UTF-8 string. | ||
| * Uses `Bun.file().text()` when running under Bun, `fs.readFile` otherwise. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const source = await read('./src/Pet.ts') | ||
| * ``` | ||
| */ | ||
| async function read(path) { | ||
| if (runtime.isBun) return Bun.file(path).text(); | ||
| return readFile(path, { encoding: "utf8" }); | ||
| } | ||
| /** | ||
| * Writes `data` to `path`, trimming leading/trailing whitespace before saving. | ||
| * Skips the write when the trimmed content is empty or identical to what is already on disk. | ||
| * Creates any missing parent directories automatically. | ||
| * When `sanity` is `true`, re-reads the file after writing and throws if the content does not match. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * await write('./src/Pet.ts', source) // writes and returns trimmed content | ||
| * await write('./src/Pet.ts', source) // null — file unchanged | ||
| * await write('./src/Pet.ts', ' ') // null — empty content skipped | ||
| * ``` | ||
| */ | ||
| async function write(path, data, options = {}) { | ||
| const trimmed = data.trim(); | ||
| if (trimmed === "") return null; | ||
| const resolved = resolve(path); | ||
| if (runtime.isBun) { | ||
| const file = Bun.file(resolved); | ||
| if ((await file.exists() ? await file.text() : null) === trimmed) return null; | ||
| await Bun.write(resolved, trimmed); | ||
| return trimmed; | ||
| } | ||
| try { | ||
| if (await readFile(resolved, { encoding: "utf-8" }) === trimmed) return null; | ||
| } catch {} | ||
| await mkdir(dirname(resolved), { recursive: true }); | ||
| await writeFile(resolved, trimmed, { encoding: "utf-8" }); | ||
| if (options.sanity) { | ||
| const savedData = await readFile(resolved, { encoding: "utf-8" }); | ||
| if (savedData !== trimmed) throw new Error(`Sanity check failed for ${path}\n\nData[${data.length}]:\n${data}\n\nSaved[${savedData.length}]:\n${savedData}\n`); | ||
| return savedData; | ||
| } | ||
| return trimmed; | ||
| } | ||
| /** | ||
| * Recursively removes `path`. Silently succeeds when `path` does not exist. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * await clean('./dist') | ||
| * ``` | ||
| */ | ||
| async function clean(path) { | ||
| return rm(path, { | ||
| recursive: true, | ||
| force: true | ||
| }); | ||
| } | ||
| /** | ||
| * Converts a filesystem path to use POSIX (`/`) separators. | ||
| * | ||
| * Most of the codebase compares and composes paths as strings (prefix matching, joining for | ||
| * import specifiers, splitting on `/`). On POSIX `path.resolve` already returns `/`-separated | ||
| * paths, but on Windows it returns `\`-separated paths, which breaks every such comparison. | ||
| * | ||
| * Routing every path that crosses a module boundary through `toPosixPath` keeps the rest of the | ||
| * code platform-agnostic. The conversion runs unconditionally so Windows-specific behavior is | ||
| * exercisable from POSIX CI. | ||
| * | ||
| * @example | ||
| * toPosixPath('C:\\repo\\src\\pet.ts') // 'C:/repo/src/pet.ts' | ||
| */ | ||
| function toPosixPath(filePath) { | ||
| return filePath.replaceAll("\\", "/"); | ||
| } | ||
| /** | ||
| * Builds a nested file path from a dotted name. Splits on dots that precede a letter | ||
| * (so version numbers embedded in operationIds like `v2025.0` stay intact), camelCases | ||
| * every earlier segment, applies `caseLast` to the final segment, and joins with `/`. | ||
| * | ||
| * Empty segments are dropped before joining. They arise when the name starts with a dot | ||
| * followed by a letter (e.g. `..Schema` splits into `['..', 'Schema']` and `'..'` cases to | ||
| * an empty string). Without this a leading `/` would form, which `path.resolve` reads as an | ||
| * absolute path, letting generated files escape the configured output directory. | ||
| * | ||
| * @example Nested path from a dotted name | ||
| * `toFilePath('pet.petId') // 'pet/petId'` | ||
| * | ||
| * @example PascalCase the final segment | ||
| * `toFilePath('pet.Pet', pascalCase) // 'pet/Pet'` | ||
| * | ||
| * @example Suffix applied to the final segment only | ||
| * `toFilePath('tag.tag', (part) => camelCase(part, { suffix: 'schema' })) // 'tag/tagSchema'` | ||
| */ | ||
| function toFilePath(name, caseLast = camelCase) { | ||
| const parts = name.split(/\.(?=[a-zA-Z])/); | ||
| return parts.map((part, i) => i === parts.length - 1 ? caseLast(part) : camelCase(part)).filter(Boolean).join("/"); | ||
| } | ||
| //#endregion | ||
| //#region src/constants.ts | ||
| /** | ||
| * Plugin `include` filter types that select operations directly. When one of these is set | ||
| * without a `schemaName` include, the generate phase pre-scans operations to compute the set | ||
| * of schemas they reach, so unreachable schemas can be pruned for that plugin. | ||
| */ | ||
| const OPERATION_FILTER_TYPES = new Set([ | ||
| "tag", | ||
| "operationId", | ||
| "path", | ||
| "method", | ||
| "contentType" | ||
| ]); | ||
| /** | ||
| * Stable codes Kubb attaches to a `Diagnostic`. Each maps to a known failure mode | ||
| * and stays stable so it can be referenced in tooling and (later) docs. Reference | ||
| * these instead of inlining the string at a throw site. | ||
| */ | ||
| const diagnosticCode = { | ||
| /** | ||
| * Fallback for an unstructured error with no specific code. | ||
| */ | ||
| unknown: "KUBB_UNKNOWN", | ||
| /** | ||
| * The `input.path` file or URL could not be read. | ||
| */ | ||
| inputNotFound: "KUBB_INPUT_NOT_FOUND", | ||
| /** | ||
| * An adapter was configured without an `input`. | ||
| */ | ||
| inputRequired: "KUBB_INPUT_REQUIRED", | ||
| /** | ||
| * A `$ref` (or equivalent reference) could not be resolved in the source document. | ||
| */ | ||
| refNotFound: "KUBB_REF_NOT_FOUND", | ||
| /** | ||
| * A server variable value is not allowed by its `enum`. | ||
| */ | ||
| invalidServerVariable: "KUBB_INVALID_SERVER_VARIABLE", | ||
| /** | ||
| * A required plugin is missing from the config. | ||
| */ | ||
| pluginNotFound: "KUBB_PLUGIN_NOT_FOUND", | ||
| /** | ||
| * A plugin threw while generating. | ||
| */ | ||
| pluginFailed: "KUBB_PLUGIN_FAILED", | ||
| /** | ||
| * A plugin reported a non-fatal warning through `ctx.warn`. | ||
| */ | ||
| pluginWarning: "KUBB_PLUGIN_WARNING", | ||
| /** | ||
| * A plugin reported an informational message through `ctx.info`. | ||
| */ | ||
| pluginInfo: "KUBB_PLUGIN_INFO", | ||
| /** | ||
| * A schema uses a `format` Kubb does not map to a specific type. Reserved for | ||
| * adapters to emit as a `warning`. | ||
| */ | ||
| unsupportedFormat: "KUBB_UNSUPPORTED_FORMAT", | ||
| /** | ||
| * A referenced schema or operation is marked `deprecated`. Reserved for adapters | ||
| * to emit as an `info`. | ||
| */ | ||
| deprecated: "KUBB_DEPRECATED", | ||
| /** | ||
| * An adapter is required but the config has none. The build cannot read the input | ||
| * without one. | ||
| */ | ||
| adapterRequired: "KUBB_ADAPTER_REQUIRED", | ||
| /** | ||
| * A resolved output path escapes the output directory, which can stem from a path | ||
| * traversal in the spec or a misconfigured `group.name`. | ||
| */ | ||
| pathTraversal: "KUBB_PATH_TRAVERSAL", | ||
| /** | ||
| * A plugin's options are invalid, for example `output.mode: 'file'` paired with a `group` option. | ||
| */ | ||
| invalidPluginOptions: "KUBB_INVALID_PLUGIN_OPTIONS", | ||
| /** | ||
| * A post-generate shell hook (`hooks.done`) exited with a failure. | ||
| */ | ||
| hookFailed: "KUBB_HOOK_FAILED", | ||
| /** | ||
| * The formatter pass over the generated files failed. | ||
| */ | ||
| formatFailed: "KUBB_FORMAT_FAILED", | ||
| /** | ||
| * The linter pass over the generated files failed. | ||
| */ | ||
| lintFailed: "KUBB_LINT_FAILED", | ||
| /** | ||
| * Not a failure. Carries a plugin's elapsed time, summed into the run total. | ||
| */ | ||
| performance: "KUBB_PERFORMANCE", | ||
| /** | ||
| * Not a failure. A newer Kubb version is available on npm. | ||
| */ | ||
| updateAvailable: "KUBB_UPDATE_AVAILABLE" | ||
| }; | ||
| //#endregion | ||
| //#region src/createStorage.ts | ||
| /** | ||
| * Defines a custom storage backend. The builder receives user options and | ||
| * returns a `Storage` implementation. Kubb ships with filesystem and in-memory | ||
| * storages. A custom backend writes generated files elsewhere, such as cloud | ||
| * storage or a database. | ||
| * | ||
| * @example In-memory storage (the built-in implementation) | ||
| * ```ts | ||
| * import { createStorage } from '@kubb/core' | ||
| * | ||
| * export const memoryStorage = createStorage(() => { | ||
| * const store = new Map<string, string>() | ||
| * | ||
| * return { | ||
| * name: 'memory', | ||
| * async hasItem(key) { | ||
| * return store.has(key) | ||
| * }, | ||
| * async getItem(key) { | ||
| * return store.get(key) ?? null | ||
| * }, | ||
| * async setItem(key, value) { | ||
| * store.set(key, value) | ||
| * }, | ||
| * async removeItem(key) { | ||
| * store.delete(key) | ||
| * }, | ||
| * async getKeys(base) { | ||
| * const keys = [...store.keys()] | ||
| * return base ? keys.filter((k) => k.startsWith(base)) : keys | ||
| * }, | ||
| * async clear(base) { | ||
| * if (!base) store.clear() | ||
| * }, | ||
| * } | ||
| * }) | ||
| * ``` | ||
| */ | ||
| function createStorage(build) { | ||
| return (options) => build(options ?? {}); | ||
| } | ||
| //#endregion | ||
| //#region src/FileManager.ts | ||
| function mergeFile(a, b) { | ||
| return { | ||
| ...a, | ||
| banner: b.banner, | ||
| footer: b.footer, | ||
| copy: b.copy ?? a.copy, | ||
| sources: a.sources.length ? b.sources.length ? [...a.sources, ...b.sources] : a.sources : b.sources, | ||
| imports: a.imports.length ? b.imports.length ? [...a.imports, ...b.imports] : a.imports : b.imports, | ||
| exports: a.exports.length ? b.exports.length ? [...a.exports, ...b.exports] : a.exports : b.exports | ||
| }; | ||
| } | ||
| function isIndexPath(path) { | ||
| return path.endsWith("/index.ts") || path === "index.ts"; | ||
| } | ||
| function compareFiles(a, b) { | ||
| const lenDiff = a.path.length - b.path.length; | ||
| if (lenDiff !== 0) return lenDiff; | ||
| const aIsIndex = isIndexPath(a.path); | ||
| const bIsIndex = isIndexPath(b.path); | ||
| if (aIsIndex && !bIsIndex) return 1; | ||
| if (!aIsIndex && bIsIndex) return -1; | ||
| return 0; | ||
| } | ||
| /** | ||
| * In-memory file store for generated files. Files sharing a `path` are merged | ||
| * (sources/imports/exports concatenated). The `files` getter is sorted by | ||
| * path length (barrel `index.ts` last within a bucket). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const manager = new FileManager() | ||
| * manager.upsert(myFile) | ||
| * manager.files // sorted view | ||
| * ``` | ||
| */ | ||
| var FileManager = class { | ||
| /** | ||
| * Subscribe to file-store changes. Listeners on `upsert` see each resolved file as it lands | ||
| * through `add` or `upsert`. | ||
| */ | ||
| hooks = new AsyncEventEmitter(); | ||
| #cache = /* @__PURE__ */ new Map(); | ||
| #sorted = null; | ||
| add(...files) { | ||
| return this.#store(files, false); | ||
| } | ||
| upsert(...files) { | ||
| return this.#store(files, true); | ||
| } | ||
| #store(files, mergeExisting) { | ||
| const batch = files.length > 1 ? this.#dedupe(files) : files; | ||
| const resolved = []; | ||
| for (const file of batch) { | ||
| const existing = this.#cache.get(file.path); | ||
| const merged = existing && mergeExisting ? ast.factory.createFile(mergeFile(existing, file)) : ast.factory.createFile(file); | ||
| this.#cache.set(merged.path, merged); | ||
| resolved.push(merged); | ||
| this.hooks.emit("upsert", merged); | ||
| } | ||
| if (resolved.length > 0) this.#sorted = null; | ||
| return resolved; | ||
| } | ||
| #dedupe(files) { | ||
| const seen = /* @__PURE__ */ new Map(); | ||
| for (const file of files) { | ||
| const prev = seen.get(file.path); | ||
| seen.set(file.path, prev ? mergeFile(prev, file) : file); | ||
| } | ||
| return [...seen.values()]; | ||
| } | ||
| getByPath(path) { | ||
| return this.#cache.get(path) ?? null; | ||
| } | ||
| deleteByPath(path) { | ||
| if (!this.#cache.delete(path)) return; | ||
| this.#sorted = null; | ||
| } | ||
| clear() { | ||
| this.#cache.clear(); | ||
| this.#sorted = null; | ||
| } | ||
| /** | ||
| * Releases all stored files and clears every `hooks` listener. Called by the core after | ||
| * `kubb:build:end`. | ||
| */ | ||
| dispose() { | ||
| this.clear(); | ||
| this.hooks.removeAll(); | ||
| } | ||
| [Symbol.dispose]() { | ||
| this.dispose(); | ||
| } | ||
| /** | ||
| * All stored files in stable sort order (shortest path first, barrel files | ||
| * last within a length bucket). Returns a cached view, do not mutate. | ||
| */ | ||
| get files() { | ||
| return this.#sorted ??= [...this.#cache.values()].sort(compareFiles); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/FileProcessor.ts | ||
| function joinSources(file) { | ||
| const sources = file.sources; | ||
| if (sources.length === 0) return ""; | ||
| const parts = []; | ||
| for (const source of sources) { | ||
| const text = extractStringsFromNodes(source.nodes); | ||
| if (text) parts.push(text); | ||
| } | ||
| return parts.join("\n\n"); | ||
| } | ||
| async function parseCopy(file) { | ||
| let content; | ||
| try { | ||
| content = await read(file.copy); | ||
| } catch (err) { | ||
| throw new Error(`[kubb] Could not copy file into output: ${file.copy}`, { cause: err }); | ||
| } | ||
| return [ | ||
| file.banner, | ||
| content, | ||
| file.footer | ||
| ].filter((segment) => Boolean(segment)).map((segment) => segment.trimEnd()).join("\n"); | ||
| } | ||
| /** | ||
| * Turns `FileNode`s into source strings and writes them to storage. | ||
| * | ||
| * Two modes share the same instance. Stateless mode (`parse`, `stream`, `run`) just runs the | ||
| * conversion. Queue mode (`enqueue`, `flush`, `drain`) buffers files deduped by path and | ||
| * writes each batch through storage with up to `STREAM_FLUSH_EVERY` requests in flight. | ||
| * | ||
| * `flush` does not wait for its batch to finish, so dispatch can overlap with IO. The next | ||
| * `flush` or `drain` picks the in-flight batch up. `drain` blocks until everything has been | ||
| * written and is meant for the end of a build. | ||
| * | ||
| * To surface build-level hook signals (`kubb:files:processing:*` and friends) subscribe to | ||
| * `hooks` and re-emit on the kubb bus. | ||
| */ | ||
| var FileProcessor = class { | ||
| hooks = new AsyncEventEmitter(); | ||
| #parsers; | ||
| #storage; | ||
| #extension; | ||
| #pending = /* @__PURE__ */ new Map(); | ||
| #runningFlush = null; | ||
| constructor(options) { | ||
| this.#parsers = options.parsers ?? null; | ||
| this.#storage = options.storage; | ||
| this.#extension = options.extension ?? null; | ||
| } | ||
| /** | ||
| * Files waiting in the queue. | ||
| */ | ||
| get size() { | ||
| return this.#pending.size; | ||
| } | ||
| async parse(file) { | ||
| if (file.copy) return parseCopy(file); | ||
| const parsers = this.#parsers; | ||
| const parseExtName = this.#extension?.[file.extname] || void 0; | ||
| if (!parsers || !file.extname) return joinSources(file); | ||
| const parser = parsers.get(file.extname); | ||
| if (!parser) return joinSources(file); | ||
| return parser.parse(file, { extname: parseExtName }); | ||
| } | ||
| async *stream(files) { | ||
| const total = files.length; | ||
| if (total === 0) return; | ||
| let processed = 0; | ||
| for (const file of files) { | ||
| const source = await this.parse(file); | ||
| processed++; | ||
| yield { | ||
| file, | ||
| source, | ||
| processed, | ||
| total, | ||
| percentage: processed / total * 100 | ||
| }; | ||
| } | ||
| } | ||
| async run(files) { | ||
| await this.hooks.emit("start", files); | ||
| for await (const { file, source, processed, total, percentage } of this.stream(files)) await this.hooks.emit("update", { | ||
| file, | ||
| source, | ||
| processed, | ||
| percentage, | ||
| total | ||
| }); | ||
| await this.hooks.emit("end", files); | ||
| return files; | ||
| } | ||
| /** | ||
| * Adds a file to the next flush. A later `enqueue` for the same path replaces the previous | ||
| * entry, matching `FileManager.upsert`. Fires the `enqueue` event. | ||
| */ | ||
| enqueue(file) { | ||
| this.#pending.set(file.path, file); | ||
| this.hooks.emit("enqueue", file); | ||
| } | ||
| /** | ||
| * Starts processing the queued files. Waits for any previous flush to finish (so two | ||
| * batches never run together) and then returns without waiting for the new one. The next | ||
| * `flush` or `drain` picks up the in-flight task. | ||
| */ | ||
| async flush() { | ||
| if (this.#runningFlush) await this.#runningFlush; | ||
| if (this.#pending.size === 0) return; | ||
| const batch = [...this.#pending.values()]; | ||
| this.#pending.clear(); | ||
| this.#runningFlush = this.#processAndWrite(batch).finally(() => { | ||
| this.#runningFlush = null; | ||
| }); | ||
| } | ||
| /** | ||
| * Waits for the in-flight flush and writes any files still queued. Fires the `drain` event | ||
| * when both are done. | ||
| */ | ||
| async drain() { | ||
| if (this.#runningFlush) await this.#runningFlush; | ||
| if (this.#pending.size > 0) { | ||
| const batch = [...this.#pending.values()]; | ||
| this.#pending.clear(); | ||
| await this.#processAndWrite(batch); | ||
| } | ||
| await this.hooks.emit("drain"); | ||
| } | ||
| async #processAndWrite(files) { | ||
| const storage = this.#storage; | ||
| await this.hooks.emit("start", files); | ||
| const queue = []; | ||
| for await (const item of this.stream(files)) { | ||
| await this.hooks.emit("update", item); | ||
| if (item.source) { | ||
| queue.push(storage.setItem(item.file.path, item.source)); | ||
| if (queue.length >= 50) await Promise.all(queue.splice(0)); | ||
| } | ||
| } | ||
| await Promise.all(queue); | ||
| await this.hooks.emit("end", files); | ||
| } | ||
| /** | ||
| * Clears every listener and the pending queue. | ||
| */ | ||
| dispose() { | ||
| this.hooks.removeAll(); | ||
| this.#pending.clear(); | ||
| } | ||
| [Symbol.dispose]() { | ||
| this.dispose(); | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region \0@oxc-project+runtime@0.135.0/helpers/esm/usingCtx.js | ||
| function _usingCtx() { | ||
| var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) { | ||
| var n = Error(); | ||
| return n.name = "SuppressedError", n.error = r, n.suppressed = e, n; | ||
| }; | ||
| var e = {}; | ||
| var n = []; | ||
| function using(r, e) { | ||
| if (null != e) { | ||
| if (Object(e) !== e) throw new TypeError("using declarations can only be used with objects, functions, null, or undefined."); | ||
| if (r) var o = e[Symbol.asyncDispose || Symbol["for"]("Symbol.asyncDispose")]; | ||
| if (void 0 === o && (o = e[Symbol.dispose || Symbol["for"]("Symbol.dispose")], r)) var t = o; | ||
| if ("function" != typeof o) throw new TypeError("Object is not disposable."); | ||
| t && (o = function o() { | ||
| try { | ||
| t.call(e); | ||
| } catch (r) { | ||
| return Promise.reject(r); | ||
| } | ||
| }), n.push({ | ||
| v: e, | ||
| d: o, | ||
| a: r | ||
| }); | ||
| } else r && n.push({ | ||
| d: e, | ||
| a: r | ||
| }); | ||
| return e; | ||
| } | ||
| return { | ||
| e, | ||
| u: using.bind(null, !1), | ||
| a: using.bind(null, !0), | ||
| d: function d() { | ||
| var o; | ||
| var t = this.e; | ||
| var s = 0; | ||
| function next() { | ||
| for (; o = n.pop();) try { | ||
| if (!o.a && 1 === s) return s = 0, n.push(o), Promise.resolve().then(next); | ||
| if (o.d) { | ||
| var r = o.d.call(o.v); | ||
| if (o.a) return s |= 2, Promise.resolve(r).then(next, err); | ||
| } else s |= 1; | ||
| } catch (r) { | ||
| return err(r); | ||
| } | ||
| if (1 === s) return t !== e ? Promise.reject(t) : Promise.resolve(); | ||
| if (t !== e) throw t; | ||
| } | ||
| function err(n) { | ||
| return t = t !== e ? new r(n, t) : n, next(); | ||
| } | ||
| return next(); | ||
| } | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region src/storages/memoryStorage.ts | ||
| /** | ||
| * In-memory storage driver. Useful for testing and dry-run scenarios where | ||
| * generated output should be captured without touching the filesystem. | ||
| * | ||
| * All data lives in a `Map` scoped to the storage instance and is discarded | ||
| * when the instance is garbage-collected. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { memoryStorage } from '@kubb/core' | ||
| * import { defineConfig } from 'kubb' | ||
| * | ||
| * export default defineConfig({ | ||
| * input: { path: './petStore.yaml' }, | ||
| * output: { path: './src/gen' }, | ||
| * storage: memoryStorage(), | ||
| * }) | ||
| * ``` | ||
| */ | ||
| const memoryStorage = createStorage(() => { | ||
| const store = /* @__PURE__ */ new Map(); | ||
| return { | ||
| name: "memory", | ||
| async hasItem(key) { | ||
| return store.has(key); | ||
| }, | ||
| async getItem(key) { | ||
| return store.get(key) ?? null; | ||
| }, | ||
| async setItem(key, value) { | ||
| store.set(key, value); | ||
| }, | ||
| async removeItem(key) { | ||
| store.delete(key); | ||
| }, | ||
| async getKeys(base) { | ||
| const keys = [...store.keys()]; | ||
| return base ? keys.filter((k) => k.startsWith(base)) : keys; | ||
| }, | ||
| async clear(base) { | ||
| if (!base) { | ||
| store.clear(); | ||
| return; | ||
| } | ||
| for (const key of store.keys()) if (key.startsWith(base)) store.delete(key); | ||
| } | ||
| }; | ||
| }); | ||
| //#endregion | ||
| export { getErrorMessage as _, createStorage as a, clean as c, write as d, runtime as f, BuildError as g, AsyncEventEmitter as h, FileManager as i, toFilePath as l, pascalCase as m, _usingCtx as n, OPERATION_FILTER_TYPES as o, camelCase as p, FileProcessor as r, diagnosticCode as s, memoryStorage as t, toPosixPath as u }; | ||
| //# sourceMappingURL=memoryStorage-Dow5-isU.js.map |
| {"version":3,"file":"memoryStorage-Dow5-isU.js","names":["#emitter","NodeEventEmitter","#emitAll","#cache","#store","#dedupe","#sorted","#parsers","#storage","#extension","#pending","#runningFlush","#processAndWrite"],"sources":["../../../internals/utils/src/errors.ts","../../../internals/utils/src/asyncEventEmitter.ts","../../../internals/utils/src/casing.ts","../../../internals/utils/src/runtime.ts","../../../internals/utils/src/fs.ts","../src/constants.ts","../src/createStorage.ts","../src/FileManager.ts","../src/FileProcessor.ts","../src/storages/memoryStorage.ts"],"sourcesContent":["/**\n * Thrown when one or more errors occur during a Kubb build.\n * Carries the full list of underlying errors on `errors`.\n *\n * @example\n * ```ts\n * throw new BuildError('Build failed', { errors: [err1, err2] })\n * ```\n */\nexport class BuildError extends Error {\n errors: Array<Error>\n\n constructor(message: string, options: { cause?: Error; errors: Array<Error> }) {\n super(message, { cause: options.cause })\n this.name = 'BuildError'\n this.errors = options.errors\n }\n}\n\n/**\n * Coerces an unknown thrown value to an `Error` instance.\n * Returns the value as-is when it is already an `Error`; otherwise wraps it with `String(value)`.\n *\n * @example\n * ```ts\n * try { ... } catch(err) {\n * throw new BuildError('Build failed', { cause: toError(err), errors: [] })\n * }\n * ```\n */\nexport function toError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value))\n}\n\n/**\n * Extracts a human-readable message from any thrown value.\n *\n * @example\n * ```ts\n * getErrorMessage(new Error('oops')) // 'oops'\n * getErrorMessage('plain string') // 'plain string'\n * ```\n */\nexport function getErrorMessage(value: unknown): string {\n return value instanceof Error ? value.message : String(value)\n}\n\n/**\n * Extracts the `.cause` of an `Error` as an `Error`, or `undefined` when absent or not an `Error`.\n *\n * @example\n * ```ts\n * const cause = toCause(buildError) // Error | undefined\n * ```\n */\nexport function toCause(error: Error): Error | undefined {\n return error.cause instanceof Error ? error.cause : undefined\n}\n","import { EventEmitter as NodeEventEmitter } from 'node:events'\nimport { toError } from './errors.ts'\n\n/**\n * A function that can be registered as an event listener, synchronous or async.\n */\ntype AsyncListener<TArgs extends Array<unknown>> = (...args: TArgs) => void | Promise<void>\n\n/**\n * Typed `EventEmitter` that awaits all async listeners before resolving.\n * Wraps Node's `EventEmitter` with full TypeScript event-map inference.\n *\n * @example\n * ```ts\n * const emitter = new AsyncEventEmitter<{ build: [name: string] }>()\n * emitter.on('build', async (name) => { console.log(name) })\n * await emitter.emit('build', 'petstore') // all listeners awaited\n * ```\n */\nexport class AsyncEventEmitter<TEvents extends { [K in keyof TEvents]: Array<unknown> }> {\n /**\n * Maximum number of listeners per event before Node emits a memory-leak warning.\n * @default 10\n */\n constructor(maxListener = 10) {\n this.#emitter.setMaxListeners(maxListener)\n }\n\n #emitter = new NodeEventEmitter()\n\n /**\n * Emits `eventName` and awaits all registered listeners sequentially.\n * Throws if any listener rejects, wrapping the cause with the event name and serialized arguments.\n *\n * @example\n * ```ts\n * await emitter.emit('build', 'petstore')\n * ```\n */\n emit<TEventName extends keyof TEvents & string>(eventName: TEventName, ...eventArgs: TEvents[TEventName]): Promise<void> | void {\n const listeners = this.#emitter.listeners(eventName) as Array<AsyncListener<TEvents[TEventName]>>\n\n if (listeners.length === 0) {\n return\n }\n\n return this.#emitAll(eventName, listeners, eventArgs)\n }\n\n async #emitAll<TEventName extends keyof TEvents & string>(\n eventName: TEventName,\n listeners: Array<AsyncListener<TEvents[TEventName]>>,\n eventArgs: TEvents[TEventName],\n ): Promise<void> {\n for (const listener of listeners) {\n try {\n await listener(...eventArgs)\n } catch (err) {\n let serializedArgs: string\n try {\n serializedArgs = JSON.stringify(eventArgs)\n } catch {\n serializedArgs = String(eventArgs)\n }\n throw new Error(`Error in async listener for \"${eventName}\" with eventArgs ${serializedArgs}`, { cause: toError(err) })\n }\n }\n }\n\n /**\n * Registers a persistent listener for `eventName`.\n *\n * @example\n * ```ts\n * emitter.on('build', async (name) => { console.log(name) })\n * ```\n */\n on<TEventName extends keyof TEvents & string>(eventName: TEventName, handler: AsyncListener<TEvents[TEventName]>): void {\n this.#emitter.on(eventName, handler as AsyncListener<Array<unknown>>)\n }\n\n /**\n * Removes a previously registered listener.\n *\n * @example\n * ```ts\n * emitter.off('build', handler)\n * ```\n */\n off<TEventName extends keyof TEvents & string>(eventName: TEventName, handler: AsyncListener<TEvents[TEventName]>): void {\n this.#emitter.off(eventName, handler as AsyncListener<Array<unknown>>)\n }\n\n /**\n * Returns the number of listeners registered for `eventName`.\n *\n * @example\n * ```ts\n * emitter.on('build', handler)\n * emitter.listenerCount('build') // 1\n * ```\n */\n listenerCount<TEventName extends keyof TEvents & string>(eventName: TEventName): number {\n return this.#emitter.listenerCount(eventName)\n }\n\n /**\n * Raises or lowers the per-event listener ceiling before Node warns about a memory leak.\n * Set this above the expected listener count when many listeners attach by design.\n *\n * @example\n * ```ts\n * emitter.setMaxListeners(40)\n * ```\n */\n setMaxListeners(max: number): void {\n this.#emitter.setMaxListeners(max)\n }\n\n /**\n * Removes all listeners from every event channel.\n *\n * @example\n * ```ts\n * emitter.removeAll()\n * ```\n */\n removeAll(): void {\n this.#emitter.removeAllListeners()\n }\n}\n","type Options = {\n /**\n * Text prepended before casing is applied.\n */\n prefix?: string\n /**\n * Text appended before casing is applied.\n */\n suffix?: string\n}\n\n/**\n * Shared implementation for camelCase and PascalCase conversion.\n * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)\n * and capitalizes each word according to `pascal`.\n *\n * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.\n */\nfunction toCamelOrPascal(text: string, pascal: boolean): string {\n return text\n .trim()\n .replace(/([a-z\\d])([A-Z])/g, '$1 $2')\n .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')\n .replace(/(\\d)([a-z])/g, '$1 $2')\n .split(/[\\s\\-_./\\\\:]+/)\n .filter(Boolean)\n .map((word, i) => {\n if (word.length > 1 && word === word.toUpperCase()) return word\n const head = i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()\n return head + word.slice(1)\n })\n .join('')\n .replace(/[^a-zA-Z0-9]/g, '')\n}\n\n/**\n * Converts `text` to camelCase.\n *\n * @example Word boundaries\n * `camelCase('hello-world') // 'helloWorld'`\n *\n * @example With a prefix\n * `camelCase('tag', { prefix: 'create' }) // 'createTag'`\n */\nexport function camelCase(text: string, { prefix = '', suffix = '' }: Options = {}): string {\n return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false)\n}\n\n/**\n * Converts `text` to PascalCase.\n *\n * @example Word boundaries\n * `pascalCase('hello-world') // 'HelloWorld'`\n *\n * @example With a suffix\n * `pascalCase('tag', { suffix: 'schema' }) // 'TagSchema'`\n */\nexport function pascalCase(text: string, { prefix = '', suffix = '' }: Options = {}): string {\n return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true)\n}\n","/**\n * Name of the JavaScript runtime executing the current process.\n */\ntype RuntimeName = 'bun' | 'deno' | 'node'\n\n/**\n * Detects the JavaScript runtime executing the current process and exposes its name and version.\n *\n * Prefer the shared {@link runtime} instance over constructing your own.\n */\nclass Runtime {\n /**\n * `true` when the current process is running under Bun.\n *\n * Detection keys off the global `Bun` object rather than `process.versions`,\n * because Bun polyfills `process.versions.node` for Node compatibility and would\n * otherwise look like Node.\n *\n * @example\n * ```ts\n * if (runtime.isBun) {\n * await Bun.write(path, data)\n * }\n * ```\n */\n get isBun(): boolean {\n return typeof Bun !== 'undefined'\n }\n\n /**\n * `true` when the current process is running under Deno.\n */\n get isDeno(): boolean {\n return typeof (globalThis as { Deno?: unknown }).Deno !== 'undefined'\n }\n\n /**\n * `true` when the current process is running under Node.\n *\n * Bun and Deno are excluded first so a polyfilled `process` does not register as Node.\n */\n get isNode(): boolean {\n return !this.isBun && !this.isDeno && typeof process !== 'undefined' && process.versions?.node != null\n }\n\n /**\n * Name of the runtime executing the current process.\n *\n * @example\n * ```ts\n * runtime.name // 'bun' when run with `bun kubb`, 'node' otherwise\n * ```\n */\n get name(): RuntimeName {\n if (this.isBun) return 'bun'\n if (this.isDeno) return 'deno'\n\n return 'node'\n }\n\n /**\n * Version of the active runtime, or an empty string when it cannot be read.\n *\n * @example\n * ```ts\n * runtime.version // '1.3.11' under Bun, '22.22.2' under Node\n * ```\n */\n get version(): string {\n if (this.isBun) return process.versions.bun ?? ''\n if (this.isDeno) return (globalThis as { Deno?: { version?: { deno?: string } } }).Deno?.version?.deno ?? ''\n\n return process.versions?.node ?? ''\n }\n}\n\n/**\n * Shared {@link Runtime} instance describing the JavaScript runtime executing the current process.\n */\nexport const runtime = new Runtime()\n","import { existsSync } from 'node:fs'\nimport { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises'\nimport { dirname, join, posix, resolve } from 'node:path'\nimport { camelCase } from './casing.ts'\nimport { runtime } from './runtime.ts'\n\n/**\n * Walks up the directory tree from `cwd` (defaults to `process.cwd()`) and\n * returns the absolute path of the nearest `package.json`, or `null` when none\n * is found before reaching the filesystem root.\n *\n * @example\n * ```ts\n * const pkgPath = findPackageJSON('/home/user/project/src') // '/home/user/project/package.json'\n * ```\n */\nexport function findPackageJSON(cwd?: string): string | null {\n let dir = cwd ? resolve(cwd) : process.cwd()\n while (true) {\n const pkgPath = join(dir, 'package.json')\n if (existsSync(pkgPath)) return pkgPath\n const parent = dirname(dir)\n if (parent === dir) return null\n dir = parent\n }\n}\n\n/**\n * Converts all backslashes to forward slashes.\n * Extended-length Windows paths (`\\\\?\\...`) are left unchanged.\n */\nfunction toSlash(p: string): string {\n if (p.startsWith('\\\\\\\\?\\\\')) return p\n\n return p.replaceAll('\\\\', '/')\n}\n\n/**\n * Returns the relative path from `rootDir` to `filePath`, always using forward slashes\n * and prefixed with `./` when not already traversing upward.\n *\n * @example\n * ```ts\n * getRelativePath('/src/components', '/src/components/Button.tsx') // './Button.tsx'\n * getRelativePath('/src/components', '/src/utils/helpers.ts') // '../utils/helpers.ts'\n * ```\n */\nexport function getRelativePath(rootDir?: string | null, filePath?: string | null): string {\n if (!rootDir || !filePath) {\n throw new Error(`Root and file should be filled in when retrieving the relativePath, ${rootDir || ''} ${filePath || ''}`)\n }\n\n const relativePath = posix.relative(toSlash(rootDir), toSlash(filePath))\n\n return relativePath.startsWith('../') ? relativePath : `./${relativePath}`\n}\n\n/**\n * Resolves to `true` when the file or directory at `path` exists.\n * Uses `Bun.file().exists()` when running under Bun, `fs.access` otherwise.\n *\n * @example\n * ```ts\n * if (await exists('./kubb.config.ts')) {\n * const content = await read('./kubb.config.ts')\n * }\n * ```\n */\nexport async function exists(path: string): Promise<boolean> {\n if (runtime.isBun) {\n return Bun.file(path).exists()\n }\n return access(path).then(\n () => true,\n () => false,\n )\n}\n\n/**\n * Reads the file at `path` as a UTF-8 string.\n * Uses `Bun.file().text()` when running under Bun, `fs.readFile` otherwise.\n *\n * @example\n * ```ts\n * const source = await read('./src/Pet.ts')\n * ```\n */\nexport async function read(path: string): Promise<string> {\n if (runtime.isBun) {\n return Bun.file(path).text()\n }\n return readFile(path, { encoding: 'utf8' })\n}\n\ntype WriteOptions = {\n /**\n * When `true`, re-reads the file immediately after writing and throws if the\n * content does not match — useful for catching write failures on unreliable file systems.\n */\n sanity?: boolean\n}\n\n/**\n * Writes `data` to `path`, trimming leading/trailing whitespace before saving.\n * Skips the write when the trimmed content is empty or identical to what is already on disk.\n * Creates any missing parent directories automatically.\n * When `sanity` is `true`, re-reads the file after writing and throws if the content does not match.\n *\n * @example\n * ```ts\n * await write('./src/Pet.ts', source) // writes and returns trimmed content\n * await write('./src/Pet.ts', source) // null — file unchanged\n * await write('./src/Pet.ts', ' ') // null — empty content skipped\n * ```\n */\nexport async function write(path: string, data: string, options: WriteOptions = {}): Promise<string | null> {\n const trimmed = data.trim()\n if (trimmed === '') return null\n\n const resolved = resolve(path)\n\n if (runtime.isBun) {\n const file = Bun.file(resolved)\n const oldContent = (await file.exists()) ? await file.text() : null\n if (oldContent === trimmed) return null\n await Bun.write(resolved, trimmed)\n return trimmed\n }\n\n try {\n const oldContent = await readFile(resolved, { encoding: 'utf-8' })\n if (oldContent === trimmed) return null\n } catch {\n /* file doesn't exist yet */\n }\n\n await mkdir(dirname(resolved), { recursive: true })\n await writeFile(resolved, trimmed, { encoding: 'utf-8' })\n\n if (options.sanity) {\n const savedData = await readFile(resolved, { encoding: 'utf-8' })\n if (savedData !== trimmed) {\n throw new Error(`Sanity check failed for ${path}\\n\\nData[${data.length}]:\\n${data}\\n\\nSaved[${savedData.length}]:\\n${savedData}\\n`)\n }\n return savedData\n }\n\n return trimmed\n}\n\n/**\n * Recursively removes `path`. Silently succeeds when `path` does not exist.\n *\n * @example\n * ```ts\n * await clean('./dist')\n * ```\n */\nexport async function clean(path: string): Promise<void> {\n return rm(path, { recursive: true, force: true })\n}\n\n/**\n * Converts a filesystem path to use POSIX (`/`) separators.\n *\n * Most of the codebase compares and composes paths as strings (prefix matching, joining for\n * import specifiers, splitting on `/`). On POSIX `path.resolve` already returns `/`-separated\n * paths, but on Windows it returns `\\`-separated paths, which breaks every such comparison.\n *\n * Routing every path that crosses a module boundary through `toPosixPath` keeps the rest of the\n * code platform-agnostic. The conversion runs unconditionally so Windows-specific behavior is\n * exercisable from POSIX CI.\n *\n * @example\n * toPosixPath('C:\\\\repo\\\\src\\\\pet.ts') // 'C:/repo/src/pet.ts'\n */\nexport function toPosixPath(filePath: string): string {\n return filePath.replaceAll('\\\\', '/')\n}\n\n/**\n * Strips the file extension from a path or file name.\n * Only removes the last `.ext` segment when the dot is not part of a directory name.\n *\n * @example\n * trimExtName('petStore.ts') // 'petStore'\n * trimExtName('/src/models/pet.ts') // '/src/models/pet'\n * trimExtName('/project.v2/gen/pet.ts') // '/project.v2/gen/pet'\n * trimExtName('noExtension') // 'noExtension'\n */\nexport function trimExtName(text: string): string {\n const dotIndex = text.lastIndexOf('.')\n if (dotIndex > 0 && !text.includes('/', dotIndex)) {\n return text.slice(0, dotIndex)\n }\n return text\n}\n\n/**\n * Builds a nested file path from a dotted name. Splits on dots that precede a letter\n * (so version numbers embedded in operationIds like `v2025.0` stay intact), camelCases\n * every earlier segment, applies `caseLast` to the final segment, and joins with `/`.\n *\n * Empty segments are dropped before joining. They arise when the name starts with a dot\n * followed by a letter (e.g. `..Schema` splits into `['..', 'Schema']` and `'..'` cases to\n * an empty string). Without this a leading `/` would form, which `path.resolve` reads as an\n * absolute path, letting generated files escape the configured output directory.\n *\n * @example Nested path from a dotted name\n * `toFilePath('pet.petId') // 'pet/petId'`\n *\n * @example PascalCase the final segment\n * `toFilePath('pet.Pet', pascalCase) // 'pet/Pet'`\n *\n * @example Suffix applied to the final segment only\n * `toFilePath('tag.tag', (part) => camelCase(part, { suffix: 'schema' })) // 'tag/tagSchema'`\n */\nexport function toFilePath(name: string, caseLast: (part: string) => string = camelCase): string {\n const parts = name.split(/\\.(?=[a-zA-Z])/)\n return parts\n .map((part, i) => (i === parts.length - 1 ? caseLast(part) : camelCase(part)))\n .filter(Boolean)\n .join('/')\n}\n","/**\n * Number of file writes to batch in parallel during `flushPendingFiles`.\n */\nexport const STREAM_FLUSH_EVERY = 50\n\n/**\n * Maximum number of █ characters in a plugin timing bar.\n */\nexport const SUMMARY_MAX_BAR_LENGTH = 10 as const\n\n/**\n * Divides elapsed milliseconds into bar-length units (1 block per 100 ms).\n */\nexport const SUMMARY_TIME_SCALE_DIVISOR = 100 as const\n\n/**\n * How many schema/operation nodes to generate between write flushes. Flushing as generation runs\n * lets writes reach disk before the build finishes, rather than queuing them all to the end.\n */\nexport const GENERATE_FLUSH_EVERY = 8\n\n/**\n * Upper bound of hook listeners a single plugin can add to one event (its schema, operation,\n * and operations generators, plus lifecycle hooks). Used to size the hooks emitter's\n * max-listener ceiling so a multi-generator plugin set does not trip Node's leak warning.\n */\nexport const HOOK_LISTENERS_PER_PLUGIN = 4\n\n/**\n * Plugin `include` filter types that select operations directly. When one of these is set\n * without a `schemaName` include, the generate phase pre-scans operations to compute the set\n * of schemas they reach, so unreachable schemas can be pruned for that plugin.\n */\nexport const OPERATION_FILTER_TYPES: ReadonlySet<string> = new Set(['tag', 'operationId', 'path', 'method', 'contentType'])\n\n/**\n * Stable codes Kubb attaches to a `Diagnostic`. Each maps to a known failure mode\n * and stays stable so it can be referenced in tooling and (later) docs. Reference\n * these instead of inlining the string at a throw site.\n */\nexport const diagnosticCode = {\n /**\n * Fallback for an unstructured error with no specific code.\n */\n unknown: 'KUBB_UNKNOWN',\n /**\n * The `input.path` file or URL could not be read.\n */\n inputNotFound: 'KUBB_INPUT_NOT_FOUND',\n /**\n * An adapter was configured without an `input`.\n */\n inputRequired: 'KUBB_INPUT_REQUIRED',\n /**\n * A `$ref` (or equivalent reference) could not be resolved in the source document.\n */\n refNotFound: 'KUBB_REF_NOT_FOUND',\n /**\n * A server variable value is not allowed by its `enum`.\n */\n invalidServerVariable: 'KUBB_INVALID_SERVER_VARIABLE',\n /**\n * A required plugin is missing from the config.\n */\n pluginNotFound: 'KUBB_PLUGIN_NOT_FOUND',\n /**\n * A plugin threw while generating.\n */\n pluginFailed: 'KUBB_PLUGIN_FAILED',\n /**\n * A plugin reported a non-fatal warning through `ctx.warn`.\n */\n pluginWarning: 'KUBB_PLUGIN_WARNING',\n /**\n * A plugin reported an informational message through `ctx.info`.\n */\n pluginInfo: 'KUBB_PLUGIN_INFO',\n /**\n * A schema uses a `format` Kubb does not map to a specific type. Reserved for\n * adapters to emit as a `warning`.\n */\n unsupportedFormat: 'KUBB_UNSUPPORTED_FORMAT',\n /**\n * A referenced schema or operation is marked `deprecated`. Reserved for adapters\n * to emit as an `info`.\n */\n deprecated: 'KUBB_DEPRECATED',\n /**\n * An adapter is required but the config has none. The build cannot read the input\n * without one.\n */\n adapterRequired: 'KUBB_ADAPTER_REQUIRED',\n /**\n * A resolved output path escapes the output directory, which can stem from a path\n * traversal in the spec or a misconfigured `group.name`.\n */\n pathTraversal: 'KUBB_PATH_TRAVERSAL',\n /**\n * A plugin's options are invalid, for example `output.mode: 'file'` paired with a `group` option.\n */\n invalidPluginOptions: 'KUBB_INVALID_PLUGIN_OPTIONS',\n /**\n * A post-generate shell hook (`hooks.done`) exited with a failure.\n */\n hookFailed: 'KUBB_HOOK_FAILED',\n /**\n * The formatter pass over the generated files failed.\n */\n formatFailed: 'KUBB_FORMAT_FAILED',\n /**\n * The linter pass over the generated files failed.\n */\n lintFailed: 'KUBB_LINT_FAILED',\n /**\n * Not a failure. Carries a plugin's elapsed time, summed into the run total.\n */\n performance: 'KUBB_PERFORMANCE',\n /**\n * Not a failure. A newer Kubb version is available on npm.\n */\n updateAvailable: 'KUBB_UPDATE_AVAILABLE',\n} as const\n\n/**\n * Union of the stable {@link diagnosticCode} values.\n */\nexport type DiagnosticCode = (typeof diagnosticCode)[keyof typeof diagnosticCode]\n","/**\n * Backend that persists generated files. Kubb ships with `fsStorage` (writes\n * to disk) and `memoryStorage` (keeps everything in RAM). Implement this\n * interface to write somewhere else, such as S3 or a database.\n */\nexport type Storage = {\n /**\n * Identifier used in logs and diagnostics (`'fs'`, `'memory'`, `'s3'`).\n */\n readonly name: string\n /**\n * Returns `true` when an entry for `key` exists.\n */\n hasItem(key: string): Promise<boolean>\n /**\n * Reads the stored string. Returns `null` when the key is missing.\n */\n getItem(key: string): Promise<string | null>\n /**\n * Stores `value` under `key`, creating any required structure (directories,\n * buckets, ...).\n */\n setItem(key: string, value: string): Promise<void>\n /**\n * Deletes the entry for `key`. No-op when the key does not exist.\n */\n removeItem(key: string): Promise<void>\n /**\n * Returns every key. Pass `base` to filter to keys starting with that prefix.\n */\n getKeys(base?: string): Promise<Array<string>>\n /**\n * Removes every entry. Pass `base` to scope the wipe to a key prefix.\n */\n clear(base?: string): Promise<void>\n /**\n * Optional teardown hook for a backend to flush buffers, close connections,\n * or release file locks.\n */\n dispose?(): Promise<void>\n}\n\n/**\n * Defines a custom storage backend. The builder receives user options and\n * returns a `Storage` implementation. Kubb ships with filesystem and in-memory\n * storages. A custom backend writes generated files elsewhere, such as cloud\n * storage or a database.\n *\n * @example In-memory storage (the built-in implementation)\n * ```ts\n * import { createStorage } from '@kubb/core'\n *\n * export const memoryStorage = createStorage(() => {\n * const store = new Map<string, string>()\n *\n * return {\n * name: 'memory',\n * async hasItem(key) {\n * return store.has(key)\n * },\n * async getItem(key) {\n * return store.get(key) ?? null\n * },\n * async setItem(key, value) {\n * store.set(key, value)\n * },\n * async removeItem(key) {\n * store.delete(key)\n * },\n * async getKeys(base) {\n * const keys = [...store.keys()]\n * return base ? keys.filter((k) => k.startsWith(base)) : keys\n * },\n * async clear(base) {\n * if (!base) store.clear()\n * },\n * }\n * })\n * ```\n */\nexport function createStorage<TOptions = Record<string, never>>(build: (options: TOptions) => Storage): (options?: TOptions) => Storage {\n return (options) => build(options ?? ({} as TOptions))\n}\n","import { AsyncEventEmitter } from '@internals/utils'\nimport { ast, type FileNode } from '@kubb/ast'\n\n/**\n * Hooks fired by a `FileManager`.\n *\n * - `upsert` fires once per resolved file added through `add` or `upsert`.\n */\nexport type FileManagerHooks = {\n upsert: [file: FileNode]\n}\n\nfunction mergeFile<TMeta extends object = object>(a: FileNode<TMeta>, b: FileNode<TMeta>): FileNode<TMeta> {\n return {\n ...a,\n // Incoming file (b) takes precedence for banner/footer so a barrel file (whose\n // banner/footer the barrel plugin resolves last) wins over a plugin-generated\n // file at the same path.\n banner: b.banner,\n footer: b.footer,\n // A verbatim-copy file cannot be merged with rendered content; the incoming `copy` wins.\n copy: b.copy ?? a.copy,\n sources: a.sources.length ? (b.sources.length ? [...a.sources, ...b.sources] : a.sources) : b.sources,\n imports: a.imports.length ? (b.imports.length ? [...a.imports, ...b.imports] : a.imports) : b.imports,\n exports: a.exports.length ? (b.exports.length ? [...a.exports, ...b.exports] : a.exports) : b.exports,\n }\n}\n\nfunction isIndexPath(path: string): boolean {\n return path.endsWith('/index.ts') || path === 'index.ts'\n}\n\n// Sort order: shortest path first. Within a length bucket, index.ts barrels last.\nfunction compareFiles(a: FileNode, b: FileNode): number {\n const lenDiff = a.path.length - b.path.length\n if (lenDiff !== 0) return lenDiff\n const aIsIndex = isIndexPath(a.path)\n const bIsIndex = isIndexPath(b.path)\n if (aIsIndex && !bIsIndex) return 1\n if (!aIsIndex && bIsIndex) return -1\n return 0\n}\n\n/**\n * In-memory file store for generated files. Files sharing a `path` are merged\n * (sources/imports/exports concatenated). The `files` getter is sorted by\n * path length (barrel `index.ts` last within a bucket).\n *\n * @example\n * ```ts\n * const manager = new FileManager()\n * manager.upsert(myFile)\n * manager.files // sorted view\n * ```\n */\nexport class FileManager {\n /**\n * Subscribe to file-store changes. Listeners on `upsert` see each resolved file as it lands\n * through `add` or `upsert`.\n */\n readonly hooks = new AsyncEventEmitter<FileManagerHooks>()\n readonly #cache = new Map<string, FileNode>()\n // Cached sorted view. Null means stale and rebuilt lazily on next `files` read.\n // Nulled (not mutated) on every write so callers holding a prior reference keep\n // their snapshot. `dispose()` must not silently empty an array the consumer\n // already holds.\n #sorted: Array<FileNode> | null = null\n\n add(...files: Array<FileNode>): Array<FileNode> {\n return this.#store(files, false)\n }\n\n upsert(...files: Array<FileNode>): Array<FileNode> {\n return this.#store(files, true)\n }\n\n #store(files: ReadonlyArray<FileNode>, mergeExisting: boolean): Array<FileNode> {\n const batch = files.length > 1 ? this.#dedupe(files) : files\n const resolved: Array<FileNode> = []\n\n for (const file of batch) {\n const existing = this.#cache.get(file.path)\n const merged = existing && mergeExisting ? ast.factory.createFile(mergeFile(existing, file)) : ast.factory.createFile(file)\n this.#cache.set(merged.path, merged)\n resolved.push(merged)\n this.hooks.emit('upsert', merged)\n }\n\n if (resolved.length > 0) this.#sorted = null\n return resolved\n }\n\n // Merges same-path entries within a batch so the cache update loop stays\n // uniform. Only called for multi-file batches.\n #dedupe(files: ReadonlyArray<FileNode>): Array<FileNode> {\n const seen = new Map<string, FileNode>()\n for (const file of files) {\n const prev = seen.get(file.path)\n seen.set(file.path, prev ? mergeFile(prev, file) : file)\n }\n return [...seen.values()]\n }\n\n getByPath(path: string): FileNode | null {\n return this.#cache.get(path) ?? null\n }\n\n deleteByPath(path: string): void {\n if (!this.#cache.delete(path)) return\n this.#sorted = null\n }\n\n clear(): void {\n this.#cache.clear()\n this.#sorted = null\n }\n\n /**\n * Releases all stored files and clears every `hooks` listener. Called by the core after\n * `kubb:build:end`.\n */\n dispose(): void {\n this.clear()\n this.hooks.removeAll()\n }\n\n [Symbol.dispose](): void {\n this.dispose()\n }\n\n /**\n * All stored files in stable sort order (shortest path first, barrel files\n * last within a length bucket). Returns a cached view, do not mutate.\n */\n get files(): Array<FileNode> {\n return (this.#sorted ??= [...this.#cache.values()].sort(compareFiles))\n }\n}\n","import { AsyncEventEmitter, read } from '@internals/utils'\nimport type { CodeNode, FileNode } from '@kubb/ast'\nimport { extractStringsFromNodes } from '@kubb/ast/utils'\nimport { STREAM_FLUSH_EVERY } from './constants.ts'\nimport type { Storage } from './createStorage.ts'\nimport type { Parser } from './defineParser.ts'\n\n/**\n * Hooks fired by a `FileProcessor`.\n *\n * - `start` opens a batch, from `run` or a queue flush.\n * - `update` fires once per file as it is converted.\n * - `end` closes a batch.\n * - `enqueue` fires for every `enqueue` call.\n * - `drain` fires when `drain()` empties the queue with no in-flight batch left.\n */\nexport type FileProcessorHooks = {\n start: [files: Array<FileNode>]\n update: [params: { file: FileNode; source?: string; processed: number; total: number; percentage: number }]\n end: [files: Array<FileNode>]\n enqueue: [file: FileNode]\n drain: []\n}\n\n/**\n * Per-file progress record yielded by `stream` and surfaced through the `update` event.\n */\nexport type ParsedFile = {\n file: FileNode\n source: string\n processed: number\n total: number\n percentage: number\n}\n\ntype FileProcessorOptions = {\n /**\n * Storage destination for queued writes.\n */\n storage: Storage\n /**\n * Parsers indexed by file extension.\n */\n parsers?: Map<FileNode['extname'], Parser>\n /**\n * Output extname per source extname, applied during conversion.\n */\n extension?: Record<FileNode['extname'], FileNode['extname'] | ''>\n}\n\nfunction joinSources(file: FileNode): string {\n const sources = file.sources\n if (sources.length === 0) return ''\n const parts: Array<string> = []\n for (const source of sources) {\n const text = extractStringsFromNodes(source.nodes as Array<CodeNode>)\n if (text) parts.push(text)\n }\n return parts.join('\\n\\n')\n}\n\nasync function parseCopy(file: FileNode): Promise<string> {\n let content: string\n try {\n content = await read(file.copy as string)\n } catch (err) {\n throw new Error(`[kubb] Could not copy file into output: ${file.copy}`, { cause: err })\n }\n\n return [file.banner, content, file.footer]\n .filter((segment): segment is string => Boolean(segment))\n .map((segment) => segment.trimEnd())\n .join('\\n')\n}\n\n/**\n * Turns `FileNode`s into source strings and writes them to storage.\n *\n * Two modes share the same instance. Stateless mode (`parse`, `stream`, `run`) just runs the\n * conversion. Queue mode (`enqueue`, `flush`, `drain`) buffers files deduped by path and\n * writes each batch through storage with up to `STREAM_FLUSH_EVERY` requests in flight.\n *\n * `flush` does not wait for its batch to finish, so dispatch can overlap with IO. The next\n * `flush` or `drain` picks the in-flight batch up. `drain` blocks until everything has been\n * written and is meant for the end of a build.\n *\n * To surface build-level hook signals (`kubb:files:processing:*` and friends) subscribe to\n * `hooks` and re-emit on the kubb bus.\n */\nexport class FileProcessor {\n readonly hooks = new AsyncEventEmitter<FileProcessorHooks>()\n readonly #parsers: Map<FileNode['extname'], Parser> | null\n readonly #storage: Storage\n readonly #extension: Record<FileNode['extname'], FileNode['extname'] | ''> | null\n readonly #pending = new Map<string, FileNode>()\n #runningFlush: Promise<void> | null = null\n\n constructor(options: FileProcessorOptions) {\n this.#parsers = options.parsers ?? null\n this.#storage = options.storage\n this.#extension = options.extension ?? null\n }\n\n /**\n * Files waiting in the queue.\n */\n get size(): number {\n return this.#pending.size\n }\n\n async parse(file: FileNode): Promise<string> {\n if (file.copy) {\n return parseCopy(file)\n }\n\n const parsers = this.#parsers\n const parseExtName = this.#extension?.[file.extname] || undefined\n\n if (!parsers || !file.extname) {\n return joinSources(file)\n }\n\n const parser = parsers.get(file.extname)\n\n if (!parser) {\n return joinSources(file)\n }\n\n return parser.parse(file, { extname: parseExtName })\n }\n\n async *stream(files: ReadonlyArray<FileNode>): AsyncGenerator<ParsedFile> {\n const total = files.length\n if (total === 0) return\n\n let processed = 0\n for (const file of files) {\n const source = await this.parse(file)\n processed++\n\n yield { file, source, processed, total, percentage: (processed / total) * 100 }\n }\n }\n\n async run(files: Array<FileNode>): Promise<Array<FileNode>> {\n await this.hooks.emit('start', files)\n\n for await (const { file, source, processed, total, percentage } of this.stream(files)) {\n await this.hooks.emit('update', { file, source, processed, percentage, total })\n }\n\n await this.hooks.emit('end', files)\n\n return files\n }\n\n /**\n * Adds a file to the next flush. A later `enqueue` for the same path replaces the previous\n * entry, matching `FileManager.upsert`. Fires the `enqueue` event.\n */\n enqueue(file: FileNode): void {\n this.#pending.set(file.path, file)\n this.hooks.emit('enqueue', file)\n }\n\n /**\n * Starts processing the queued files. Waits for any previous flush to finish (so two\n * batches never run together) and then returns without waiting for the new one. The next\n * `flush` or `drain` picks up the in-flight task.\n */\n async flush(): Promise<void> {\n if (this.#runningFlush) await this.#runningFlush\n if (this.#pending.size === 0) return\n\n const batch = [...this.#pending.values()]\n this.#pending.clear()\n\n this.#runningFlush = this.#processAndWrite(batch).finally(() => {\n this.#runningFlush = null\n })\n }\n\n /**\n * Waits for the in-flight flush and writes any files still queued. Fires the `drain` event\n * when both are done.\n */\n async drain(): Promise<void> {\n if (this.#runningFlush) await this.#runningFlush\n\n if (this.#pending.size > 0) {\n const batch = [...this.#pending.values()]\n this.#pending.clear()\n await this.#processAndWrite(batch)\n }\n\n await this.hooks.emit('drain')\n }\n\n async #processAndWrite(files: Array<FileNode>): Promise<void> {\n const storage = this.#storage\n\n await this.hooks.emit('start', files)\n\n // Single pass: each file's write starts right after its `update` fires, so IO overlaps\n // parsing and the batch never holds every rendered source in memory at once.\n const queue: Array<Promise<void>> = []\n for await (const item of this.stream(files)) {\n await this.hooks.emit('update', item)\n if (item.source) {\n queue.push(storage.setItem(item.file.path, item.source))\n if (queue.length >= STREAM_FLUSH_EVERY) await Promise.all(queue.splice(0))\n }\n }\n await Promise.all(queue)\n\n await this.hooks.emit('end', files)\n }\n\n /**\n * Clears every listener and the pending queue.\n */\n dispose(): void {\n this.hooks.removeAll()\n this.#pending.clear()\n }\n\n [Symbol.dispose](): void {\n this.dispose()\n }\n}\n","import { createStorage } from '../createStorage.ts'\n\n/**\n * In-memory storage driver. Useful for testing and dry-run scenarios where\n * generated output should be captured without touching the filesystem.\n *\n * All data lives in a `Map` scoped to the storage instance and is discarded\n * when the instance is garbage-collected.\n *\n * @example\n * ```ts\n * import { memoryStorage } from '@kubb/core'\n * import { defineConfig } from 'kubb'\n *\n * export default defineConfig({\n * input: { path: './petStore.yaml' },\n * output: { path: './src/gen' },\n * storage: memoryStorage(),\n * })\n * ```\n */\nexport const memoryStorage = createStorage(() => {\n const store = new Map<string, string>()\n\n return {\n name: 'memory',\n async hasItem(key: string) {\n return store.has(key)\n },\n async getItem(key: string) {\n return store.get(key) ?? null\n },\n async setItem(key: string, value: string) {\n store.set(key, value)\n },\n async removeItem(key: string) {\n store.delete(key)\n },\n async getKeys(base?: string) {\n const keys = [...store.keys()]\n return base ? keys.filter((k) => k.startsWith(base)) : keys\n },\n async clear(base?: string) {\n if (!base) {\n store.clear()\n return\n }\n for (const key of store.keys()) {\n if (key.startsWith(base)) {\n store.delete(key)\n }\n }\n },\n }\n})\n"],"mappings":"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"} |
| //#region \0rolldown/runtime.js | ||
| var __defProp = Object.defineProperty; | ||
| var __name = (target, value) => __defProp(target, "name", { | ||
| value, | ||
| configurable: true | ||
| }); | ||
| //#endregion | ||
| export { __name as t }; |
+74
-272
@@ -1,7 +0,6 @@ | ||
| import { t as __name } from "./chunk--u3MIqq1.js"; | ||
| import { $ as defineParser, A as KubbPluginStartContext, B as Override, C as KubbGenerationSummaryContext, D as KubbLifecycleStartContext, E as KubbInfoContext, F as Logger, G as ResolveOptionsContext, H as PossibleConfig, I as LoggerContext, J as ResolverFileParams, K as Resolver, L as LoggerOptions, M as KubbSuccessContext, N as KubbVersionNewContext, O as KubbPluginEndContext, P as KubbWarnContext, Q as Parser, R as NormalizedPlugin, S as KubbGenerationStartContext, T as KubbHookStartContext, U as ResolveBannerContext, V as PluginFactoryOptions, W as ResolveNameParams, X as UserConfig, Y as ResolverPathParams, Z as UserLogger, _ as KubbErrorContext, _t as logLevel, a as Config, at as createKubb, b as KubbFilesProcessingStartContext, c as GeneratorContext, ct as Plugin, d as InputData, dt as defineGenerator, et as Middleware, f as InputPath, ft as Storage, g as KubbDebugContext, gt as createRenderer, h as KubbConfigEndContext, ht as RendererFactory, i as CLIOptions, it as BuildOutput, j as KubbPluginsEndContext, k as KubbPluginSetupContext, l as Group, lt as definePlugin, m as KubbBuildStartContext, mt as Renderer, n as AdapterFactoryOptions, nt as Kubb, o as DevtoolsOptions, ot as PluginDriver, p as KubbBuildEndContext, pt as createStorage, q as ResolverContext, r as AdapterSource, rt as KubbHooks, s as Exclude, st as FileManager, t as Adapter, tt as defineMiddleware, u as Include, ut as Generator, v as KubbFileProcessingUpdateContext, vt as AsyncEventEmitter, w as KubbHookEndContext, x as KubbGenerationEndContext, y as KubbFilesProcessingEndContext, z as Output } from "./types-CuNocrbJ.js"; | ||
| import * as ast from "@kubb/ast"; | ||
| import { FileNode, InputNode, Node } from "@kubb/ast"; | ||
| import { t as __name } from "./rolldown-runtime-C0LytTxp.js"; | ||
| import { $ as KubbPluginSetupContext, A as KubbHookStartContext, At as Adapter, B as ParsedFile, C as KubbFilesProcessingEndContext, Ct as GenerationResult, D as KubbGenerationStartContext, Dt as UserReporter, E as KubbGenerationEndContext, Et as ReporterName, F as KubbSuccessContext, G as Generator, H as createKubb, I as KubbWarnContext, J as KubbDriver, K as GeneratorContext, L as PossibleConfig, M as KubbInfoContext, Mt as AdapterSource, N as KubbLifecycleStartContext, Nt as createAdapter, O as KubbHookEndContext, Ot as createReporter, P as KubbPluginsEndContext, Pt as AsyncEventEmitter, Q as KubbPluginEndContext, R as UserConfig, S as KubbFileProcessingUpdate, St as createStorage, T as KubbFilesProcessingUpdateContext, Tt as ReporterContext, U as Parser, V as Kubb, W as defineParser, X as Group, Y as Exclude, Z as Include, _ as InputPath, _t as defineResolver, a as DiagnosticLocation, at as Override, b as KubbDiagnosticContext, bt as createRenderer, c as PerformanceDiagnostic, ct as definePlugin, d as SerializedDiagnostic, dt as ResolveBannerFile, et as KubbPluginStartContext, f as UpdateDiagnostic, ft as ResolveOptionsContext, g as InputData, gt as ResolverPathParams, h as Config, ht as ResolverFileParams, i as DiagnosticKind, it as OutputOptions, j as KubbHooks, jt as AdapterFactoryOptions, k as KubbHookLineContext, kt as logLevel, l as ProblemCode, lt as BannerMeta, m as CLIOptions, mt as ResolverContext, n as DiagnosticByCode, nt as Output, o as DiagnosticSeverity, ot as Plugin, p as BuildOutput, pt as Resolver, q as defineGenerator, r as DiagnosticDoc, rt as OutputMode, s as Diagnostics, st as PluginFactoryOptions, t as Diagnostic, tt as NormalizedPlugin, u as ProblemDiagnostic, ut as ResolveBannerContext, v as KubbBuildEndContext, vt as Renderer, w as KubbFilesProcessingStartContext, wt as Reporter, x as KubbErrorContext, xt as Storage, y as KubbBuildStartContext, yt as RendererFactory, z as FileProcessorHooks } from "./diagnostics-CQYd4UQZ.js"; | ||
| import { ast } from "@kubb/ast"; | ||
| //#region ../../internals/utils/src/urlPath.d.ts | ||
| //#region ../../internals/utils/src/url.d.ts | ||
| type URLObject = { | ||
@@ -13,6 +12,24 @@ /** | ||
| /** | ||
| * Extracted path parameters as a key-value map, or `undefined` when the path has none. | ||
| * Extracted path parameters as a key-value map, or `null` when the path has none. | ||
| */ | ||
| params?: Record<string, string>; | ||
| params: Record<string, string> | null; | ||
| }; | ||
| /** | ||
| * Supported identifier casing strategies for path parameters. | ||
| */ | ||
| type PathCasing = 'camelcase'; | ||
| type TemplateOptions = { | ||
| /** | ||
| * Literal text prepended inside the template literal, e.g. a base URL. | ||
| */ | ||
| prefix?: string | null; | ||
| /** | ||
| * Transform applied to each extracted parameter name before interpolation. | ||
| */ | ||
| replacer?: (pathParam: string) => string; | ||
| /** | ||
| * Casing strategy applied to path parameter names. | ||
| */ | ||
| casing?: PathCasing; | ||
| }; | ||
| type ObjectOptions = { | ||
@@ -25,3 +42,3 @@ /** | ||
| /** | ||
| * Optional transform applied to each extracted parameter name. | ||
| * Transform applied to each extracted parameter name. | ||
| */ | ||
@@ -33,11 +50,4 @@ replacer?: (pathParam: string) => string; | ||
| stringify?: boolean; | ||
| }; | ||
| /** | ||
| * Supported identifier casing strategies for path parameters. | ||
| */ | ||
| type PathCasing = 'camelcase'; | ||
| type Options = { | ||
| /** | ||
| * Casing strategy applied to path parameter names. | ||
| * @default undefined (original identifier preserved) | ||
| */ | ||
@@ -47,275 +57,74 @@ casing?: PathCasing; | ||
| /** | ||
| * Parses and transforms an OpenAPI/Swagger path string into various URL formats. | ||
| * | ||
| * @example | ||
| * const p = new URLPath('/pet/{petId}') | ||
| * p.URL // '/pet/:petId' | ||
| * p.template // '`/pet/${petId}`' | ||
| * Helpers for OpenAPI/Swagger paths, plus a thin wrapper over the native `URL`. | ||
| */ | ||
| declare class URLPath { | ||
| #private; | ||
| declare class Url { | ||
| /** | ||
| * The raw OpenAPI/Swagger path string, e.g. `/pet/{petId}`. | ||
| */ | ||
| path: string; | ||
| constructor(path: string, options?: Options); | ||
| /** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`. | ||
| * Converts an OpenAPI/Swagger path to Express-style colon syntax. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * new URLPath('/pet/{petId}').URL // '/pet/:petId' | ||
| * ``` | ||
| * Url.toPath('/pet/{petId}') // '/pet/:petId' | ||
| */ | ||
| get URL(): string; | ||
| /** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`). | ||
| static toPath(path: string): string; | ||
| /** | ||
| * Converts an OpenAPI/Swagger path to a TypeScript template literal string. | ||
| * `prefix` is prepended inside the literal, `replacer` transforms each parameter name, | ||
| * and `casing` controls parameter identifier casing. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * new URLPath('https://petstore.swagger.io/v2/pet').isURL // true | ||
| * new URLPath('/pet/{petId}').isURL // false | ||
| * ``` | ||
| */ | ||
| get isURL(): boolean; | ||
| /** | ||
| * Converts the OpenAPI path to a TypeScript template literal string. | ||
| * Url.toTemplateString('/pet/{petId}') // '`/pet/${petId}`' | ||
| * | ||
| * @example | ||
| * new URLPath('/pet/{petId}').template // '`/pet/${petId}`' | ||
| * new URLPath('/account/monetary-accountID').template // '`/account/${monetaryAccountId}`' | ||
| * Url.toTemplateString('/pet/{petId}', { prefix: 'https://api' }) // '`https://api/pet/${petId}`' | ||
| */ | ||
| get template(): string; | ||
| /** Returns the path and its extracted params as a structured `URLObject`, or as a stringified expression when `stringify` is set. | ||
| static toTemplateString(path: string, { | ||
| prefix, | ||
| replacer, | ||
| casing | ||
| }?: TemplateOptions): string; | ||
| /** | ||
| * Returns the path and its extracted params as a structured `URLObject`, or as a stringified | ||
| * expression when `stringify` is set. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * new URLPath('/pet/{petId}').object | ||
| * Url.toObject('/pet/{petId}') | ||
| * // { url: '/pet/:petId', params: { petId: 'petId' } } | ||
| * ``` | ||
| */ | ||
| get object(): URLObject | string; | ||
| /** Returns a map of path parameter names, or `undefined` when the path has no parameters. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * new URLPath('/pet/{petId}').params // { petId: 'petId' } | ||
| * new URLPath('/pet').params // undefined | ||
| * ``` | ||
| */ | ||
| get params(): Record<string, string> | undefined; | ||
| toObject({ | ||
| static toObject(path: string, { | ||
| type, | ||
| replacer, | ||
| stringify | ||
| stringify, | ||
| casing | ||
| }?: ObjectOptions): URLObject | string; | ||
| /** | ||
| * Converts the OpenAPI path to a TypeScript template literal string. | ||
| * An optional `replacer` can transform each extracted parameter name before interpolation. | ||
| * | ||
| * @example | ||
| * new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`' | ||
| */ | ||
| toTemplateString({ | ||
| prefix, | ||
| replacer | ||
| }?: { | ||
| prefix?: string; | ||
| replacer?: (pathParam: string) => string; | ||
| }): string; | ||
| /** | ||
| * Extracts all `{param}` segments from the path and returns them as a key-value map. | ||
| * An optional `replacer` transforms each parameter name in both key and value positions. | ||
| * Returns `undefined` when no path parameters are found. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * new URLPath('/pet/{petId}/tag/{tagId}').getParams() | ||
| * // { petId: 'petId', tagId: 'tagId' } | ||
| * ``` | ||
| */ | ||
| getParams(replacer?: (pathParam: string) => string): Record<string, string> | undefined; | ||
| /** Converts the OpenAPI path to Express-style colon syntax. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * new URLPath('/pet/{petId}').toURLPath() // '/pet/:petId' | ||
| * ``` | ||
| */ | ||
| toURLPath(): string; | ||
| } | ||
| //#endregion | ||
| //#region src/createAdapter.d.ts | ||
| type AdapterBuilder<T extends AdapterFactoryOptions> = (options: T['options']) => Adapter<T>; | ||
| //#region src/reporters/cliReporter.d.ts | ||
| /** | ||
| * Factory for implementing custom adapters that translate non-OpenAPI specs into Kubb's AST. | ||
| * | ||
| * Use this to support GraphQL schemas, gRPC definitions, AsyncAPI, or custom domain-specific languages. | ||
| * Built-in adapters include `@kubb/adapter-oas` for OpenAPI and Swagger documents. | ||
| * | ||
| * @note Adapters must parse their input format to Kubb's `InputNode` structure. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * export const myAdapter = createAdapter<MyAdapter>((options) => { | ||
| * return { | ||
| * name: 'my-adapter', | ||
| * options, | ||
| * async parse(source) { | ||
| * // Transform source format to InputNode | ||
| * return { ... } | ||
| * }, | ||
| * } | ||
| * }) | ||
| * | ||
| * // Instantiate: | ||
| * const adapter = myAdapter({ validate: true }) | ||
| * ``` | ||
| * The default `cli` reporter. Renders the {@link Report} for each config as it finishes, independent | ||
| * of the live logger view. Suppressed at `silent`. The `verbose` level adds the per-plugin timings. | ||
| */ | ||
| declare function createAdapter<T extends AdapterFactoryOptions = AdapterFactoryOptions>(build: AdapterBuilder<T>): (options?: T['options']) => Adapter<T>; | ||
| declare const cliReporter: Reporter; | ||
| //#endregion | ||
| //#region src/defineLogger.d.ts | ||
| //#region src/reporters/fileReporter.d.ts | ||
| /** | ||
| * Wraps a logger definition into a typed {@link Logger}. | ||
| * The `file` reporter. Writes a config's {@link Report} to `.kubb/kubb-<name>-<timestamp>.log` as a | ||
| * plain-text document: a `# <name> — <timestamp>` header, a `## Summary` with the same counts the | ||
| * cli and json reporters expose, a `## Problems` section in the miette block format, and a | ||
| * `## Timings` section. Selected with `--reporter file` (or `reporters: ['file']`). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * export const myLogger = defineLogger({ | ||
| * name: 'my-logger', | ||
| * install(context, options) { | ||
| * context.on('kubb:info', (message) => console.log('ℹ', message)) | ||
| * context.on('kubb:error', (error) => console.error('✗', error.message)) | ||
| * }, | ||
| * }) | ||
| * ``` | ||
| * @note It captures the collected diagnostics once a config finishes, not the live | ||
| * `kubb:info`/`kubb:plugin` event stream. Color is stripped so the file stays plain text even when | ||
| * the run is attached to a TTY. | ||
| */ | ||
| declare function defineLogger<Options extends LoggerOptions = LoggerOptions>(logger: UserLogger<Options>): Logger<Options>; | ||
| declare const fileReporter: Reporter; | ||
| //#endregion | ||
| //#region src/defineResolver.d.ts | ||
| //#region src/reporters/jsonReporter.d.ts | ||
| /** | ||
| * Builder type for the plugin-specific resolver fields. | ||
| * | ||
| * `default`, `resolveOptions`, `resolvePath`, `resolveFile`, `resolveBanner`, and `resolveFooter` | ||
| * are optional — built-in fallbacks are injected when omitted. | ||
| * | ||
| * The builder receives `ctx` — a reference to the fully assembled resolver — so methods can | ||
| * call sibling resolver methods without using `this`. Because `ctx` is captured by the closure | ||
| * and the resolver is populated after the builder runs, `ctx` correctly reflects any overrides | ||
| * that were applied by the builder itself. | ||
| * The `json` reporter. `report` returns one config's {@link Report}, which {@link createReporter} | ||
| * buffers, and `drain` writes them as a single pretty-printed JSON array on `kubb:lifecycle:end`. | ||
| * Buffering keeps a multi-config run one valid JSON document on stdout instead of concatenated | ||
| * objects that would break `jq .`. The terminal reporter is suppressed while `json` is active so | ||
| * stdout stays valid JSON. | ||
| */ | ||
| type ResolverBuilder<T extends PluginFactoryOptions> = (ctx: T['resolver']) => Omit<T['resolver'], 'default' | 'resolveOptions' | 'resolvePath' | 'resolveFile' | 'resolveBanner' | 'resolveFooter' | 'name' | 'pluginName'> & Partial<Pick<T['resolver'], 'default' | 'resolveOptions' | 'resolvePath' | 'resolveFile' | 'resolveBanner' | 'resolveFooter'>> & { | ||
| name: string; | ||
| pluginName: T['name']; | ||
| }; | ||
| /** | ||
| * Default option resolver — applies include/exclude filters and merges matching override options. | ||
| * | ||
| * Returns `null` when the node is filtered out by an `exclude` rule or not matched by any `include` rule. | ||
| * | ||
| * @example Include/exclude filtering | ||
| * ```ts | ||
| * const options = defaultResolveOptions(operationNode, { | ||
| * options: { output: 'types' }, | ||
| * exclude: [{ type: 'tag', pattern: 'internal' }], | ||
| * }) | ||
| * // → null when node has tag 'internal' | ||
| * ``` | ||
| * | ||
| * @example Override merging | ||
| * ```ts | ||
| * const options = defaultResolveOptions(operationNode, { | ||
| * options: { enumType: 'asConst' }, | ||
| * override: [{ type: 'operationId', pattern: 'listPets', options: { enumType: 'enum' } }], | ||
| * }) | ||
| * // → { enumType: 'enum' } when operationId matches | ||
| * ``` | ||
| */ | ||
| /** | ||
| * Defines a resolver for a plugin, injecting built-in defaults for name casing, | ||
| * include/exclude/override filtering, path resolution, and file construction. | ||
| * | ||
| * All four defaults can be overridden by providing them in the builder function: | ||
| * - `default` — name casing strategy (camelCase / PascalCase) | ||
| * - `resolveOptions` — include/exclude/override filtering | ||
| * - `resolvePath` — output path computation | ||
| * - `resolveFile` — full `FileNode` construction | ||
| * | ||
| * The builder receives `ctx` — a reference to the assembled resolver — so methods can | ||
| * call sibling resolver methods using `ctx` instead of `this`. | ||
| * | ||
| * @example Basic resolver with naming helpers | ||
| * ```ts | ||
| * export const resolver = defineResolver<PluginTs>((ctx) => ({ | ||
| * name: 'default', | ||
| * resolveName(node) { | ||
| * return ctx.default(node.name, 'function') | ||
| * }, | ||
| * resolveTypedName(node) { | ||
| * return ctx.default(node.name, 'type') | ||
| * }, | ||
| * })) | ||
| * ``` | ||
| * | ||
| * @example Override resolvePath for a custom output structure | ||
| * ```ts | ||
| * export const resolver = defineResolver<PluginTs>((_ctx) => ({ | ||
| * name: 'custom', | ||
| * resolvePath({ baseName }, { root, output }) { | ||
| * return path.resolve(root, output.path, 'generated', baseName) | ||
| * }, | ||
| * })) | ||
| * ``` | ||
| * | ||
| * @example Use ctx.default inside a helper | ||
| * ```ts | ||
| * export const resolver = defineResolver<PluginTs>((ctx) => ({ | ||
| * name: 'default', | ||
| * resolveParamName(node, param) { | ||
| * return ctx.default(`${node.operationId} ${param.in} ${param.name}`, 'type') | ||
| * }, | ||
| * })) | ||
| * ``` | ||
| */ | ||
| declare function defineResolver<T extends PluginFactoryOptions>(build: ResolverBuilder<T>): T['resolver']; | ||
| declare const jsonReporter: Reporter; | ||
| //#endregion | ||
| //#region src/FileProcessor.d.ts | ||
| type ParseOptions = { | ||
| parsers?: Map<FileNode['extname'], Parser>; | ||
| extension?: Record<FileNode['extname'], FileNode['extname'] | ''>; | ||
| }; | ||
| type RunOptions = ParseOptions & { | ||
| /** | ||
| * @default 'sequential' | ||
| */ | ||
| mode?: 'sequential' | 'parallel'; | ||
| onStart?: (files: Array<FileNode>) => Promise<void> | void; | ||
| onEnd?: (files: Array<FileNode>) => Promise<void> | void; | ||
| onUpdate?: (params: { | ||
| file: FileNode; | ||
| source?: string; | ||
| processed: number; | ||
| total: number; | ||
| percentage: number; | ||
| }) => Promise<void> | void; | ||
| }; | ||
| /** | ||
| * Converts a single file to a string using the registered parsers. | ||
| * Falls back to joining source values when no matching parser is found. | ||
| * | ||
| * @internal | ||
| */ | ||
| declare class FileProcessor { | ||
| #private; | ||
| parse(file: FileNode, { | ||
| parsers, | ||
| extension | ||
| }?: ParseOptions): Promise<string>; | ||
| run(files: Array<FileNode>, { | ||
| parsers, | ||
| mode, | ||
| extension, | ||
| onStart, | ||
| onEnd, | ||
| onUpdate | ||
| }?: RunOptions): Promise<Array<FileNode>>; | ||
| } | ||
| //#endregion | ||
| //#region src/storages/fsStorage.d.ts | ||
@@ -329,7 +138,7 @@ /** | ||
| * | ||
| * Internally uses the `write` utility from `@internals/utils`, which: | ||
| * - trims leading/trailing whitespace before writing | ||
| * - skips the write when file content is already identical (deduplication) | ||
| * - creates missing parent directories automatically | ||
| * - supports Bun's native file API when running under Bun | ||
| * Writes are deduplicated and directory-safe: | ||
| * - leading and trailing whitespace is trimmed before writing | ||
| * - the write is skipped when the file content is already identical | ||
| * - missing parent directories are created automatically | ||
| * - Bun's native file API is used when running under Bun | ||
| * | ||
@@ -372,10 +181,3 @@ * @example | ||
| //#endregion | ||
| //#region src/utils/isInputPath.d.ts | ||
| /** | ||
| * Type guard to check if a given config has an `input.path`. | ||
| */ | ||
| declare function isInputPath(config: UserConfig | undefined): config is UserConfig<InputPath>; | ||
| declare function isInputPath(config: Config | undefined): config is Config<InputPath>; | ||
| //#endregion | ||
| export { Adapter, AdapterFactoryOptions, AdapterSource, AsyncEventEmitter, BuildOutput, CLIOptions, Config, DevtoolsOptions, Exclude, FileManager, FileProcessor, Generator, GeneratorContext, Group, Include, InputData, InputPath, Kubb, KubbBuildEndContext, KubbBuildStartContext, KubbConfigEndContext, KubbDebugContext, KubbErrorContext, KubbFileProcessingUpdateContext, KubbFilesProcessingEndContext, KubbFilesProcessingStartContext, KubbGenerationEndContext, KubbGenerationStartContext, KubbGenerationSummaryContext, KubbHookEndContext, KubbHookStartContext, KubbHooks, KubbInfoContext, KubbLifecycleStartContext, KubbPluginEndContext, KubbPluginSetupContext, KubbPluginStartContext, KubbPluginsEndContext, KubbSuccessContext, KubbVersionNewContext, KubbWarnContext, Logger, LoggerContext, LoggerOptions, Middleware, NormalizedPlugin, Output, Override, Parser, Plugin, PluginDriver, PluginFactoryOptions, PossibleConfig, Renderer, RendererFactory, ResolveBannerContext, ResolveNameParams, ResolveOptionsContext, Resolver, ResolverContext, ResolverFileParams, ResolverPathParams, Storage, URLPath, UserConfig, UserLogger, ast, createAdapter, createKubb, createRenderer, createStorage, defineGenerator, defineLogger, defineMiddleware, defineParser, definePlugin, defineResolver, fsStorage, isInputPath, logLevel, memoryStorage }; | ||
| export { type Adapter, type AdapterFactoryOptions, type AdapterSource, AsyncEventEmitter, type BannerMeta, BuildOutput, CLIOptions, Config, type Diagnostic, type DiagnosticByCode, type DiagnosticDoc, type DiagnosticKind, type DiagnosticLocation, type DiagnosticSeverity, Diagnostics, type Exclude, type FileProcessorHooks, type GenerationResult, type Generator, type GeneratorContext, type Group, type Include, InputData, InputPath, type Kubb, KubbBuildEndContext, KubbBuildStartContext, KubbDiagnosticContext, KubbDriver, KubbErrorContext, KubbFileProcessingUpdate, KubbFilesProcessingEndContext, KubbFilesProcessingStartContext, KubbFilesProcessingUpdateContext, KubbGenerationEndContext, KubbGenerationStartContext, KubbHookEndContext, KubbHookLineContext, KubbHookStartContext, KubbHooks, KubbInfoContext, KubbLifecycleStartContext, type KubbPluginEndContext, type KubbPluginSetupContext, type KubbPluginStartContext, KubbPluginsEndContext, KubbSuccessContext, KubbWarnContext, type NormalizedPlugin, type Output, type OutputMode, type OutputOptions, type Override, type ParsedFile, type Parser, type PerformanceDiagnostic, type Plugin, type PluginFactoryOptions, PossibleConfig, type ProblemCode, type ProblemDiagnostic, type Renderer, type RendererFactory, type Reporter, type ReporterContext, type ReporterName, type ResolveBannerContext, type ResolveBannerFile, type ResolveOptionsContext, type Resolver, type ResolverContext, type ResolverFileParams, type ResolverPathParams, type SerializedDiagnostic, type Storage, type UpdateDiagnostic, Url, UserConfig, type UserReporter, ast, cliReporter, createAdapter, createKubb, createRenderer, createReporter, createStorage, defineGenerator, defineParser, definePlugin, defineResolver, fileReporter, fsStorage, jsonReporter, logLevel, memoryStorage }; | ||
| //# sourceMappingURL=index.d.ts.map |
+84
-24
| Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); | ||
| const require_PluginDriver = require("./PluginDriver-BXibeQk-.cjs"); | ||
| const require_memoryStorage = require("./memoryStorage-Bk7RHgGj.cjs"); | ||
| let node_path = require("node:path"); | ||
| node_path = require_memoryStorage.__toESM(node_path, 1); | ||
| let _kubb_ast = require("@kubb/ast"); | ||
| let vitest = require("vitest"); | ||
| //#region src/mocks.ts | ||
| /** | ||
| * Creates a minimal `PluginDriver` mock for unit tests. | ||
| * Creates a minimal `KubbDriver` mock for unit tests. | ||
| */ | ||
| function createMockedPluginDriver(options = {}) { | ||
| const fileManager = new require_memoryStorage.FileManager(); | ||
| return { | ||
@@ -20,3 +22,25 @@ config: options?.config ?? { | ||
| getResolver: (_pluginName) => options?.plugin?.resolver, | ||
| fileManager: new require_PluginDriver.FileManager() | ||
| fileManager, | ||
| async dispatch({ result, renderer }) { | ||
| try { | ||
| var _usingCtx$1 = require_memoryStorage._usingCtx(); | ||
| if (!result) return; | ||
| if (Array.isArray(result)) { | ||
| fileManager.upsert(...result); | ||
| return; | ||
| } | ||
| if (!renderer) return; | ||
| const instance = _usingCtx$1.u(renderer()); | ||
| if (instance.stream) { | ||
| for (const file of instance.stream(result)) fileManager.upsert(file); | ||
| return; | ||
| } | ||
| await instance.render(result); | ||
| fileManager.upsert(...instance.files); | ||
| } catch (_) { | ||
| _usingCtx$1.e = _; | ||
| } finally { | ||
| _usingCtx$1.d(); | ||
| } | ||
| } | ||
| }; | ||
@@ -26,11 +50,9 @@ } | ||
| * Creates a minimal `Adapter` mock for unit tests. | ||
| * `parse` returns an empty `InputNode` by default; override via `options.parse`. | ||
| * `parse` returns an empty `InputNode` by default. Override via `options.parse`. | ||
| * `getImports` returns `[]` by default. | ||
| */ | ||
| function createMockedAdapter(options = {}) { | ||
| const inputNode = options.inputNode ?? null; | ||
| return { | ||
| name: options.name ?? "oas", | ||
| options: options.resolvedOptions ?? {}, | ||
| inputNode, | ||
| parse: options.parse ?? (async () => ({ | ||
@@ -55,3 +77,3 @@ kind: "Input", | ||
| resolver: params.resolver, | ||
| transformer: params.transformer, | ||
| macros: params.macros, | ||
| dependencies: params.dependencies, | ||
@@ -66,3 +88,2 @@ hooks: {} | ||
| root, | ||
| getMode: (output) => require_PluginDriver.PluginDriver.getMode((0, node_path.resolve)(root, output.path)), | ||
| adapter: opts.adapter, | ||
@@ -73,6 +94,5 @@ resolver: opts.resolver, | ||
| getResolver: (name) => opts.driver.getResolver(name), | ||
| inputNode: { | ||
| kind: "Input", | ||
| schemas: [], | ||
| operations: [] | ||
| meta: opts.meta ?? { | ||
| circularNames: [], | ||
| enumNames: [] | ||
| }, | ||
@@ -84,4 +104,3 @@ addFile: async (...files) => opts.driver.fileManager.add(...files), | ||
| error: (msg) => console.error(msg), | ||
| info: (msg) => console.info(msg), | ||
| openInStudio: async () => {} | ||
| info: (msg) => console.info(msg) | ||
| }; | ||
@@ -101,7 +120,11 @@ } | ||
| const context = createMockedPluginContext(opts); | ||
| const transformedNode = opts.plugin.transformer ? (0, _kubb_ast.transform)(node, opts.plugin.transformer) : node; | ||
| await require_PluginDriver.applyHookResult(await generator.schema(transformedNode, { | ||
| const transformedNode = opts.plugin.macros?.length ? (0, _kubb_ast.applyMacros)(node, opts.plugin.macros) : node; | ||
| const result = await generator.schema(transformedNode, { | ||
| ...context, | ||
| options: opts.options | ||
| }), opts.driver, generator.renderer ?? void 0); | ||
| }); | ||
| await opts.driver.dispatch({ | ||
| result, | ||
| renderer: generator.renderer | ||
| }); | ||
| } | ||
@@ -120,7 +143,11 @@ /** | ||
| const context = createMockedPluginContext(opts); | ||
| const transformedNode = opts.plugin.transformer ? (0, _kubb_ast.transform)(node, opts.plugin.transformer) : node; | ||
| await require_PluginDriver.applyHookResult(await generator.operation(transformedNode, { | ||
| const transformedNode = opts.plugin.macros?.length ? (0, _kubb_ast.applyMacros)(node, opts.plugin.macros) : node; | ||
| const result = await generator.operation(transformedNode, { | ||
| ...context, | ||
| options: opts.options | ||
| }), opts.driver, generator.renderer ?? void 0); | ||
| }); | ||
| await opts.driver.dispatch({ | ||
| result, | ||
| renderer: generator.renderer | ||
| }); | ||
| } | ||
@@ -139,8 +166,40 @@ /** | ||
| const context = createMockedPluginContext(opts); | ||
| const transformedNodes = opts.plugin.transformer ? nodes.map((n) => (0, _kubb_ast.transform)(n, opts.plugin.transformer)) : nodes; | ||
| await require_PluginDriver.applyHookResult(await generator.operations(transformedNodes, { | ||
| const transformedNodes = opts.plugin.macros?.length ? nodes.map((n) => (0, _kubb_ast.applyMacros)(n, opts.plugin.macros)) : nodes; | ||
| const result = await generator.operations(transformedNodes, { | ||
| ...context, | ||
| options: opts.options | ||
| }), opts.driver, generator.renderer ?? void 0); | ||
| }); | ||
| await opts.driver.dispatch({ | ||
| result, | ||
| renderer: generator.renderer | ||
| }); | ||
| } | ||
| /** | ||
| * Renders the driver's collected `FileNode`s to source and asserts each against a file snapshot. | ||
| * Pair it with the `renderGenerator*` helpers to snapshot a generator's output. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * await renderGeneratorSchema(typeGenerator, node, { config, adapter, driver, plugin, options, resolver }) | ||
| * await matchFiles(driver.fileManager.files, { parsers, format }) | ||
| * ``` | ||
| */ | ||
| async function matchFiles(files, options = {}) { | ||
| if (!files?.length) return; | ||
| const { parsers = /* @__PURE__ */ new Map(), format, pre } = options; | ||
| const fileProcessor = new require_memoryStorage.FileProcessor({ | ||
| storage: require_memoryStorage.memoryStorage(), | ||
| parsers | ||
| }); | ||
| const processed = /* @__PURE__ */ new Map(); | ||
| for (const file of files) { | ||
| if (!file?.path || processed.has(file.path)) continue; | ||
| const parsed = await fileProcessor.parse(file); | ||
| const code = file.baseName.endsWith(".json") || !format ? parsed : await format(parsed); | ||
| processed.set(file.path, code); | ||
| const snapshotPath = node_path.default.join("__snapshots__", ...pre ? [require_memoryStorage.camelCase(pre)] : [], file.baseName); | ||
| await (0, vitest.expect)(code).toMatchFileSnapshot(snapshotPath); | ||
| } | ||
| return processed; | ||
| } | ||
| //#endregion | ||
@@ -150,2 +209,3 @@ exports.createMockedAdapter = createMockedAdapter; | ||
| exports.createMockedPluginDriver = createMockedPluginDriver; | ||
| exports.matchFiles = matchFiles; | ||
| exports.renderGeneratorOperation = renderGeneratorOperation; | ||
@@ -152,0 +212,0 @@ exports.renderGeneratorOperations = renderGeneratorOperations; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"mocks.cjs","names":["FileManager","PluginDriver","applyHookResult"],"sources":["../src/mocks.ts"],"sourcesContent":["import { resolve } from 'node:path'\nimport type { FileNode, OperationNode, SchemaNode, Visitor } from '@kubb/ast'\nimport { transform } from '@kubb/ast'\nimport { FileManager } from './FileManager.ts'\nimport { PluginDriver } from './PluginDriver.ts'\nimport { applyHookResult } from './renderNode.ts'\nimport type { Adapter, AdapterFactoryOptions, Config, Generator, GeneratorContext, NormalizedPlugin, PluginFactoryOptions } from './types.ts'\n\n/**\n\n * Creates a minimal `PluginDriver` mock for unit tests.\n */\nexport function createMockedPluginDriver(options: { name?: string; plugin?: NormalizedPlugin; config?: Config } = {}): PluginDriver {\n return {\n config: options?.config ?? {\n root: '.',\n output: {\n path: './path',\n },\n },\n getPlugin(_pluginName: string): NormalizedPlugin | undefined {\n return options?.plugin\n },\n getResolver: (_pluginName: string) => options?.plugin?.resolver,\n fileManager: new FileManager(),\n } as unknown as PluginDriver\n}\n\n/**\n * Creates a minimal `Adapter` mock for unit tests.\n * `parse` returns an empty `InputNode` by default; override via `options.parse`.\n * `getImports` returns `[]` by default.\n */\nexport function createMockedAdapter<TOptions extends AdapterFactoryOptions = AdapterFactoryOptions>(\n options: {\n name?: TOptions['name']\n resolvedOptions?: TOptions['resolvedOptions']\n inputNode?: Adapter<TOptions>['inputNode']\n parse?: Adapter<TOptions>['parse']\n getImports?: Adapter<TOptions>['getImports']\n } = {},\n): Adapter<TOptions> {\n const inputNode = options.inputNode ?? null\n return {\n name: (options.name ?? 'oas') as TOptions['name'],\n options: (options.resolvedOptions ?? {}) as TOptions['resolvedOptions'],\n inputNode,\n parse: options.parse ?? (async () => ({ kind: 'Input' as const, schemas: [], operations: [] })),\n getImports: options.getImports ?? ((_node: SchemaNode, _resolve: (schemaName: string) => { name: string; path: string }) => []),\n } as Adapter<TOptions>\n}\n\n/**\n * Creates a minimal plugin mock for unit tests.\n *\n * @example\n * `const plugin = createMockedPlugin<PluginTs>({ name: '@kubb/plugin-ts', options })`\n */\nexport function createMockedPlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions>(params: {\n name: TOptions['name']\n options: TOptions['resolvedOptions']\n resolver?: TOptions['resolver']\n transformer?: Visitor\n dependencies?: Array<string>\n}): NormalizedPlugin<TOptions> {\n return {\n name: params.name,\n options: params.options,\n resolver: params.resolver,\n transformer: params.transformer,\n dependencies: params.dependencies,\n hooks: {},\n } as unknown as NormalizedPlugin<TOptions>\n}\n\ntype RenderGeneratorOptions<TOptions extends PluginFactoryOptions> = {\n config: Config\n adapter: Adapter\n driver: PluginDriver\n plugin: NormalizedPlugin<TOptions>\n options: TOptions['resolvedOptions']\n resolver: TOptions['resolver']\n}\n\nfunction createMockedPluginContext<TOptions extends PluginFactoryOptions>(opts: RenderGeneratorOptions<TOptions>): Omit<GeneratorContext<TOptions>, 'options'> {\n const root = resolve(opts.config.root, opts.config.output.path)\n\n return {\n config: opts.config,\n root,\n getMode: (output: { path: string }) => PluginDriver.getMode(resolve(root, output.path)),\n adapter: opts.adapter,\n resolver: opts.resolver,\n plugin: opts.plugin,\n driver: opts.driver,\n getResolver: (name: string) => opts.driver.getResolver(name),\n inputNode: { kind: 'Input', schemas: [], operations: [] },\n addFile: async (...files: Array<FileNode>) => opts.driver.fileManager.add(...files),\n upsertFile: async (...files: Array<FileNode>) => opts.driver.fileManager.upsert(...files),\n hooks: opts.driver.hooks ?? ({} as never),\n warn: (msg: string) => console.warn(msg),\n error: (msg: string) => console.error(msg),\n info: (msg: string) => console.info(msg),\n openInStudio: async () => {},\n } as unknown as Omit<GeneratorContext<TOptions>, 'options'>\n}\n\n/**\n * Renders a generator's `schema` method in a test context.\n *\n * @example\n * ```ts\n * await renderGeneratorSchema(typeGenerator, node, { config, adapter, driver, plugin, options, resolver })\n * await matchFiles(driver.fileManager.files)\n * ```\n */\nexport async function renderGeneratorSchema<TOptions extends PluginFactoryOptions>(\n generator: Generator<TOptions>,\n node: SchemaNode,\n opts: RenderGeneratorOptions<TOptions>,\n): Promise<void> {\n if (!generator.schema) return\n const context = createMockedPluginContext(opts)\n const transformedNode = opts.plugin.transformer ? transform(node, opts.plugin.transformer) : node\n const result = await generator.schema(transformedNode, {\n ...context,\n options: opts.options,\n })\n await applyHookResult(result, opts.driver, generator.renderer ?? undefined)\n}\n\n/**\n * Renders a generator's `operation` method in a test context.\n *\n * @example\n * ```ts\n * await renderGeneratorOperation(typeGenerator, node, { config, adapter, driver, plugin, options, resolver })\n * await matchFiles(driver.fileManager.files)\n * ```\n */\nexport async function renderGeneratorOperation<TOptions extends PluginFactoryOptions>(\n generator: Generator<TOptions>,\n node: OperationNode,\n opts: RenderGeneratorOptions<TOptions>,\n): Promise<void> {\n if (!generator.operation) return\n const context = createMockedPluginContext(opts)\n const transformedNode = opts.plugin.transformer ? transform(node, opts.plugin.transformer) : node\n const result = await generator.operation(transformedNode, {\n ...context,\n options: opts.options,\n })\n await applyHookResult(result, opts.driver, generator.renderer ?? undefined)\n}\n\n/**\n * Renders a generator's `operations` method in a test context.\n *\n * @example\n * ```ts\n * await renderGeneratorOperations(classClientGenerator, nodes, { config, adapter, driver, plugin, options, resolver })\n * await matchFiles(driver.fileManager.files)\n * ```\n */\nexport async function renderGeneratorOperations<TOptions extends PluginFactoryOptions>(\n generator: Generator<TOptions>,\n nodes: Array<OperationNode>,\n opts: RenderGeneratorOptions<TOptions>,\n): Promise<void> {\n if (!generator.operations) return\n const context = createMockedPluginContext(opts)\n const transformedNodes = opts.plugin.transformer ? nodes.map((n) => transform(n, opts.plugin.transformer!)) : nodes\n const result = await generator.operations(transformedNodes, {\n ...context,\n options: opts.options,\n })\n await applyHookResult(result, opts.driver, generator.renderer ?? undefined)\n}\n"],"mappings":"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"} | ||
| {"version":3,"file":"mocks.cjs","names":["FileManager","FileProcessor","memoryStorage","path","camelCase"],"sources":["../src/mocks.ts"],"sourcesContent":["import path, { resolve } from 'node:path'\nimport { camelCase } from '@internals/utils'\nimport type { FileNode, InputMeta, Macro, OperationNode, SchemaNode } from '@kubb/ast'\nimport { applyMacros } from '@kubb/ast'\nimport { expect } from 'vitest'\nimport type { Parser } from './defineParser.ts'\nimport { FileManager } from './FileManager.ts'\nimport { FileProcessor } from './FileProcessor.ts'\nimport type { KubbDriver } from './KubbDriver.ts'\nimport { memoryStorage } from './storages/memoryStorage.ts'\nimport type { Adapter, AdapterFactoryOptions, Config, Generator, GeneratorContext, NormalizedPlugin, PluginFactoryOptions, RendererFactory } from './types.ts'\n\n/**\n * Creates a minimal `KubbDriver` mock for unit tests.\n */\nexport function createMockedPluginDriver(options: { name?: string; plugin?: NormalizedPlugin; config?: Config } = {}): KubbDriver {\n const fileManager = new FileManager()\n\n return {\n config: options?.config ?? {\n root: '.',\n output: {\n path: './path',\n },\n },\n getPlugin(_pluginName: string): NormalizedPlugin | undefined {\n return options?.plugin\n },\n getResolver: (_pluginName: string) => options?.plugin?.resolver,\n fileManager,\n async dispatch({ result, renderer }: { result: unknown; renderer?: RendererFactory | null }): Promise<void> {\n if (!result) return\n\n if (Array.isArray(result)) {\n fileManager.upsert(...(result as Array<FileNode>))\n return\n }\n\n if (!renderer) return\n\n using instance = renderer()\n if (instance.stream) {\n for (const file of instance.stream(result)) fileManager.upsert(file)\n return\n }\n\n await instance.render(result)\n fileManager.upsert(...instance.files)\n },\n } as unknown as KubbDriver\n}\n\n/**\n * Creates a minimal `Adapter` mock for unit tests.\n * `parse` returns an empty `InputNode` by default. Override via `options.parse`.\n * `getImports` returns `[]` by default.\n */\nexport function createMockedAdapter<TOptions extends AdapterFactoryOptions = AdapterFactoryOptions>(\n options: {\n name?: TOptions['name']\n resolvedOptions?: TOptions['resolvedOptions']\n parse?: Adapter<TOptions>['parse']\n getImports?: Adapter<TOptions>['getImports']\n } = {},\n): Adapter<TOptions> {\n return {\n name: (options.name ?? 'oas') as TOptions['name'],\n options: (options.resolvedOptions ?? {}) as TOptions['resolvedOptions'],\n parse: options.parse ?? (async () => ({ kind: 'Input' as const, schemas: [], operations: [] })),\n getImports: options.getImports ?? ((_node: SchemaNode, _resolve: (schemaName: string) => { name: string; path: string }) => []),\n } as Adapter<TOptions>\n}\n\n/**\n * Creates a minimal plugin mock for unit tests.\n *\n * @example\n * `const plugin = createMockedPlugin<PluginTs>({ name: '@kubb/plugin-ts', options })`\n */\nexport function createMockedPlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions>(params: {\n name: TOptions['name']\n options: TOptions['resolvedOptions']\n resolver?: TOptions['resolver']\n macros?: Array<Macro>\n dependencies?: Array<string>\n}): NormalizedPlugin<TOptions> {\n return {\n name: params.name,\n options: params.options,\n resolver: params.resolver,\n macros: params.macros,\n dependencies: params.dependencies,\n hooks: {},\n } as unknown as NormalizedPlugin<TOptions>\n}\n\ntype RenderGeneratorOptions<TOptions extends PluginFactoryOptions> = {\n config: Config\n adapter: Adapter\n meta?: InputMeta\n driver: KubbDriver\n plugin: NormalizedPlugin<TOptions>\n options: TOptions['resolvedOptions']\n resolver: TOptions['resolver']\n}\n\nfunction createMockedPluginContext<TOptions extends PluginFactoryOptions>(opts: RenderGeneratorOptions<TOptions>): Omit<GeneratorContext<TOptions>, 'options'> {\n const root = resolve(opts.config.root, opts.config.output.path)\n\n return {\n config: opts.config,\n root,\n adapter: opts.adapter,\n resolver: opts.resolver,\n plugin: opts.plugin,\n driver: opts.driver,\n getResolver: (name: string) => opts.driver.getResolver(name),\n meta: opts.meta ?? { circularNames: [], enumNames: [] },\n addFile: async (...files: Array<FileNode>) => opts.driver.fileManager.add(...files),\n upsertFile: async (...files: Array<FileNode>) => opts.driver.fileManager.upsert(...files),\n hooks: opts.driver.hooks ?? ({} as never),\n warn: (msg: string) => console.warn(msg),\n error: (msg: string) => console.error(msg),\n info: (msg: string) => console.info(msg),\n } as unknown as Omit<GeneratorContext<TOptions>, 'options'>\n}\n\n/**\n * Renders a generator's `schema` method in a test context.\n *\n * @example\n * ```ts\n * await renderGeneratorSchema(typeGenerator, node, { config, adapter, driver, plugin, options, resolver })\n * await matchFiles(driver.fileManager.files)\n * ```\n */\nexport async function renderGeneratorSchema<TOptions extends PluginFactoryOptions>(\n generator: Generator<TOptions>,\n node: SchemaNode,\n opts: RenderGeneratorOptions<TOptions>,\n): Promise<void> {\n if (!generator.schema) return\n const context = createMockedPluginContext(opts)\n const transformedNode = opts.plugin.macros?.length ? applyMacros(node, opts.plugin.macros) : node\n const result = await generator.schema(transformedNode, {\n ...context,\n options: opts.options,\n })\n await opts.driver.dispatch({ result, renderer: generator.renderer })\n}\n\n/**\n * Renders a generator's `operation` method in a test context.\n *\n * @example\n * ```ts\n * await renderGeneratorOperation(typeGenerator, node, { config, adapter, driver, plugin, options, resolver })\n * await matchFiles(driver.fileManager.files)\n * ```\n */\nexport async function renderGeneratorOperation<TOptions extends PluginFactoryOptions>(\n generator: Generator<TOptions>,\n node: OperationNode,\n opts: RenderGeneratorOptions<TOptions>,\n): Promise<void> {\n if (!generator.operation) return\n const context = createMockedPluginContext(opts)\n const transformedNode = opts.plugin.macros?.length ? applyMacros(node, opts.plugin.macros) : node\n const result = await generator.operation(transformedNode, {\n ...context,\n options: opts.options,\n })\n await opts.driver.dispatch({ result, renderer: generator.renderer })\n}\n\n/**\n * Renders a generator's `operations` method in a test context.\n *\n * @example\n * ```ts\n * await renderGeneratorOperations(classClientGenerator, nodes, { config, adapter, driver, plugin, options, resolver })\n * await matchFiles(driver.fileManager.files)\n * ```\n */\nexport async function renderGeneratorOperations<TOptions extends PluginFactoryOptions>(\n generator: Generator<TOptions>,\n nodes: Array<OperationNode>,\n opts: RenderGeneratorOptions<TOptions>,\n): Promise<void> {\n if (!generator.operations) return\n const context = createMockedPluginContext(opts)\n const transformedNodes = opts.plugin.macros?.length ? nodes.map((n) => applyMacros(n, opts.plugin.macros!)) : nodes\n const result = await generator.operations(transformedNodes, {\n ...context,\n options: opts.options,\n })\n await opts.driver.dispatch({ result, renderer: generator.renderer })\n}\n\ntype MatchFilesOptions = {\n /**\n * Parsers indexed by file extension, used to render each `FileNode` to source.\n * Without a matching parser the file's raw content is used.\n */\n parsers?: Map<FileNode['extname'], Parser>\n /**\n * Formatter applied to non-JSON output before snapshotting, e.g. prettier. When\n * omitted the parsed source is snapshotted as-is.\n */\n format?: (source?: string) => string | Promise<string>\n /**\n * Subfolder under `__snapshots__`, camelCased. Useful to keep variant snapshots apart.\n */\n pre?: string\n}\n\n/**\n * Renders the driver's collected `FileNode`s to source and asserts each against a file snapshot.\n * Pair it with the `renderGenerator*` helpers to snapshot a generator's output.\n *\n * @example\n * ```ts\n * await renderGeneratorSchema(typeGenerator, node, { config, adapter, driver, plugin, options, resolver })\n * await matchFiles(driver.fileManager.files, { parsers, format })\n * ```\n */\nexport async function matchFiles(files: Array<FileNode> | undefined, options: MatchFilesOptions = {}): Promise<Map<string, string> | undefined> {\n if (!files?.length) return\n\n const { parsers = new Map(), format, pre } = options\n const fileProcessor = new FileProcessor({ storage: memoryStorage(), parsers })\n const processed = new Map<string, string>()\n\n for (const file of files) {\n if (!file?.path || processed.has(file.path)) {\n continue\n }\n\n const parsed = await fileProcessor.parse(file)\n const code = file.baseName.endsWith('.json') || !format ? parsed : await format(parsed)\n\n processed.set(file.path, code)\n\n const snapshotPath = path.join('__snapshots__', ...(pre ? [camelCase(pre)] : []), file.baseName)\n await expect(code).toMatchFileSnapshot(snapshotPath)\n }\n\n return processed\n}\n"],"mappings":"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"} |
+37
-11
@@ -1,9 +0,8 @@ | ||
| import { t as __name } from "./chunk--u3MIqq1.js"; | ||
| import { R as NormalizedPlugin, V as PluginFactoryOptions, a as Config, n as AdapterFactoryOptions, ot as PluginDriver, t as Adapter, ut as Generator } from "./types-CuNocrbJ.js"; | ||
| import { OperationNode, SchemaNode, Visitor } from "@kubb/ast"; | ||
| import { t as __name } from "./rolldown-runtime-C0LytTxp.js"; | ||
| import { At as Adapter, G as Generator, J as KubbDriver, U as Parser, h as Config, jt as AdapterFactoryOptions, st as PluginFactoryOptions, tt as NormalizedPlugin } from "./diagnostics-CQYd4UQZ.js"; | ||
| import { FileNode, InputMeta, Macro, OperationNode, SchemaNode } from "@kubb/ast"; | ||
| //#region src/mocks.d.ts | ||
| /** | ||
| * Creates a minimal `PluginDriver` mock for unit tests. | ||
| * Creates a minimal `KubbDriver` mock for unit tests. | ||
| */ | ||
@@ -14,6 +13,6 @@ declare function createMockedPluginDriver(options?: { | ||
| config?: Config; | ||
| }): PluginDriver; | ||
| }): KubbDriver; | ||
| /** | ||
| * Creates a minimal `Adapter` mock for unit tests. | ||
| * `parse` returns an empty `InputNode` by default; override via `options.parse`. | ||
| * `parse` returns an empty `InputNode` by default. Override via `options.parse`. | ||
| * `getImports` returns `[]` by default. | ||
@@ -24,3 +23,2 @@ */ | ||
| resolvedOptions?: TOptions['resolvedOptions']; | ||
| inputNode?: Adapter<TOptions>['inputNode']; | ||
| parse?: Adapter<TOptions>['parse']; | ||
@@ -39,3 +37,3 @@ getImports?: Adapter<TOptions>['getImports']; | ||
| resolver?: TOptions['resolver']; | ||
| transformer?: Visitor; | ||
| macros?: Array<Macro>; | ||
| dependencies?: Array<string>; | ||
@@ -46,3 +44,4 @@ }): NormalizedPlugin<TOptions>; | ||
| adapter: Adapter; | ||
| driver: PluginDriver; | ||
| meta?: InputMeta; | ||
| driver: KubbDriver; | ||
| plugin: NormalizedPlugin<TOptions>; | ||
@@ -82,4 +81,31 @@ options: TOptions['resolvedOptions']; | ||
| declare function renderGeneratorOperations<TOptions extends PluginFactoryOptions>(generator: Generator<TOptions>, nodes: Array<OperationNode>, opts: RenderGeneratorOptions<TOptions>): Promise<void>; | ||
| type MatchFilesOptions = { | ||
| /** | ||
| * Parsers indexed by file extension, used to render each `FileNode` to source. | ||
| * Without a matching parser the file's raw content is used. | ||
| */ | ||
| parsers?: Map<FileNode['extname'], Parser>; | ||
| /** | ||
| * Formatter applied to non-JSON output before snapshotting, e.g. prettier. When | ||
| * omitted the parsed source is snapshotted as-is. | ||
| */ | ||
| format?: (source?: string) => string | Promise<string>; | ||
| /** | ||
| * Subfolder under `__snapshots__`, camelCased. Useful to keep variant snapshots apart. | ||
| */ | ||
| pre?: string; | ||
| }; | ||
| /** | ||
| * Renders the driver's collected `FileNode`s to source and asserts each against a file snapshot. | ||
| * Pair it with the `renderGenerator*` helpers to snapshot a generator's output. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * await renderGeneratorSchema(typeGenerator, node, { config, adapter, driver, plugin, options, resolver }) | ||
| * await matchFiles(driver.fileManager.files, { parsers, format }) | ||
| * ``` | ||
| */ | ||
| declare function matchFiles(files: Array<FileNode> | undefined, options?: MatchFilesOptions): Promise<Map<string, string> | undefined>; | ||
| //#endregion | ||
| export { createMockedAdapter, createMockedPlugin, createMockedPluginDriver, renderGeneratorOperation, renderGeneratorOperations, renderGeneratorSchema }; | ||
| export { createMockedAdapter, createMockedPlugin, createMockedPluginDriver, matchFiles, renderGeneratorOperation, renderGeneratorOperations, renderGeneratorSchema }; | ||
| //# sourceMappingURL=mocks.d.ts.map |
+86
-28
@@ -1,11 +0,12 @@ | ||
| import "./chunk--u3MIqq1.js"; | ||
| import { n as applyHookResult, r as FileManager, t as PluginDriver } from "./PluginDriver-DV3p2Hky.js"; | ||
| import { resolve } from "node:path"; | ||
| import { transform } from "@kubb/ast"; | ||
| import "./rolldown-runtime-C0LytTxp.js"; | ||
| import { i as FileManager, n as _usingCtx, p as camelCase, r as FileProcessor, t as memoryStorage } from "./memoryStorage-Dow5-isU.js"; | ||
| import path, { resolve } from "node:path"; | ||
| import { applyMacros } from "@kubb/ast"; | ||
| import { expect } from "vitest"; | ||
| //#region src/mocks.ts | ||
| /** | ||
| * Creates a minimal `PluginDriver` mock for unit tests. | ||
| * Creates a minimal `KubbDriver` mock for unit tests. | ||
| */ | ||
| function createMockedPluginDriver(options = {}) { | ||
| const fileManager = new FileManager(); | ||
| return { | ||
@@ -20,3 +21,25 @@ config: options?.config ?? { | ||
| getResolver: (_pluginName) => options?.plugin?.resolver, | ||
| fileManager: new FileManager() | ||
| fileManager, | ||
| async dispatch({ result, renderer }) { | ||
| try { | ||
| var _usingCtx$1 = _usingCtx(); | ||
| if (!result) return; | ||
| if (Array.isArray(result)) { | ||
| fileManager.upsert(...result); | ||
| return; | ||
| } | ||
| if (!renderer) return; | ||
| const instance = _usingCtx$1.u(renderer()); | ||
| if (instance.stream) { | ||
| for (const file of instance.stream(result)) fileManager.upsert(file); | ||
| return; | ||
| } | ||
| await instance.render(result); | ||
| fileManager.upsert(...instance.files); | ||
| } catch (_) { | ||
| _usingCtx$1.e = _; | ||
| } finally { | ||
| _usingCtx$1.d(); | ||
| } | ||
| } | ||
| }; | ||
@@ -26,11 +49,9 @@ } | ||
| * Creates a minimal `Adapter` mock for unit tests. | ||
| * `parse` returns an empty `InputNode` by default; override via `options.parse`. | ||
| * `parse` returns an empty `InputNode` by default. Override via `options.parse`. | ||
| * `getImports` returns `[]` by default. | ||
| */ | ||
| function createMockedAdapter(options = {}) { | ||
| const inputNode = options.inputNode ?? null; | ||
| return { | ||
| name: options.name ?? "oas", | ||
| options: options.resolvedOptions ?? {}, | ||
| inputNode, | ||
| parse: options.parse ?? (async () => ({ | ||
@@ -55,3 +76,3 @@ kind: "Input", | ||
| resolver: params.resolver, | ||
| transformer: params.transformer, | ||
| macros: params.macros, | ||
| dependencies: params.dependencies, | ||
@@ -66,3 +87,2 @@ hooks: {} | ||
| root, | ||
| getMode: (output) => PluginDriver.getMode(resolve(root, output.path)), | ||
| adapter: opts.adapter, | ||
@@ -73,6 +93,5 @@ resolver: opts.resolver, | ||
| getResolver: (name) => opts.driver.getResolver(name), | ||
| inputNode: { | ||
| kind: "Input", | ||
| schemas: [], | ||
| operations: [] | ||
| meta: opts.meta ?? { | ||
| circularNames: [], | ||
| enumNames: [] | ||
| }, | ||
@@ -84,4 +103,3 @@ addFile: async (...files) => opts.driver.fileManager.add(...files), | ||
| error: (msg) => console.error(msg), | ||
| info: (msg) => console.info(msg), | ||
| openInStudio: async () => {} | ||
| info: (msg) => console.info(msg) | ||
| }; | ||
@@ -101,7 +119,11 @@ } | ||
| const context = createMockedPluginContext(opts); | ||
| const transformedNode = opts.plugin.transformer ? transform(node, opts.plugin.transformer) : node; | ||
| await applyHookResult(await generator.schema(transformedNode, { | ||
| const transformedNode = opts.plugin.macros?.length ? applyMacros(node, opts.plugin.macros) : node; | ||
| const result = await generator.schema(transformedNode, { | ||
| ...context, | ||
| options: opts.options | ||
| }), opts.driver, generator.renderer ?? void 0); | ||
| }); | ||
| await opts.driver.dispatch({ | ||
| result, | ||
| renderer: generator.renderer | ||
| }); | ||
| } | ||
@@ -120,7 +142,11 @@ /** | ||
| const context = createMockedPluginContext(opts); | ||
| const transformedNode = opts.plugin.transformer ? transform(node, opts.plugin.transformer) : node; | ||
| await applyHookResult(await generator.operation(transformedNode, { | ||
| const transformedNode = opts.plugin.macros?.length ? applyMacros(node, opts.plugin.macros) : node; | ||
| const result = await generator.operation(transformedNode, { | ||
| ...context, | ||
| options: opts.options | ||
| }), opts.driver, generator.renderer ?? void 0); | ||
| }); | ||
| await opts.driver.dispatch({ | ||
| result, | ||
| renderer: generator.renderer | ||
| }); | ||
| } | ||
@@ -139,11 +165,43 @@ /** | ||
| const context = createMockedPluginContext(opts); | ||
| const transformedNodes = opts.plugin.transformer ? nodes.map((n) => transform(n, opts.plugin.transformer)) : nodes; | ||
| await applyHookResult(await generator.operations(transformedNodes, { | ||
| const transformedNodes = opts.plugin.macros?.length ? nodes.map((n) => applyMacros(n, opts.plugin.macros)) : nodes; | ||
| const result = await generator.operations(transformedNodes, { | ||
| ...context, | ||
| options: opts.options | ||
| }), opts.driver, generator.renderer ?? void 0); | ||
| }); | ||
| await opts.driver.dispatch({ | ||
| result, | ||
| renderer: generator.renderer | ||
| }); | ||
| } | ||
| /** | ||
| * Renders the driver's collected `FileNode`s to source and asserts each against a file snapshot. | ||
| * Pair it with the `renderGenerator*` helpers to snapshot a generator's output. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * await renderGeneratorSchema(typeGenerator, node, { config, adapter, driver, plugin, options, resolver }) | ||
| * await matchFiles(driver.fileManager.files, { parsers, format }) | ||
| * ``` | ||
| */ | ||
| async function matchFiles(files, options = {}) { | ||
| if (!files?.length) return; | ||
| const { parsers = /* @__PURE__ */ new Map(), format, pre } = options; | ||
| const fileProcessor = new FileProcessor({ | ||
| storage: memoryStorage(), | ||
| parsers | ||
| }); | ||
| const processed = /* @__PURE__ */ new Map(); | ||
| for (const file of files) { | ||
| if (!file?.path || processed.has(file.path)) continue; | ||
| const parsed = await fileProcessor.parse(file); | ||
| const code = file.baseName.endsWith(".json") || !format ? parsed : await format(parsed); | ||
| processed.set(file.path, code); | ||
| const snapshotPath = path.join("__snapshots__", ...pre ? [camelCase(pre)] : [], file.baseName); | ||
| await expect(code).toMatchFileSnapshot(snapshotPath); | ||
| } | ||
| return processed; | ||
| } | ||
| //#endregion | ||
| export { createMockedAdapter, createMockedPlugin, createMockedPluginDriver, renderGeneratorOperation, renderGeneratorOperations, renderGeneratorSchema }; | ||
| export { createMockedAdapter, createMockedPlugin, createMockedPluginDriver, matchFiles, renderGeneratorOperation, renderGeneratorOperations, renderGeneratorSchema }; | ||
| //# sourceMappingURL=mocks.js.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"mocks.js","names":[],"sources":["../src/mocks.ts"],"sourcesContent":["import { resolve } from 'node:path'\nimport type { FileNode, OperationNode, SchemaNode, Visitor } from '@kubb/ast'\nimport { transform } from '@kubb/ast'\nimport { FileManager } from './FileManager.ts'\nimport { PluginDriver } from './PluginDriver.ts'\nimport { applyHookResult } from './renderNode.ts'\nimport type { Adapter, AdapterFactoryOptions, Config, Generator, GeneratorContext, NormalizedPlugin, PluginFactoryOptions } from './types.ts'\n\n/**\n\n * Creates a minimal `PluginDriver` mock for unit tests.\n */\nexport function createMockedPluginDriver(options: { name?: string; plugin?: NormalizedPlugin; config?: Config } = {}): PluginDriver {\n return {\n config: options?.config ?? {\n root: '.',\n output: {\n path: './path',\n },\n },\n getPlugin(_pluginName: string): NormalizedPlugin | undefined {\n return options?.plugin\n },\n getResolver: (_pluginName: string) => options?.plugin?.resolver,\n fileManager: new FileManager(),\n } as unknown as PluginDriver\n}\n\n/**\n * Creates a minimal `Adapter` mock for unit tests.\n * `parse` returns an empty `InputNode` by default; override via `options.parse`.\n * `getImports` returns `[]` by default.\n */\nexport function createMockedAdapter<TOptions extends AdapterFactoryOptions = AdapterFactoryOptions>(\n options: {\n name?: TOptions['name']\n resolvedOptions?: TOptions['resolvedOptions']\n inputNode?: Adapter<TOptions>['inputNode']\n parse?: Adapter<TOptions>['parse']\n getImports?: Adapter<TOptions>['getImports']\n } = {},\n): Adapter<TOptions> {\n const inputNode = options.inputNode ?? null\n return {\n name: (options.name ?? 'oas') as TOptions['name'],\n options: (options.resolvedOptions ?? {}) as TOptions['resolvedOptions'],\n inputNode,\n parse: options.parse ?? (async () => ({ kind: 'Input' as const, schemas: [], operations: [] })),\n getImports: options.getImports ?? ((_node: SchemaNode, _resolve: (schemaName: string) => { name: string; path: string }) => []),\n } as Adapter<TOptions>\n}\n\n/**\n * Creates a minimal plugin mock for unit tests.\n *\n * @example\n * `const plugin = createMockedPlugin<PluginTs>({ name: '@kubb/plugin-ts', options })`\n */\nexport function createMockedPlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions>(params: {\n name: TOptions['name']\n options: TOptions['resolvedOptions']\n resolver?: TOptions['resolver']\n transformer?: Visitor\n dependencies?: Array<string>\n}): NormalizedPlugin<TOptions> {\n return {\n name: params.name,\n options: params.options,\n resolver: params.resolver,\n transformer: params.transformer,\n dependencies: params.dependencies,\n hooks: {},\n } as unknown as NormalizedPlugin<TOptions>\n}\n\ntype RenderGeneratorOptions<TOptions extends PluginFactoryOptions> = {\n config: Config\n adapter: Adapter\n driver: PluginDriver\n plugin: NormalizedPlugin<TOptions>\n options: TOptions['resolvedOptions']\n resolver: TOptions['resolver']\n}\n\nfunction createMockedPluginContext<TOptions extends PluginFactoryOptions>(opts: RenderGeneratorOptions<TOptions>): Omit<GeneratorContext<TOptions>, 'options'> {\n const root = resolve(opts.config.root, opts.config.output.path)\n\n return {\n config: opts.config,\n root,\n getMode: (output: { path: string }) => PluginDriver.getMode(resolve(root, output.path)),\n adapter: opts.adapter,\n resolver: opts.resolver,\n plugin: opts.plugin,\n driver: opts.driver,\n getResolver: (name: string) => opts.driver.getResolver(name),\n inputNode: { kind: 'Input', schemas: [], operations: [] },\n addFile: async (...files: Array<FileNode>) => opts.driver.fileManager.add(...files),\n upsertFile: async (...files: Array<FileNode>) => opts.driver.fileManager.upsert(...files),\n hooks: opts.driver.hooks ?? ({} as never),\n warn: (msg: string) => console.warn(msg),\n error: (msg: string) => console.error(msg),\n info: (msg: string) => console.info(msg),\n openInStudio: async () => {},\n } as unknown as Omit<GeneratorContext<TOptions>, 'options'>\n}\n\n/**\n * Renders a generator's `schema` method in a test context.\n *\n * @example\n * ```ts\n * await renderGeneratorSchema(typeGenerator, node, { config, adapter, driver, plugin, options, resolver })\n * await matchFiles(driver.fileManager.files)\n * ```\n */\nexport async function renderGeneratorSchema<TOptions extends PluginFactoryOptions>(\n generator: Generator<TOptions>,\n node: SchemaNode,\n opts: RenderGeneratorOptions<TOptions>,\n): Promise<void> {\n if (!generator.schema) return\n const context = createMockedPluginContext(opts)\n const transformedNode = opts.plugin.transformer ? transform(node, opts.plugin.transformer) : node\n const result = await generator.schema(transformedNode, {\n ...context,\n options: opts.options,\n })\n await applyHookResult(result, opts.driver, generator.renderer ?? undefined)\n}\n\n/**\n * Renders a generator's `operation` method in a test context.\n *\n * @example\n * ```ts\n * await renderGeneratorOperation(typeGenerator, node, { config, adapter, driver, plugin, options, resolver })\n * await matchFiles(driver.fileManager.files)\n * ```\n */\nexport async function renderGeneratorOperation<TOptions extends PluginFactoryOptions>(\n generator: Generator<TOptions>,\n node: OperationNode,\n opts: RenderGeneratorOptions<TOptions>,\n): Promise<void> {\n if (!generator.operation) return\n const context = createMockedPluginContext(opts)\n const transformedNode = opts.plugin.transformer ? transform(node, opts.plugin.transformer) : node\n const result = await generator.operation(transformedNode, {\n ...context,\n options: opts.options,\n })\n await applyHookResult(result, opts.driver, generator.renderer ?? undefined)\n}\n\n/**\n * Renders a generator's `operations` method in a test context.\n *\n * @example\n * ```ts\n * await renderGeneratorOperations(classClientGenerator, nodes, { config, adapter, driver, plugin, options, resolver })\n * await matchFiles(driver.fileManager.files)\n * ```\n */\nexport async function renderGeneratorOperations<TOptions extends PluginFactoryOptions>(\n generator: Generator<TOptions>,\n nodes: Array<OperationNode>,\n opts: RenderGeneratorOptions<TOptions>,\n): Promise<void> {\n if (!generator.operations) return\n const context = createMockedPluginContext(opts)\n const transformedNodes = opts.plugin.transformer ? nodes.map((n) => transform(n, opts.plugin.transformer!)) : nodes\n const result = await generator.operations(transformedNodes, {\n ...context,\n options: opts.options,\n })\n await applyHookResult(result, opts.driver, generator.renderer ?? undefined)\n}\n"],"mappings":"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"} | ||
| {"version":3,"file":"mocks.js","names":[],"sources":["../src/mocks.ts"],"sourcesContent":["import path, { resolve } from 'node:path'\nimport { camelCase } from '@internals/utils'\nimport type { FileNode, InputMeta, Macro, OperationNode, SchemaNode } from '@kubb/ast'\nimport { applyMacros } from '@kubb/ast'\nimport { expect } from 'vitest'\nimport type { Parser } from './defineParser.ts'\nimport { FileManager } from './FileManager.ts'\nimport { FileProcessor } from './FileProcessor.ts'\nimport type { KubbDriver } from './KubbDriver.ts'\nimport { memoryStorage } from './storages/memoryStorage.ts'\nimport type { Adapter, AdapterFactoryOptions, Config, Generator, GeneratorContext, NormalizedPlugin, PluginFactoryOptions, RendererFactory } from './types.ts'\n\n/**\n * Creates a minimal `KubbDriver` mock for unit tests.\n */\nexport function createMockedPluginDriver(options: { name?: string; plugin?: NormalizedPlugin; config?: Config } = {}): KubbDriver {\n const fileManager = new FileManager()\n\n return {\n config: options?.config ?? {\n root: '.',\n output: {\n path: './path',\n },\n },\n getPlugin(_pluginName: string): NormalizedPlugin | undefined {\n return options?.plugin\n },\n getResolver: (_pluginName: string) => options?.plugin?.resolver,\n fileManager,\n async dispatch({ result, renderer }: { result: unknown; renderer?: RendererFactory | null }): Promise<void> {\n if (!result) return\n\n if (Array.isArray(result)) {\n fileManager.upsert(...(result as Array<FileNode>))\n return\n }\n\n if (!renderer) return\n\n using instance = renderer()\n if (instance.stream) {\n for (const file of instance.stream(result)) fileManager.upsert(file)\n return\n }\n\n await instance.render(result)\n fileManager.upsert(...instance.files)\n },\n } as unknown as KubbDriver\n}\n\n/**\n * Creates a minimal `Adapter` mock for unit tests.\n * `parse` returns an empty `InputNode` by default. Override via `options.parse`.\n * `getImports` returns `[]` by default.\n */\nexport function createMockedAdapter<TOptions extends AdapterFactoryOptions = AdapterFactoryOptions>(\n options: {\n name?: TOptions['name']\n resolvedOptions?: TOptions['resolvedOptions']\n parse?: Adapter<TOptions>['parse']\n getImports?: Adapter<TOptions>['getImports']\n } = {},\n): Adapter<TOptions> {\n return {\n name: (options.name ?? 'oas') as TOptions['name'],\n options: (options.resolvedOptions ?? {}) as TOptions['resolvedOptions'],\n parse: options.parse ?? (async () => ({ kind: 'Input' as const, schemas: [], operations: [] })),\n getImports: options.getImports ?? ((_node: SchemaNode, _resolve: (schemaName: string) => { name: string; path: string }) => []),\n } as Adapter<TOptions>\n}\n\n/**\n * Creates a minimal plugin mock for unit tests.\n *\n * @example\n * `const plugin = createMockedPlugin<PluginTs>({ name: '@kubb/plugin-ts', options })`\n */\nexport function createMockedPlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions>(params: {\n name: TOptions['name']\n options: TOptions['resolvedOptions']\n resolver?: TOptions['resolver']\n macros?: Array<Macro>\n dependencies?: Array<string>\n}): NormalizedPlugin<TOptions> {\n return {\n name: params.name,\n options: params.options,\n resolver: params.resolver,\n macros: params.macros,\n dependencies: params.dependencies,\n hooks: {},\n } as unknown as NormalizedPlugin<TOptions>\n}\n\ntype RenderGeneratorOptions<TOptions extends PluginFactoryOptions> = {\n config: Config\n adapter: Adapter\n meta?: InputMeta\n driver: KubbDriver\n plugin: NormalizedPlugin<TOptions>\n options: TOptions['resolvedOptions']\n resolver: TOptions['resolver']\n}\n\nfunction createMockedPluginContext<TOptions extends PluginFactoryOptions>(opts: RenderGeneratorOptions<TOptions>): Omit<GeneratorContext<TOptions>, 'options'> {\n const root = resolve(opts.config.root, opts.config.output.path)\n\n return {\n config: opts.config,\n root,\n adapter: opts.adapter,\n resolver: opts.resolver,\n plugin: opts.plugin,\n driver: opts.driver,\n getResolver: (name: string) => opts.driver.getResolver(name),\n meta: opts.meta ?? { circularNames: [], enumNames: [] },\n addFile: async (...files: Array<FileNode>) => opts.driver.fileManager.add(...files),\n upsertFile: async (...files: Array<FileNode>) => opts.driver.fileManager.upsert(...files),\n hooks: opts.driver.hooks ?? ({} as never),\n warn: (msg: string) => console.warn(msg),\n error: (msg: string) => console.error(msg),\n info: (msg: string) => console.info(msg),\n } as unknown as Omit<GeneratorContext<TOptions>, 'options'>\n}\n\n/**\n * Renders a generator's `schema` method in a test context.\n *\n * @example\n * ```ts\n * await renderGeneratorSchema(typeGenerator, node, { config, adapter, driver, plugin, options, resolver })\n * await matchFiles(driver.fileManager.files)\n * ```\n */\nexport async function renderGeneratorSchema<TOptions extends PluginFactoryOptions>(\n generator: Generator<TOptions>,\n node: SchemaNode,\n opts: RenderGeneratorOptions<TOptions>,\n): Promise<void> {\n if (!generator.schema) return\n const context = createMockedPluginContext(opts)\n const transformedNode = opts.plugin.macros?.length ? applyMacros(node, opts.plugin.macros) : node\n const result = await generator.schema(transformedNode, {\n ...context,\n options: opts.options,\n })\n await opts.driver.dispatch({ result, renderer: generator.renderer })\n}\n\n/**\n * Renders a generator's `operation` method in a test context.\n *\n * @example\n * ```ts\n * await renderGeneratorOperation(typeGenerator, node, { config, adapter, driver, plugin, options, resolver })\n * await matchFiles(driver.fileManager.files)\n * ```\n */\nexport async function renderGeneratorOperation<TOptions extends PluginFactoryOptions>(\n generator: Generator<TOptions>,\n node: OperationNode,\n opts: RenderGeneratorOptions<TOptions>,\n): Promise<void> {\n if (!generator.operation) return\n const context = createMockedPluginContext(opts)\n const transformedNode = opts.plugin.macros?.length ? applyMacros(node, opts.plugin.macros) : node\n const result = await generator.operation(transformedNode, {\n ...context,\n options: opts.options,\n })\n await opts.driver.dispatch({ result, renderer: generator.renderer })\n}\n\n/**\n * Renders a generator's `operations` method in a test context.\n *\n * @example\n * ```ts\n * await renderGeneratorOperations(classClientGenerator, nodes, { config, adapter, driver, plugin, options, resolver })\n * await matchFiles(driver.fileManager.files)\n * ```\n */\nexport async function renderGeneratorOperations<TOptions extends PluginFactoryOptions>(\n generator: Generator<TOptions>,\n nodes: Array<OperationNode>,\n opts: RenderGeneratorOptions<TOptions>,\n): Promise<void> {\n if (!generator.operations) return\n const context = createMockedPluginContext(opts)\n const transformedNodes = opts.plugin.macros?.length ? nodes.map((n) => applyMacros(n, opts.plugin.macros!)) : nodes\n const result = await generator.operations(transformedNodes, {\n ...context,\n options: opts.options,\n })\n await opts.driver.dispatch({ result, renderer: generator.renderer })\n}\n\ntype MatchFilesOptions = {\n /**\n * Parsers indexed by file extension, used to render each `FileNode` to source.\n * Without a matching parser the file's raw content is used.\n */\n parsers?: Map<FileNode['extname'], Parser>\n /**\n * Formatter applied to non-JSON output before snapshotting, e.g. prettier. When\n * omitted the parsed source is snapshotted as-is.\n */\n format?: (source?: string) => string | Promise<string>\n /**\n * Subfolder under `__snapshots__`, camelCased. Useful to keep variant snapshots apart.\n */\n pre?: string\n}\n\n/**\n * Renders the driver's collected `FileNode`s to source and asserts each against a file snapshot.\n * Pair it with the `renderGenerator*` helpers to snapshot a generator's output.\n *\n * @example\n * ```ts\n * await renderGeneratorSchema(typeGenerator, node, { config, adapter, driver, plugin, options, resolver })\n * await matchFiles(driver.fileManager.files, { parsers, format })\n * ```\n */\nexport async function matchFiles(files: Array<FileNode> | undefined, options: MatchFilesOptions = {}): Promise<Map<string, string> | undefined> {\n if (!files?.length) return\n\n const { parsers = new Map(), format, pre } = options\n const fileProcessor = new FileProcessor({ storage: memoryStorage(), parsers })\n const processed = new Map<string, string>()\n\n for (const file of files) {\n if (!file?.path || processed.has(file.path)) {\n continue\n }\n\n const parsed = await fileProcessor.parse(file)\n const code = file.baseName.endsWith('.json') || !format ? parsed : await format(parsed)\n\n processed.set(file.path, code)\n\n const snapshotPath = path.join('__snapshots__', ...(pre ? [camelCase(pre)] : []), file.baseName)\n await expect(code).toMatchFileSnapshot(snapshotPath)\n }\n\n return processed\n}\n"],"mappings":"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"} |
+17
-10
@@ -0,14 +1,21 @@ | ||
| MIT License | ||
| Copyright (c) 2026 Stijn Van Hulle | ||
| This repository contains software under two licenses: | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
| 1. Most of the code in this repository is licensed under the | ||
| MIT License — see licenses/LICENSE-MIT for the full license text. | ||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
| 2. The following components are licensed under the | ||
| GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later) | ||
| — see licenses/LICENSE-AGPL-3.0 for the full license text: | ||
| - packages/agent (published as @kubb/agent) | ||
| Each package's own LICENSE file or package.json specifies its applicable license. | ||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. |
+9
-30
| { | ||
| "name": "@kubb/core", | ||
| "version": "5.0.0-beta.75", | ||
| "description": "Core functionality for Kubb's plugin-based code generation system, providing the foundation for transforming OpenAPI specifications.", | ||
| "version": "5.0.0-beta.76", | ||
| "description": "Core engine for Kubb. Provides the plugin driver, file manager and build orchestration used by every Kubb plugin.", | ||
| "keywords": [ | ||
| "ast", | ||
| "code-generator", | ||
| "codegen", | ||
| "core-library", | ||
| "file-system", | ||
| "kubb", | ||
| "oas", | ||
| "openapi", | ||
| "plugin-framework", | ||
| "meta-framework", | ||
| "plugin-system", | ||
| "plugins", | ||
| "swagger", | ||
| "typescript" | ||
@@ -28,3 +21,2 @@ ], | ||
| "files": [ | ||
| "src", | ||
| "schemas", | ||
@@ -67,31 +59,17 @@ "dist", | ||
| "dependencies": { | ||
| "fflate": "^0.8.2", | ||
| "tinyexec": "^1.1.2", | ||
| "@kubb/ast": "5.0.0-beta.75" | ||
| "@kubb/ast": "5.0.0-beta.76" | ||
| }, | ||
| "devDependencies": { | ||
| "p-limit": "^7.3.0", | ||
| "@internals/utils": "0.0.0", | ||
| "@kubb/renderer-jsx": "5.0.0-beta.75" | ||
| "@kubb/renderer-jsx": "5.0.0-beta.76" | ||
| }, | ||
| "peerDependencies": { | ||
| "@kubb/renderer-jsx": "5.0.0-beta.75" | ||
| "@kubb/renderer-jsx": "5.0.0-beta.76" | ||
| }, | ||
| "size-limit": [ | ||
| { | ||
| "path": "./dist/*.js", | ||
| "limit": "510 KiB", | ||
| "gzip": true | ||
| } | ||
| ], | ||
| "engines": { | ||
| "node": ">=22" | ||
| }, | ||
| "inlinedDependencies": { | ||
| "p-limit": "7.3.0", | ||
| "yocto-queue": "1.2.2" | ||
| }, | ||
| "scripts": { | ||
| "build": "tsdown && size-limit", | ||
| "clean": "npx rimraf ./dist", | ||
| "build": "tsdown", | ||
| "clean": "node -e \"require('node:fs').rmSync('./dist', {recursive:true,force:true})\"", | ||
| "lint": "oxlint .", | ||
@@ -101,2 +79,3 @@ "lint:fix": "oxlint --fix .", | ||
| "release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check", | ||
| "release:stage": "pnpm stage publish --no-git-check", | ||
| "start": "tsdown --watch", | ||
@@ -103,0 +82,0 @@ "test": "vitest --passWithNoTests", |
+25
-158
| <div align="center"> | ||
| <a href="https://kubb.dev" target="_blank" rel="noopener noreferrer"> | ||
| <img width="180" src="https://raw.githubusercontent.com/kubb-labs/kubb/main/assets/logo.png" alt="Kubb logo"> | ||
| <img src="https://kubb.dev/og.png" alt="Kubb banner"> | ||
| </a> | ||
@@ -8,10 +8,6 @@ | ||
| [![npm downloads][npm-downloads-src]][npm-downloads-href] | ||
| [![Coverage][coverage-src]][coverage-href] | ||
| [![Stars][stars-src]][stars-href] | ||
| [![License][license-src]][license-href] | ||
| [![Sponsors][sponsors-src]][sponsors-href] | ||
| [![Node][node-src]][node-href] | ||
| ### The meta framework for code generation | ||
| **Stop writing glue code. Define your API once and Kubb generates types, clients, hooks, validators, mocks and more.** | ||
| <h4> | ||
@@ -28,48 +24,26 @@ <a href="https://kubb.dev" target="_blank">Documentation</a> | ||
| ## Installation | ||
| # @kubb/core | ||
| ```bash | ||
| npm install @kubb/core | ||
| # or | ||
| pnpm add @kubb/core | ||
| ``` | ||
| ### Core engine for Kubb | ||
| ## Quick Start | ||
| Core engine for Kubb's plugin-based code generation system. Provides the plugin driver, file manager, `defineConfig`, `definePlugin`, `defineMiddleware`, and the build orchestration layer used by every Kubb plugin. | ||
| Get started with Kubb in seconds: | ||
| > **Note:** Most users should install the [`kubb`](https://npmx.dev/package/kubb) meta-package instead of `@kubb/core` directly. Install `@kubb/core` only when building custom plugins or extending the Kubb internals. | ||
| ```bash | ||
| npx kubb init | ||
| ``` | ||
| ## Installation | ||
| The interactive setup will: | ||
| - Create a `package.json` (if needed) | ||
| - Guide you through plugin selection | ||
| - Install packages automatically | ||
| - Generate `kubb.config.ts` | ||
| Then generate your code: | ||
| ```bash | ||
| npx kubb generate | ||
| bun add @kubb/core | ||
| # or | ||
| pnpm add @kubb/core | ||
| # or | ||
| npm install @kubb/core | ||
| ``` | ||
| See the [documentation](https://kubb.dev) for detailed usage and advanced features. | ||
| ## Features | ||
| - Works with Node.js 22+ and TypeScript 6. | ||
| - Convert Swagger 2.0, OpenAPI 3.0, and OpenAPI 3.1 to TypeScript types, API clients, and more via the [plugin ecosystem](https://github.com/kubb-labs/kubb-plugins). | ||
| - Extensible plugin and middleware system for customizing and composing code generation. | ||
| - CLI support with interactive setup, progress bar, and detailed logs. | ||
| - Model Context Protocol (MCP) server for AI assistants like [Claude](https://claude.ai), [Cursor](https://cursor.sh), and other MCP-compatible tools. | ||
| - JSX-based renderer (`@kubb/renderer-jsx`) for building custom plugin output. | ||
| - Barrel file generation via the `@kubb/middleware-barrel` middleware. | ||
| ## Supporting Kubb | ||
| Kubb is an open source project with its ongoing development made possible entirely by the support of Sponsors. If you would like to become a sponsor, please consider: | ||
| Kubb is an open source project, and its development is funded entirely by sponsors. If you would like to become a sponsor, please consider: | ||
| - [Become a Sponsor on GitHub](https://github.com/sponsors/stijnvanhulle) | ||
| - [See sponsorship tiers and our sponsors](https://kubb.dev/sponsors) | ||
@@ -82,124 +56,17 @@ <p align="center"> | ||
| ## Contributors [![Contributors][contributors-src]][contributors-href] | ||
| <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --> | ||
| <!-- prettier-ignore-start --> | ||
| <!-- markdownlint-disable --> | ||
| <table> | ||
| <tbody> | ||
| <tr> | ||
| <td align="center" valign="top" width="14.28%"><a href="http://www.stijnvanhulle.be"><img src="https://avatars.githubusercontent.com/u/5904681?v=4?s=100" width="100px;" alt="Stijn Van Hulle"/><br /><sub><b>Stijn Van Hulle</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=stijnvanhulle" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://aluc.io/"><img src="https://avatars.githubusercontent.com/u/15520015?v=4?s=100" width="100px;" alt="Alfred"/><br /><sub><b>Alfred</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=b6pzeusbc54tvhw5jgpyw8pwz2x6gs" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/raveclassic"><img src="https://avatars.githubusercontent.com/u/1743568?v=4?s=100" width="100px;" alt="Kirill Agalakov"/><br /><sub><b>Kirill Agalakov</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=raveclassic" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="http://wicky.nillia.ms"><img src="https://avatars.githubusercontent.com/u/1091390?v=4?s=100" width="100px;" alt="Nick Williams"/><br /><sub><b>Nick Williams</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=WickyNilliams" title="Documentation">📖</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/helt"><img src="https://avatars.githubusercontent.com/u/1732112?v=4?s=100" width="100px;" alt="helt"/><br /><sub><b>helt</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=helt" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/Ti-webdev"><img src="https://avatars.githubusercontent.com/u/478565?v=4?s=100" width="100px;" alt="Vasily Mikhaylovsky"/><br /><sub><b>Vasily Mikhaylovsky</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=Ti-webdev" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/chiptus"><img src="https://avatars.githubusercontent.com/u/1381655?v=4?s=100" width="100px;" alt="Chaim Lev-Ari"/><br /><sub><b>Chaim Lev-Ari</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=chiptus" title="Code">💻</a></td> | ||
| </tr> | ||
| <tr> | ||
| <td align="center" valign="top" width="14.28%"><a href="http://projects.pafnuty.name"><img src="https://avatars.githubusercontent.com/u/1635679?v=4?s=100" width="100px;" alt="Pavel Belousov"/><br /><sub><b>Pavel Belousov</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=pafnuty" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/dmitry-blackwave"><img src="https://avatars.githubusercontent.com/u/5526543?v=4?s=100" width="100px;" alt="Dmitry Belov"/><br /><sub><b>Dmitry Belov</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=dmitry-blackwave" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/aburgel"><img src="https://avatars.githubusercontent.com/u/341478?v=4?s=100" width="100px;" alt="Alex Burgel"/><br /><sub><b>Alex Burgel</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=aburgel" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/dgarciamuria"><img src="https://avatars.githubusercontent.com/u/8144333?v=4?s=100" width="100px;" alt="Daniel Garcia"/><br /><sub><b>Daniel Garcia</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=dgarciamuria" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/wuyuanyi135"><img src="https://avatars.githubusercontent.com/u/11760870?v=4?s=100" width="100px;" alt="wuyuanyi135"/><br /><sub><b>wuyuanyi135</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=wuyuanyi135" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/cjthompson"><img src="https://avatars.githubusercontent.com/u/1958266?v=4?s=100" width="100px;" alt="Chris Thompson"/><br /><sub><b>Chris Thompson</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=cjthompson" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/hkang1"><img src="https://avatars.githubusercontent.com/u/220971?v=4?s=100" width="100px;" alt="Caleb Hoyoul Kang"/><br /><sub><b>Caleb Hoyoul Kang</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=hkang1" title="Code">💻</a></td> | ||
| </tr> | ||
| <tr> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/grreeenn"><img src="https://avatars.githubusercontent.com/u/13204857?v=4?s=100" width="100px;" alt="Gregory Zhukovsky"/><br /><sub><b>Gregory Zhukovsky</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=grreeenn" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/ChilloManiac"><img src="https://avatars.githubusercontent.com/u/3761964?v=4?s=100" width="100px;" alt="Christoffer Nørbjerg"/><br /><sub><b>Christoffer Nørbjerg</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=ChilloManiac" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://codefy.de/de/karriere"><img src="https://avatars.githubusercontent.com/u/122524301?v=4?s=100" width="100px;" alt="CHE1RON"/><br /><sub><b>CHE1RON</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=CHE1RON" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/ekaradon"><img src="https://avatars.githubusercontent.com/u/9439390?v=4?s=100" width="100px;" alt="ekaradon"/><br /><sub><b>ekaradon</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=ekaradon" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://thijmen.dev"><img src="https://avatars.githubusercontent.com/u/383903?v=4?s=100" width="100px;" alt="Thijmen Stavenuiter"/><br /><sub><b>Thijmen Stavenuiter</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=Thijmen" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/bohdanhusak"><img src="https://avatars.githubusercontent.com/u/13829370?v=4?s=100" width="100px;" alt="Bohdan Husak"/><br /><sub><b>Bohdan Husak</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=bohdanhusak" title="Documentation">📖</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/Ericlm"><img src="https://avatars.githubusercontent.com/u/19361503?v=4?s=100" width="100px;" alt="Éric Le Maître"/><br /><sub><b>Éric Le Maître</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=Ericlm" title="Code">💻</a></td> | ||
| </tr> | ||
| <tr> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/chambber"><img src="https://avatars.githubusercontent.com/u/11406841?v=4?s=100" width="100px;" alt="Rubens Pereira do Nascimento"/><br /><sub><b>Rubens Pereira do Nascimento</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=chambber" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/msutkowski"><img src="https://avatars.githubusercontent.com/u/784953?v=4?s=100" width="100px;" alt="Matt Sutkowski"/><br /><sub><b>Matt Sutkowski</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=msutkowski" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/vitorcamachoo"><img src="https://avatars.githubusercontent.com/u/20595956?v=4?s=100" width="100px;" alt="Vítor Camacho"/><br /><sub><b>Vítor Camacho</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=vitorcamachoo" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/VasekProchazka"><img src="https://avatars.githubusercontent.com/u/13906845?v=4?s=100" width="100px;" alt="Václav Procházka"/><br /><sub><b>Václav Procházka</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=VasekProchazka" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://codx.dev"><img src="https://avatars.githubusercontent.com/u/59735735?v=4?s=100" width="100px;" alt="Luiz Bett"/><br /><sub><b>Luiz Bett</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=heyBett" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/lambdank"><img src="https://avatars.githubusercontent.com/u/5475129?v=4?s=100" width="100px;" alt="Sebastian Andersen"/><br /><sub><b>Sebastian Andersen</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=lambdank" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://akino.icu"><img src="https://avatars.githubusercontent.com/u/64176534?v=4?s=100" width="100px;" alt="Akino"/><br /><sub><b>Akino</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=akinoccc" title="Code">💻</a></td> | ||
| </tr> | ||
| <tr> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/rmachado-studocu"><img src="https://avatars.githubusercontent.com/u/89906313?v=4?s=100" width="100px;" alt="Ricardo Machado"/><br /><sub><b>Ricardo Machado</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=rmachado-studocu" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="http://simonelnahas.com"><img src="https://avatars.githubusercontent.com/u/29279201?v=4?s=100" width="100px;" alt="Simon El Nahas"/><br /><sub><b>Simon El Nahas</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=simonelnahas" title="Documentation">📖</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/maartenvansambeek"><img src="https://avatars.githubusercontent.com/u/91739524?v=4?s=100" width="100px;" alt="maartenvansambeek"/><br /><sub><b>maartenvansambeek</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=maartenvansambeek" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://sdufresne.info"><img src="https://avatars.githubusercontent.com/u/583851?v=4?s=100" width="100px;" alt="Stefan du Fresne"/><br /><sub><b>Stefan du Fresne</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=SCdF" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://hugofelippe.github.io/"><img src="https://avatars.githubusercontent.com/u/19368365?v=4?s=100" width="100px;" alt="Hugo Felippe de Souza Cruz"/><br /><sub><b>Hugo Felippe de Souza Cruz</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=hugoFelippe" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/belgattitude"><img src="https://avatars.githubusercontent.com/u/259798?v=4?s=100" width="100px;" alt="Sébastien Vanvelthem"/><br /><sub><b>Sébastien Vanvelthem</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=belgattitude" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="http://bento.me/vitalygashkov"><img src="https://avatars.githubusercontent.com/u/30000398?v=4?s=100" width="100px;" alt="Vitaly Gashkov"/><br /><sub><b>Vitaly Gashkov</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=vitalygashkov" title="Documentation">📖</a></td> | ||
| </tr> | ||
| <tr> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://ducduc.nl"><img src="https://avatars.githubusercontent.com/u/9675738?v=4?s=100" width="100px;" alt="Duco Drupsteen"/><br /><sub><b>Duco Drupsteen</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=ducodrupsteen" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/th3l0g4n"><img src="https://avatars.githubusercontent.com/u/326306?v=4?s=100" width="100px;" alt="th3l0g4n"/><br /><sub><b>th3l0g4n</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=th3l0g4n" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://rxliuli.com"><img src="https://avatars.githubusercontent.com/u/24560368?v=4?s=100" width="100px;" alt="rxliuli"/><br /><sub><b>rxliuli</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=rxliuli" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/humarkx"><img src="https://avatars.githubusercontent.com/u/13049940?v=4?s=100" width="100px;" alt="humarkx"/><br /><sub><b>humarkx</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=humarkx" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/Jakub-Cerovsky"><img src="https://avatars.githubusercontent.com/u/141134227?v=4?s=100" width="100px;" alt="Jakub Cerovsky"/><br /><sub><b>Jakub Cerovsky</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=Jakub-Cerovsky" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/yukikwi"><img src="https://avatars.githubusercontent.com/u/66879660?v=4?s=100" width="100px;" alt="Pachara Chantawong"/><br /><sub><b>Pachara Chantawong</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=yukikwi" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://volodymyrkushnir.dev/"><img src="https://avatars.githubusercontent.com/u/10290626?v=4?s=100" width="100px;" alt="Volodymyr Kushnir"/><br /><sub><b>Volodymyr Kushnir</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=volodymyr-kushnir" title="Code">💻</a></td> | ||
| </tr> | ||
| <tr> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/GKNewsrooms"><img src="https://avatars.githubusercontent.com/u/201248633?v=4?s=100" width="100px;" alt="GKNewsrooms"/><br /><sub><b>GKNewsrooms</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=GKNewsrooms" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/stepek"><img src="https://avatars.githubusercontent.com/u/5058678?v=4?s=100" width="100px;" alt="Kamil Stepczuk"/><br /><sub><b>Kamil Stepczuk</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=stepek" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/JoaoBrlt"><img src="https://avatars.githubusercontent.com/u/11065509?v=4?s=100" width="100px;" alt="João Brilhante"/><br /><sub><b>João Brilhante</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=JoaoBrlt" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/kamilzki"><img src="https://avatars.githubusercontent.com/u/27976736?v=4?s=100" width="100px;" alt="Kamil Sieradzki"/><br /><sub><b>Kamil Sieradzki</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=kamilzki" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/EricPierlotIdmog"><img src="https://avatars.githubusercontent.com/u/124898024?v=4?s=100" width="100px;" alt="Eric Pierlot"/><br /><sub><b>Eric Pierlot</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=EricPierlotIdmog" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="http://atholin.se"><img src="https://avatars.githubusercontent.com/u/33940473?v=4?s=100" width="100px;" alt="Alexander Sjöcrona Tholin"/><br /><sub><b>Alexander Sjöcrona Tholin</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=ATholin" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="http://hyoban.cc"><img src="https://avatars.githubusercontent.com/u/38493346?v=4?s=100" width="100px;" alt="Stephen Zhou"/><br /><sub><b>Stephen Zhou</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=hyoban" title="Code">💻</a></td> | ||
| </tr> | ||
| <tr> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://choly.ca"><img src="https://avatars.githubusercontent.com/u/943597?v=4?s=100" width="100px;" alt="Ilia Choly"/><br /><sub><b>Ilia Choly</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=icholy" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/skoropadas"><img src="https://avatars.githubusercontent.com/u/20700969?v=4?s=100" width="100px;" alt="Alex Skoropad"/><br /><sub><b>Alex Skoropad</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=skoropadas" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://pateljay.io"><img src="https://avatars.githubusercontent.com/u/36803168?v=4?s=100" width="100px;" alt="Jay Patel"/><br /><sub><b>Jay Patel</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=jay-babu" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://hamzamihaidaniel.com"><img src="https://avatars.githubusercontent.com/u/12731515?v=4?s=100" width="100px;" alt="Hamza Mihai Daniel"/><br /><sub><b>Hamza Mihai Daniel</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=hamzamihaidanielx" title="Code">💻</a></td> | ||
| <td align="center" valign="top" width="14.28%"><a href="https://github.com/saschabuehrle"><img src="https://avatars.githubusercontent.com/u/47737812?v=4?s=100" width="100px;" alt="Sascha Buehrle"/><br /><sub><b>Sascha Buehrle</b></sub></a><br /><a href="https://github.com/kubb-labs/kubb/commits?author=saschabuehrle" title="Code">💻</a></td> | ||
| </tr> | ||
| </tbody> | ||
| </table> | ||
| <!-- markdownlint-restore --> | ||
| <!-- prettier-ignore-end --> | ||
| <!-- ALL-CONTRIBUTORS-LIST:END --> | ||
| ## License | ||
| Most of this repository is licensed under the [MIT License](./licenses/LICENSE-MIT), Copyright © 2025 [Stijn Van Hulle](https://stijnvanhulle.be). Some components are licensed | ||
| under AGPL-3.0-or-later. | ||
| [MIT](https://github.com/kubb-labs/kubb/blob/main/licenses/LICENSE-MIT) | ||
| - **Most packages** — [MIT](./licenses/LICENSE-MIT) | ||
| - **`@kubb/agent`** — [AGPL-3.0-or-later](./licenses/LICENSE-AGPL-3.0) | ||
| See [LICENSE](./LICENSE) for details. | ||
| ## Star History | ||
| <a href="https://star-history.com/#kubb-labs/kubb&Date"> | ||
| <picture> | ||
| <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=kubb-labs/kubb&type=Date&theme=dark" /> | ||
| <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=kubb-labs/kubb&type=Date" /> | ||
| <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=kubb-labs/kubb&type=Date" /> | ||
| </picture> | ||
| </a> | ||
| <!-- Badges --> | ||
| [npm-version-src]: https://img.shields.io/npm/v/@kubb/core?flat&colorA=18181B&colorB=f58517 | ||
| [npm-version-src]: https://shieldcn.dev/npm/v/@kubb/core.svg?variant=secondary&size=xs&theme=zinc&mode=dark | ||
| [npm-version-href]: https://npmx.dev/package/@kubb/core | ||
| [npm-downloads-src]: https://img.shields.io/npm/dm/@kubb/core?flat&colorA=18181B&colorB=f58517 | ||
| [npm-downloads-href]: https://npmjs.com/package/@kubb/core | ||
| [license-src]: https://img.shields.io/npm/l/%40kubb%2Fcore?flat&colorA=18181B&colorB=f58517 | ||
| [license-href]: https://github.com/kubb-labs/kubb/blob/main/licenses/LICENSE-MIT | ||
| [build-src]: https://img.shields.io/github/actions/workflow/status/kubb-labs/kubb/ci.yaml?style=flat&colorA=18181B&colorB=f58517 | ||
| [build-href]: https://www.npmjs.com/package/@kubb/core | ||
| [minified-src]: https://img.shields.io/bundlephobia/min/@kubb/core?style=flat&colorA=18181B&colorB=f58517 | ||
| [minified-href]: https://www.npmjs.com/package/@kubb/core | ||
| [coverage-src]: https://img.shields.io/codecov/c/github/kubb-labs/kubb?style=flat&colorA=18181B&colorB=f58517 | ||
| [coverage-href]: https://www.npmjs.com/package/@kubb/core | ||
| [contributors-src]: https://img.shields.io/github/contributors/kubb-labs/kubb?style=flat&colorA=18181B&colorB=f58517&label=%20 | ||
| [contributors-href]: #contributors- | ||
| [sponsors-src]: https://img.shields.io/github/sponsors/stijnvanhulle?style=flat&colorA=18181B&colorB=f58517 | ||
| [sponsors-href]: https://github.com/sponsors/stijnvanhulle/ | ||
| [npm-downloads-src]: https://shieldcn.dev/npm/dm/@kubb/core.svg?variant=secondary&size=xs&theme=zinc&mode=dark | ||
| [npm-downloads-href]: https://npmx.dev/package/@kubb/core | ||
| [stars-src]: https://shieldcn.dev/github/stars/kubb-labs/kubb.svg?variant=secondary&size=xs&theme=zinc&mode=dark | ||
| [stars-href]: https://github.com/kubb-labs/kubb | ||
| [license-src]: https://shieldcn.dev/npm/license/@kubb/core.svg?variant=secondary&size=xs&theme=zinc | ||
| [license-href]: https://github.com/kubb-labs/kubb/blob/main/LICENSE | ||
| [node-src]: https://shieldcn.dev/npm/node/@kubb/core.svg?variant=secondary&size=xs&theme=zinc&mode=dark | ||
| [node-href]: https://npmx.dev/package/@kubb/core |
| //#region \0rolldown/runtime.js | ||
| var __defProp = Object.defineProperty; | ||
| var __name = (target, value) => __defProp(target, "name", { | ||
| value, | ||
| configurable: true | ||
| }); | ||
| //#endregion | ||
| export { __name as t }; |
| //#region \0rolldown/runtime.js | ||
| var __create = Object.create; | ||
| var __defProp = Object.defineProperty; | ||
| var __name = (target, value) => __defProp(target, "name", { | ||
| value, | ||
| configurable: true | ||
| }); | ||
| var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | ||
| var __getOwnPropNames = Object.getOwnPropertyNames; | ||
| var __getProtoOf = Object.getPrototypeOf; | ||
| var __hasOwnProp = Object.prototype.hasOwnProperty; | ||
| var __copyProps = (to, from, except, desc) => { | ||
| if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { | ||
| key = keys[i]; | ||
| if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { | ||
| get: ((k) => from[k]).bind(null, key), | ||
| enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable | ||
| }); | ||
| } | ||
| return to; | ||
| }; | ||
| var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { | ||
| value: mod, | ||
| enumerable: true | ||
| }) : target, mod)); | ||
| //#endregion | ||
| let node_path = require("node:path"); | ||
| node_path = __toESM(node_path, 1); | ||
| let _kubb_ast = require("@kubb/ast"); | ||
| let fflate = require("fflate"); | ||
| let tinyexec = require("tinyexec"); | ||
| //#region ../../internals/utils/src/casing.ts | ||
| /** | ||
| * Shared implementation for camelCase and PascalCase conversion. | ||
| * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons) | ||
| * and capitalizes each word according to `pascal`. | ||
| * | ||
| * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are. | ||
| */ | ||
| function toCamelOrPascal(text, pascal) { | ||
| 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) => { | ||
| if (word.length > 1 && word === word.toUpperCase()) return word; | ||
| if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1); | ||
| return word.charAt(0).toUpperCase() + word.slice(1); | ||
| }).join("").replace(/[^a-zA-Z0-9]/g, ""); | ||
| } | ||
| /** | ||
| * Splits `text` on `.` and applies `transformPart` to each segment. | ||
| * The last segment receives `isLast = true`, all earlier segments receive `false`. | ||
| * Segments are joined with `/` to form a file path. | ||
| * | ||
| * Only splits on dots followed by a letter so that version numbers | ||
| * embedded in operationIds (e.g. `v2025.0`) are kept intact. | ||
| * | ||
| * Empty segments are filtered before joining. They arise when the text starts with | ||
| * a dot followed immediately by a letter (e.g. `..Schema` splits into `['..', 'Schema']` | ||
| * and `'..'` transforms to an empty string). Without this filter the join would produce | ||
| * a leading `/`, which `path.resolve` would interpret as an absolute path, allowing | ||
| * generated files to escape the configured output directory. | ||
| */ | ||
| function applyToFileParts(text, transformPart) { | ||
| const parts = text.split(/\.(?=[a-zA-Z])/); | ||
| return parts.map((part, i) => transformPart(part, i === parts.length - 1)).filter(Boolean).join("/"); | ||
| } | ||
| /** | ||
| * Converts `text` to camelCase. | ||
| * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`. | ||
| * | ||
| * @example | ||
| * camelCase('hello-world') // 'helloWorld' | ||
| * camelCase('pet.petId', { isFile: true }) // 'pet/petId' | ||
| */ | ||
| function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) { | ||
| if (isFile) return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? { | ||
| prefix, | ||
| suffix | ||
| } : {})); | ||
| return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false); | ||
| } | ||
| /** | ||
| * Converts `text` to PascalCase. | ||
| * When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased. | ||
| * | ||
| * @example | ||
| * pascalCase('hello-world') // 'HelloWorld' | ||
| * pascalCase('pet.petId', { isFile: true }) // 'pet/PetId' | ||
| */ | ||
| function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) { | ||
| if (isFile) return applyToFileParts(text, (part, isLast) => isLast ? pascalCase(part, { | ||
| prefix, | ||
| suffix | ||
| }) : camelCase(part)); | ||
| return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true); | ||
| } | ||
| //#endregion | ||
| //#region src/constants.ts | ||
| /** | ||
| * Base URL for the Kubb Studio web app. | ||
| */ | ||
| const DEFAULT_STUDIO_URL = "https://studio.kubb.dev"; | ||
| /** | ||
| * Default banner style written at the top of every generated file. | ||
| */ | ||
| const DEFAULT_BANNER = "simple"; | ||
| /** | ||
| * Default file-extension mapping used when no explicit mapping is configured. | ||
| */ | ||
| const DEFAULT_EXTENSION = { ".ts": ".ts" }; | ||
| /** | ||
| * Numeric log-level thresholds used internally to compare verbosity. | ||
| * | ||
| * Higher numbers are more verbose. | ||
| */ | ||
| const logLevel = { | ||
| silent: Number.NEGATIVE_INFINITY, | ||
| error: 0, | ||
| warn: 1, | ||
| info: 3, | ||
| verbose: 4, | ||
| debug: 5 | ||
| }; | ||
| //#endregion | ||
| //#region src/defineResolver.ts | ||
| const stringPatternCache = /* @__PURE__ */ new Map(); | ||
| function testPattern(value, pattern) { | ||
| if (typeof pattern === "string") { | ||
| let regex = stringPatternCache.get(pattern); | ||
| if (!regex) { | ||
| regex = new RegExp(pattern); | ||
| stringPatternCache.set(pattern, regex); | ||
| } | ||
| return regex.test(value); | ||
| } | ||
| return value.match(pattern) !== null; | ||
| } | ||
| /** | ||
| * Checks if an operation matches a pattern for a given filter type (`tag`, `operationId`, `path`, `method`). | ||
| */ | ||
| function matchesOperationPattern(node, type, pattern) { | ||
| switch (type) { | ||
| case "tag": return node.tags.some((tag) => testPattern(tag, pattern)); | ||
| case "operationId": return testPattern(node.operationId, pattern); | ||
| case "path": return testPattern(node.path, pattern); | ||
| case "method": return testPattern(node.method.toLowerCase(), pattern); | ||
| case "contentType": return node.requestBody?.content?.some((c) => testPattern(c.contentType, pattern)) ?? false; | ||
| default: return false; | ||
| } | ||
| } | ||
| /** | ||
| * Checks if a schema matches a pattern for a given filter type (`schemaName`). | ||
| * | ||
| * Returns `null` when the filter type doesn't apply to schemas. | ||
| */ | ||
| function matchesSchemaPattern(node, type, pattern) { | ||
| switch (type) { | ||
| case "schemaName": return node.name ? testPattern(node.name, pattern) : false; | ||
| default: return null; | ||
| } | ||
| } | ||
| /** | ||
| * Default name resolver used by `defineResolver`. | ||
| * | ||
| * - `camelCase` for `function` and `file` types. | ||
| * - `PascalCase` for `type`. | ||
| * - `camelCase` for everything else. | ||
| */ | ||
| function defaultResolver(name, type) { | ||
| let resolvedName = camelCase(name); | ||
| if (type === "file" || type === "function") resolvedName = camelCase(name, { isFile: type === "file" }); | ||
| if (type === "type") resolvedName = pascalCase(name); | ||
| return resolvedName; | ||
| } | ||
| /** | ||
| * Default option resolver — applies include/exclude filters and merges matching override options. | ||
| * | ||
| * Returns `null` when the node is filtered out by an `exclude` rule or not matched by any `include` rule. | ||
| * | ||
| * @example Include/exclude filtering | ||
| * ```ts | ||
| * const options = defaultResolveOptions(operationNode, { | ||
| * options: { output: 'types' }, | ||
| * exclude: [{ type: 'tag', pattern: 'internal' }], | ||
| * }) | ||
| * // → null when node has tag 'internal' | ||
| * ``` | ||
| * | ||
| * @example Override merging | ||
| * ```ts | ||
| * const options = defaultResolveOptions(operationNode, { | ||
| * options: { enumType: 'asConst' }, | ||
| * override: [{ type: 'operationId', pattern: 'listPets', options: { enumType: 'enum' } }], | ||
| * }) | ||
| * // → { enumType: 'enum' } when operationId matches | ||
| * ``` | ||
| */ | ||
| function defaultResolveOptions(node, { options, exclude = [], include, override = [] }) { | ||
| if ((0, _kubb_ast.isOperationNode)(node)) { | ||
| if (exclude.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null; | ||
| if (include && !include.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null; | ||
| const overrideOptions = override.find(({ type, pattern }) => matchesOperationPattern(node, type, pattern))?.options; | ||
| return { | ||
| ...options, | ||
| ...overrideOptions | ||
| }; | ||
| } | ||
| if ((0, _kubb_ast.isSchemaNode)(node)) { | ||
| if (exclude.some(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)) return null; | ||
| if (include) { | ||
| const applicable = include.map(({ type, pattern }) => matchesSchemaPattern(node, type, pattern)).filter((r) => r !== null); | ||
| if (applicable.length > 0 && !applicable.includes(true)) return null; | ||
| } | ||
| const overrideOptions = override.find(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)?.options; | ||
| return { | ||
| ...options, | ||
| ...overrideOptions | ||
| }; | ||
| } | ||
| return options; | ||
| } | ||
| /** | ||
| * Default path resolver used by `defineResolver`. | ||
| * | ||
| * - Returns the output directory in `single` mode. | ||
| * - Resolves into a tag- or path-based subdirectory when `group` and a `tag`/`path` value are provided. | ||
| * - Falls back to a flat `output/baseName` path otherwise. | ||
| * | ||
| * A custom `group.name` function overrides the default subdirectory naming. | ||
| * For `tag` groups the default is `${camelCase(tag)}Controller`. | ||
| * For `path` groups the default is the first path segment after `/`. | ||
| * | ||
| * @example Flat output | ||
| * ```ts | ||
| * defaultResolvePath({ baseName: 'petTypes.ts' }, { root: '/src', output: { path: 'types' } }) | ||
| * // → '/src/types/petTypes.ts' | ||
| * ``` | ||
| * | ||
| * @example Tag-based grouping | ||
| * ```ts | ||
| * defaultResolvePath( | ||
| * { baseName: 'petTypes.ts', tag: 'pets' }, | ||
| * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } }, | ||
| * ) | ||
| * // → '/src/types/petsController/petTypes.ts' | ||
| * ``` | ||
| * | ||
| * @example Path-based grouping | ||
| * ```ts | ||
| * defaultResolvePath( | ||
| * { baseName: 'petTypes.ts', path: '/pets/list' }, | ||
| * { root: '/src', output: { path: 'types' }, group: { type: 'path' } }, | ||
| * ) | ||
| * // → '/src/types/pets/petTypes.ts' | ||
| * ``` | ||
| * | ||
| * @example Single-file mode | ||
| * ```ts | ||
| * defaultResolvePath( | ||
| * { baseName: 'petTypes.ts', pathMode: 'single' }, | ||
| * { root: '/src', output: { path: 'types' } }, | ||
| * ) | ||
| * // → '/src/types' | ||
| * ``` | ||
| */ | ||
| function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }, { root, output, group }) { | ||
| if ((pathMode ?? PluginDriver.getMode(node_path.default.resolve(root, output.path))) === "single") return node_path.default.resolve(root, output.path); | ||
| let result; | ||
| if (group && (groupPath || tag)) { | ||
| const groupValue = group.type === "path" ? groupPath : tag; | ||
| const defaultName = group.type === "tag" ? ({ group: g }) => `${camelCase(g)}Controller` : ({ group: g }) => { | ||
| const segment = g.split("/").filter((s) => s !== "" && s !== "." && s !== "..")[0]; | ||
| return segment ? camelCase(segment) : ""; | ||
| }; | ||
| const resolveName = group.name ?? defaultName; | ||
| result = node_path.default.resolve(root, output.path, resolveName({ group: groupValue }), baseName); | ||
| } else result = node_path.default.resolve(root, output.path, baseName); | ||
| const outputDir = node_path.default.resolve(root, output.path); | ||
| const outputDirWithSep = outputDir.endsWith(node_path.default.sep) ? outputDir : `${outputDir}${node_path.default.sep}`; | ||
| if (result !== outputDir && !result.startsWith(outputDirWithSep)) throw new Error(`[Kubb] Resolved path "${result}" is outside the output directory "${outputDir}". This may indicate a path traversal attempt in the OpenAPI specification or a misconfigured group.name function.`); | ||
| return result; | ||
| } | ||
| /** | ||
| * Default file resolver used by `defineResolver`. | ||
| * | ||
| * Resolves a `FileNode` by combining name resolution (`resolver.default`) with | ||
| * path resolution (`resolver.resolvePath`). The resolved file always has empty | ||
| * `sources`, `imports`, and `exports` arrays — consumers populate those separately. | ||
| * | ||
| * In `single` mode the name is omitted and the file sits directly in the output directory. | ||
| * | ||
| * @example Resolve a schema file | ||
| * ```ts | ||
| * const file = defaultResolveFile( | ||
| * { name: 'pet', extname: '.ts' }, | ||
| * { root: '/src', output: { path: 'types' } }, | ||
| * resolver, | ||
| * ) | ||
| * // → { baseName: 'pet.ts', path: '/src/types/pet.ts', sources: [], ... } | ||
| * ``` | ||
| * | ||
| * @example Resolve an operation file with tag grouping | ||
| * ```ts | ||
| * const file = defaultResolveFile( | ||
| * { name: 'listPets', extname: '.ts', tag: 'pets' }, | ||
| * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } }, | ||
| * resolver, | ||
| * ) | ||
| * // → { baseName: 'listPets.ts', path: '/src/types/petsController/listPets.ts', ... } | ||
| * ``` | ||
| */ | ||
| function defaultResolveFile({ name, extname, tag, path: groupPath }, context, ctx) { | ||
| const pathMode = PluginDriver.getMode(node_path.default.resolve(context.root, context.output.path)); | ||
| const baseName = `${pathMode === "single" ? "" : ctx.default(name, "file")}${extname}`; | ||
| const filePath = ctx.resolvePath({ | ||
| baseName, | ||
| pathMode, | ||
| tag, | ||
| path: groupPath | ||
| }, context); | ||
| return (0, _kubb_ast.createFile)({ | ||
| path: filePath, | ||
| baseName: node_path.default.basename(filePath), | ||
| meta: { pluginName: ctx.pluginName }, | ||
| sources: [], | ||
| imports: [], | ||
| exports: [] | ||
| }); | ||
| } | ||
| /** | ||
| * Generates the default "Generated by Kubb" banner from config and optional node metadata. | ||
| */ | ||
| function buildDefaultBanner({ title, description, version, config }) { | ||
| try { | ||
| let source = ""; | ||
| if (Array.isArray(config.input)) { | ||
| const first = config.input[0]; | ||
| if (first && "path" in first) source = node_path.default.basename(first.path); | ||
| } else if ("path" in config.input) source = node_path.default.basename(config.input.path); | ||
| else if ("data" in config.input) source = "text content"; | ||
| let banner = "/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n"; | ||
| if (config.output.defaultBanner === "simple") { | ||
| banner += "*/\n"; | ||
| return banner; | ||
| } | ||
| if (source) banner += `* Source: ${source}\n`; | ||
| if (title) banner += `* Title: ${title}\n`; | ||
| if (description) { | ||
| const formattedDescription = description.replace(/\n/gm, "\n* "); | ||
| banner += `* Description: ${formattedDescription}\n`; | ||
| } | ||
| if (version) banner += `* OpenAPI spec version: ${version}\n`; | ||
| banner += "*/\n"; | ||
| return banner; | ||
| } catch (_error) { | ||
| return "/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n*/"; | ||
| } | ||
| } | ||
| /** | ||
| * Default banner resolver — returns the banner string for a generated file. | ||
| * | ||
| * A user-supplied `output.banner` overrides the default Kubb "Generated by Kubb" notice. | ||
| * When no `output.banner` is set, the Kubb notice is used (including `title` and `version` | ||
| * from the OAS spec when a `node` is provided). | ||
| * | ||
| * - When `output.banner` is a function and `node` is provided, returns `output.banner(node)`. | ||
| * - When `output.banner` is a function and `node` is absent, falls back to the Kubb notice. | ||
| * - When `output.banner` is a string, returns it directly. | ||
| * - When `config.output.defaultBanner` is `false`, returns `undefined`. | ||
| * - Otherwise returns the Kubb "Generated by Kubb" notice. | ||
| * | ||
| * @example String banner overrides default | ||
| * ```ts | ||
| * defaultResolveBanner(undefined, { output: { banner: '// my banner' }, config }) | ||
| * // → '// my banner' | ||
| * ``` | ||
| * | ||
| * @example Function banner with node | ||
| * ```ts | ||
| * defaultResolveBanner(inputNode, { output: { banner: (node) => `// v${node.version}` }, config }) | ||
| * // → '// v3.0.0' | ||
| * ``` | ||
| * | ||
| * @example No user banner — Kubb notice with OAS metadata | ||
| * ```ts | ||
| * defaultResolveBanner(inputNode, { config }) | ||
| * // → '/** Generated by Kubb ... Title: Pet Store ... *\/' | ||
| * ``` | ||
| * | ||
| * @example Disabled default banner | ||
| * ```ts | ||
| * defaultResolveBanner(undefined, { config: { output: { defaultBanner: false }, ...config } }) | ||
| * // → undefined | ||
| * ``` | ||
| */ | ||
| function defaultResolveBanner(node, { output, config }) { | ||
| if (typeof output?.banner === "function") return output.banner(node); | ||
| if (typeof output?.banner === "string") return output.banner; | ||
| if (config.output.defaultBanner === false) return; | ||
| return buildDefaultBanner({ | ||
| title: node?.meta?.title, | ||
| version: node?.meta?.version, | ||
| config | ||
| }); | ||
| } | ||
| /** | ||
| * Default footer resolver — returns the footer string for a generated file. | ||
| * | ||
| * - When `output.footer` is a function and `node` is provided, calls it with the node. | ||
| * - When `output.footer` is a function and `node` is absent, returns `undefined`. | ||
| * - When `output.footer` is a string, returns it directly. | ||
| * - Otherwise returns `undefined`. | ||
| * | ||
| * @example String footer | ||
| * ```ts | ||
| * defaultResolveFooter(undefined, { output: { footer: '// end of file' }, config }) | ||
| * // → '// end of file' | ||
| * ``` | ||
| * | ||
| * @example Function footer with node | ||
| * ```ts | ||
| * defaultResolveFooter(inputNode, { output: { footer: (node) => `// ${node.title}` }, config }) | ||
| * // → '// Pet Store' | ||
| * ``` | ||
| */ | ||
| function defaultResolveFooter(node, { output }) { | ||
| if (typeof output?.footer === "function") return node ? output.footer(node) : void 0; | ||
| if (typeof output?.footer === "string") return output.footer; | ||
| } | ||
| /** | ||
| * Defines a resolver for a plugin, injecting built-in defaults for name casing, | ||
| * include/exclude/override filtering, path resolution, and file construction. | ||
| * | ||
| * All four defaults can be overridden by providing them in the builder function: | ||
| * - `default` — name casing strategy (camelCase / PascalCase) | ||
| * - `resolveOptions` — include/exclude/override filtering | ||
| * - `resolvePath` — output path computation | ||
| * - `resolveFile` — full `FileNode` construction | ||
| * | ||
| * The builder receives `ctx` — a reference to the assembled resolver — so methods can | ||
| * call sibling resolver methods using `ctx` instead of `this`. | ||
| * | ||
| * @example Basic resolver with naming helpers | ||
| * ```ts | ||
| * export const resolver = defineResolver<PluginTs>((ctx) => ({ | ||
| * name: 'default', | ||
| * resolveName(node) { | ||
| * return ctx.default(node.name, 'function') | ||
| * }, | ||
| * resolveTypedName(node) { | ||
| * return ctx.default(node.name, 'type') | ||
| * }, | ||
| * })) | ||
| * ``` | ||
| * | ||
| * @example Override resolvePath for a custom output structure | ||
| * ```ts | ||
| * export const resolver = defineResolver<PluginTs>((_ctx) => ({ | ||
| * name: 'custom', | ||
| * resolvePath({ baseName }, { root, output }) { | ||
| * return path.resolve(root, output.path, 'generated', baseName) | ||
| * }, | ||
| * })) | ||
| * ``` | ||
| * | ||
| * @example Use ctx.default inside a helper | ||
| * ```ts | ||
| * export const resolver = defineResolver<PluginTs>((ctx) => ({ | ||
| * name: 'default', | ||
| * resolveParamName(node, param) { | ||
| * return ctx.default(`${node.operationId} ${param.in} ${param.name}`, 'type') | ||
| * }, | ||
| * })) | ||
| * ``` | ||
| */ | ||
| function defineResolver(build) { | ||
| const resolver = {}; | ||
| Object.assign(resolver, { | ||
| default: defaultResolver, | ||
| resolveOptions: defaultResolveOptions, | ||
| resolvePath: defaultResolvePath, | ||
| resolveFile: (params, context) => defaultResolveFile(params, context, resolver), | ||
| resolveBanner: defaultResolveBanner, | ||
| resolveFooter: defaultResolveFooter, | ||
| ...build(resolver) | ||
| }); | ||
| return resolver; | ||
| } | ||
| //#endregion | ||
| //#region src/devtools.ts | ||
| /** | ||
| * Encodes an `InputNode` as a compressed, URL-safe string. | ||
| * | ||
| * The JSON representation is deflate-compressed with {@link deflateSync} before | ||
| * base64url encoding, which typically reduces payload size by 70–80 % and | ||
| * keeps URLs well within browser and server path-length limits. | ||
| * | ||
| * Use {@link decodeAst} to reverse. | ||
| */ | ||
| function encodeAst(input) { | ||
| const compressed = (0, fflate.deflateSync)(new TextEncoder().encode(JSON.stringify(input))); | ||
| return Buffer.from(compressed).toString("base64url"); | ||
| } | ||
| /** | ||
| * Constructs the Kubb Studio URL for the given `InputNode`. | ||
| * When `options.ast` is `true`, navigates to the AST inspector (`/ast`). | ||
| * The `input` is encoded and attached as the `?root=` query parameter so Studio | ||
| * can decode and render it without a round-trip to any server. | ||
| */ | ||
| function getStudioUrl(input, studioUrl, options = {}) { | ||
| return `${studioUrl.replace(/\/$/, "")}${options.ast ? "/ast" : ""}?root=${encodeAst(input)}`; | ||
| } | ||
| /** | ||
| * Opens the Kubb Studio URL for the given `InputNode` in the default browser — | ||
| * | ||
| * Falls back to printing the URL if the browser cannot be launched. | ||
| */ | ||
| async function openInStudio(input, studioUrl, options = {}) { | ||
| const url = getStudioUrl(input, studioUrl, options); | ||
| const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open"; | ||
| const args = process.platform === "win32" ? [ | ||
| "/c", | ||
| "start", | ||
| "", | ||
| url | ||
| ] : [url]; | ||
| try { | ||
| await (0, tinyexec.x)(cmd, args); | ||
| } catch { | ||
| console.log(`\n ${url}\n`); | ||
| } | ||
| } | ||
| //#endregion | ||
| //#region src/FileManager.ts | ||
| function mergeFile(a, b) { | ||
| return { | ||
| ...a, | ||
| banner: b.banner, | ||
| footer: b.footer, | ||
| sources: [...a.sources || [], ...b.sources || []], | ||
| imports: [...a.imports || [], ...b.imports || []], | ||
| exports: [...a.exports || [], ...b.exports || []] | ||
| }; | ||
| } | ||
| /** | ||
| * Collapses a list of files so that duplicates sharing the same `path` are merged | ||
| * in arrival order. Keeps the original order of first occurrence. | ||
| */ | ||
| function mergeFilesByPath(files) { | ||
| const merged = /* @__PURE__ */ new Map(); | ||
| for (const file of files) { | ||
| const existing = merged.get(file.path); | ||
| merged.set(file.path, existing ? mergeFile(existing, file) : file); | ||
| } | ||
| return merged; | ||
| } | ||
| /** | ||
| * In-memory file store for generated files. | ||
| * | ||
| * Files with the same `path` are merged — sources, imports, and exports are concatenated. | ||
| * The `files` getter returns all stored files sorted by path length (shortest first). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { FileManager } from '@kubb/core' | ||
| * | ||
| * const manager = new FileManager() | ||
| * manager.upsert(myFile) | ||
| * console.log(manager.files) // all stored files | ||
| * ``` | ||
| */ | ||
| var FileManager = class { | ||
| #cache = /* @__PURE__ */ new Map(); | ||
| #filesCache = null; | ||
| /** | ||
| * Adds one or more files. Incoming files with the same path are merged | ||
| * (sources/imports/exports concatenated), but existing cache entries are | ||
| * replaced — use {@link upsert} when you want to merge into the cache too. | ||
| */ | ||
| add(...files) { | ||
| return this.#store(files, false); | ||
| } | ||
| /** | ||
| * Adds or merges one or more files. | ||
| * If a file with the same path already exists in the cache, its | ||
| * sources/imports/exports are merged into the incoming file. | ||
| */ | ||
| upsert(...files) { | ||
| return this.#store(files, true); | ||
| } | ||
| #store(files, mergeExisting) { | ||
| const resolvedFiles = []; | ||
| for (const file of mergeFilesByPath(files).values()) { | ||
| const existing = mergeExisting ? this.#cache.get(file.path) : void 0; | ||
| const resolvedFile = (0, _kubb_ast.createFile)(existing ? mergeFile(existing, file) : file); | ||
| this.#cache.set(resolvedFile.path, resolvedFile); | ||
| resolvedFiles.push(resolvedFile); | ||
| } | ||
| this.#filesCache = null; | ||
| return resolvedFiles; | ||
| } | ||
| getByPath(path) { | ||
| return this.#cache.get(path) ?? null; | ||
| } | ||
| deleteByPath(path) { | ||
| this.#cache.delete(path); | ||
| this.#filesCache = null; | ||
| } | ||
| clear() { | ||
| this.#cache.clear(); | ||
| this.#filesCache = null; | ||
| } | ||
| /** | ||
| * All stored files, sorted by path length (shorter paths first). | ||
| */ | ||
| get files() { | ||
| if (this.#filesCache) return this.#filesCache; | ||
| this.#filesCache = [...this.#cache.values()].sort((a, b) => { | ||
| const lenDiff = a.path.length - b.path.length; | ||
| if (lenDiff !== 0) return lenDiff; | ||
| const aIsIndex = a.path.endsWith("/index.ts") || a.path === "index.ts"; | ||
| const bIsIndex = b.path.endsWith("/index.ts") || b.path === "index.ts"; | ||
| if (aIsIndex && !bIsIndex) return 1; | ||
| if (!aIsIndex && bIsIndex) return -1; | ||
| return 0; | ||
| }); | ||
| return this.#filesCache; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/renderNode.ts | ||
| /** | ||
| * Handles the return value of a plugin AST hook or generator method. | ||
| * | ||
| * - Renderer output → rendered via the provided `rendererFactory` (e.g. JSX), files stored in `driver.fileManager` | ||
| * - `Array<FileNode>` → added directly into `driver.fileManager` | ||
| * - `void` / `null` / `undefined` → no-op (plugin handled it via `this.upsertFile`) | ||
| * | ||
| * Pass a `rendererFactory` (e.g. `jsxRenderer` from `@kubb/renderer-jsx`) when the result | ||
| * may be a renderer element. Generators that only return `Array<FileNode>` do not need one. | ||
| */ | ||
| async function applyHookResult(result, driver, rendererFactory) { | ||
| if (!result) return; | ||
| if (Array.isArray(result)) { | ||
| driver.fileManager.upsert(...result); | ||
| return; | ||
| } | ||
| if (!rendererFactory) return; | ||
| const renderer = rendererFactory(); | ||
| await renderer.render(result); | ||
| driver.fileManager.upsert(...renderer.files); | ||
| renderer.unmount(); | ||
| } | ||
| //#endregion | ||
| //#region src/PluginDriver.ts | ||
| function enforceOrder(enforce) { | ||
| return enforce === "pre" ? -1 : enforce === "post" ? 1 : 0; | ||
| } | ||
| var PluginDriver = class PluginDriver { | ||
| config; | ||
| options; | ||
| /** | ||
| * Returns `'single'` when `fileOrFolder` has a file extension, `'split'` otherwise. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * PluginDriver.getMode('src/gen/types.ts') // 'single' | ||
| * PluginDriver.getMode('src/gen/types') // 'split' | ||
| * ``` | ||
| */ | ||
| static getMode(fileOrFolder) { | ||
| if (!fileOrFolder) return "split"; | ||
| return (0, node_path.extname)(fileOrFolder) ? "single" : "split"; | ||
| } | ||
| /** | ||
| * The universal `@kubb/ast` `InputNode` produced by the adapter, set by | ||
| * the build pipeline after the adapter's `parse()` resolves. | ||
| */ | ||
| inputNode = void 0; | ||
| adapter = void 0; | ||
| #studioIsOpen = false; | ||
| /** | ||
| * Central file store for all generated files. | ||
| * Plugins should use `this.addFile()` / `this.upsertFile()` (via their context) to | ||
| * add files; this property gives direct read/write access when needed. | ||
| */ | ||
| fileManager = new FileManager(); | ||
| plugins = /* @__PURE__ */ new Map(); | ||
| /** | ||
| * Tracks which plugins have generators registered via `addGenerator()` (event-based path). | ||
| * Used by the build loop to decide whether to emit generator events for a given plugin. | ||
| */ | ||
| #pluginsWithEventGenerators = /* @__PURE__ */ new Set(); | ||
| #resolvers = /* @__PURE__ */ new Map(); | ||
| #defaultResolvers = /* @__PURE__ */ new Map(); | ||
| #hookListeners = /* @__PURE__ */ new Map(); | ||
| constructor(config, options) { | ||
| this.config = config; | ||
| this.options = options; | ||
| config.plugins.map((rawPlugin) => this.#normalizePlugin(rawPlugin)).filter((plugin) => { | ||
| if (typeof plugin.apply === "function") return plugin.apply(config); | ||
| return true; | ||
| }).sort((a, b) => { | ||
| if (b.dependencies?.includes(a.name)) return -1; | ||
| if (a.dependencies?.includes(b.name)) return 1; | ||
| return enforceOrder(a.enforce) - enforceOrder(b.enforce); | ||
| }).forEach((plugin) => { | ||
| this.plugins.set(plugin.name, plugin); | ||
| }); | ||
| } | ||
| get hooks() { | ||
| return this.options.hooks; | ||
| } | ||
| /** | ||
| * Creates an `NormalizedPlugin` from a hook-style plugin and registers | ||
| * its lifecycle handlers on the `AsyncEventEmitter`. | ||
| */ | ||
| #normalizePlugin(hookPlugin) { | ||
| const normalizedPlugin = { | ||
| name: hookPlugin.name, | ||
| dependencies: hookPlugin.dependencies, | ||
| enforce: hookPlugin.enforce, | ||
| options: { | ||
| output: { path: "." }, | ||
| exclude: [], | ||
| override: [] | ||
| } | ||
| }; | ||
| this.registerPluginHooks(hookPlugin, normalizedPlugin); | ||
| return normalizedPlugin; | ||
| } | ||
| /** | ||
| * Registers a hook-style plugin's lifecycle handlers on the shared `AsyncEventEmitter`. | ||
| * | ||
| * For `kubb:plugin:setup`, the registered listener wraps the globally emitted context with a | ||
| * plugin-specific one so that `addGenerator`, `setResolver`, `setTransformer`, and | ||
| * `setRenderer` all target the correct `normalizedPlugin` entry in the plugins map. | ||
| * | ||
| * All other hooks are iterated and registered directly as pass-through listeners. | ||
| * Any event key present in the global `KubbHooks` interface can be subscribed to. | ||
| * | ||
| * External tooling can subscribe to any of these events via `hooks.on(...)` to observe | ||
| * the plugin lifecycle without modifying plugin behavior. | ||
| * | ||
| * @internal | ||
| */ | ||
| registerPluginHooks(hookPlugin, normalizedPlugin) { | ||
| const { hooks } = hookPlugin; | ||
| if (hooks["kubb:plugin:setup"]) { | ||
| const setupHandler = (globalCtx) => { | ||
| const pluginCtx = { | ||
| ...globalCtx, | ||
| options: hookPlugin.options ?? {}, | ||
| addGenerator: (gen) => { | ||
| this.registerGenerator(normalizedPlugin.name, gen); | ||
| }, | ||
| setResolver: (resolver) => { | ||
| this.setPluginResolver(normalizedPlugin.name, resolver); | ||
| }, | ||
| setTransformer: (visitor) => { | ||
| normalizedPlugin.transformer = visitor; | ||
| }, | ||
| setRenderer: (renderer) => { | ||
| normalizedPlugin.renderer = renderer; | ||
| }, | ||
| setOptions: (opts) => { | ||
| normalizedPlugin.options = { | ||
| ...normalizedPlugin.options, | ||
| ...opts | ||
| }; | ||
| }, | ||
| injectFile: (userFileNode) => { | ||
| this.fileManager.add((0, _kubb_ast.createFile)(userFileNode)); | ||
| } | ||
| }; | ||
| return hooks["kubb:plugin:setup"](pluginCtx); | ||
| }; | ||
| this.hooks.on("kubb:plugin:setup", setupHandler); | ||
| this.#trackHookListener("kubb:plugin:setup", setupHandler); | ||
| } | ||
| for (const [event, handler] of Object.entries(hooks)) { | ||
| if (event === "kubb:plugin:setup" || !handler) continue; | ||
| this.hooks.on(event, handler); | ||
| this.#trackHookListener(event, handler); | ||
| } | ||
| } | ||
| /** | ||
| * Emits the `kubb:plugin:setup` event so that all registered hook-style plugin listeners | ||
| * can configure generators, resolvers, transformers and renderers before `buildStart` runs. | ||
| * | ||
| * Call this once from `safeBuild` before the plugin execution loop begins. | ||
| */ | ||
| async emitSetupHooks() { | ||
| const noop = () => {}; | ||
| await this.hooks.emit("kubb:plugin:setup", { | ||
| config: this.config, | ||
| options: {}, | ||
| addGenerator: noop, | ||
| setResolver: noop, | ||
| setTransformer: noop, | ||
| setRenderer: noop, | ||
| setOptions: noop, | ||
| injectFile: noop, | ||
| updateConfig: noop | ||
| }); | ||
| } | ||
| /** | ||
| * Registers a generator for the given plugin on the shared event emitter. | ||
| * | ||
| * The generator's `schema`, `operation`, and `operations` methods are registered as | ||
| * listeners on `kubb:generate:schema`, `kubb:generate:operation`, and `kubb:generate:operations` | ||
| * respectively. Each listener is scoped to the owning plugin via a `ctx.plugin.name` check | ||
| * so that generators from different plugins do not cross-fire. | ||
| * | ||
| * The renderer resolution chain is: `generator.renderer → plugin.renderer → config.renderer`. | ||
| * Set `generator.renderer = null` to explicitly opt out of rendering even when the plugin | ||
| * declares a renderer. | ||
| * | ||
| * Call this method inside `addGenerator()` (in `kubb:plugin:setup`) to wire up a generator. | ||
| */ | ||
| registerGenerator(pluginName, gen) { | ||
| const resolveRenderer = () => { | ||
| const plugin = this.plugins.get(pluginName); | ||
| return gen.renderer === null ? void 0 : gen.renderer ?? plugin?.renderer ?? this.config.renderer; | ||
| }; | ||
| if (gen.schema) { | ||
| const schemaHandler = async (node, ctx) => { | ||
| if (ctx.plugin.name !== pluginName) return; | ||
| await applyHookResult(await gen.schema(node, ctx), this, resolveRenderer()); | ||
| }; | ||
| this.hooks.on("kubb:generate:schema", schemaHandler); | ||
| this.#trackHookListener("kubb:generate:schema", schemaHandler); | ||
| } | ||
| if (gen.operation) { | ||
| const operationHandler = async (node, ctx) => { | ||
| if (ctx.plugin.name !== pluginName) return; | ||
| await applyHookResult(await gen.operation(node, ctx), this, resolveRenderer()); | ||
| }; | ||
| this.hooks.on("kubb:generate:operation", operationHandler); | ||
| this.#trackHookListener("kubb:generate:operation", operationHandler); | ||
| } | ||
| if (gen.operations) { | ||
| const operationsHandler = async (nodes, ctx) => { | ||
| if (ctx.plugin.name !== pluginName) return; | ||
| await applyHookResult(await gen.operations(nodes, ctx), this, resolveRenderer()); | ||
| }; | ||
| this.hooks.on("kubb:generate:operations", operationsHandler); | ||
| this.#trackHookListener("kubb:generate:operations", operationsHandler); | ||
| } | ||
| this.#pluginsWithEventGenerators.add(pluginName); | ||
| } | ||
| /** | ||
| * Returns `true` when at least one generator was registered for the given plugin | ||
| * via `addGenerator()` in `kubb:plugin:setup` (event-based path). | ||
| * | ||
| * Used by the build loop to decide whether to walk the AST and emit generator events | ||
| * for a plugin that has no static `plugin.generators`. | ||
| */ | ||
| hasRegisteredGenerators(pluginName) { | ||
| return this.#pluginsWithEventGenerators.has(pluginName); | ||
| } | ||
| /** | ||
| * Unregisters all plugin lifecycle listeners from the shared event emitter. | ||
| * Called at the end of a build to prevent listener leaks across repeated builds. | ||
| * | ||
| * @internal | ||
| */ | ||
| dispose() { | ||
| for (const [event, handlers] of this.#hookListeners) for (const handler of handlers) this.hooks.off(event, handler); | ||
| this.#hookListeners.clear(); | ||
| this.#pluginsWithEventGenerators.clear(); | ||
| } | ||
| #trackHookListener(event, handler) { | ||
| let handlers = this.#hookListeners.get(event); | ||
| if (!handlers) { | ||
| handlers = /* @__PURE__ */ new Set(); | ||
| this.#hookListeners.set(event, handlers); | ||
| } | ||
| handlers.add(handler); | ||
| } | ||
| #createDefaultResolver(pluginName) { | ||
| const existingResolver = this.#defaultResolvers.get(pluginName); | ||
| if (existingResolver) return existingResolver; | ||
| const resolver = defineResolver((_ctx) => ({ | ||
| name: "default", | ||
| pluginName | ||
| })); | ||
| this.#defaultResolvers.set(pluginName, resolver); | ||
| return resolver; | ||
| } | ||
| /** | ||
| * Merges `partial` with the plugin's default resolver and stores the result. | ||
| * Also mirrors it onto `plugin.resolver` so callers using `getPlugin(name).resolver` | ||
| * get the up-to-date resolver without going through `getResolver()`. | ||
| */ | ||
| setPluginResolver(pluginName, partial) { | ||
| const merged = { | ||
| ...this.#createDefaultResolver(pluginName), | ||
| ...partial | ||
| }; | ||
| this.#resolvers.set(pluginName, merged); | ||
| const plugin = this.plugins.get(pluginName); | ||
| if (plugin) plugin.resolver = merged; | ||
| } | ||
| getResolver(pluginName) { | ||
| return this.#resolvers.get(pluginName) ?? this.plugins.get(pluginName)?.resolver ?? this.#createDefaultResolver(pluginName); | ||
| } | ||
| getContext(plugin) { | ||
| const driver = this; | ||
| return { | ||
| config: driver.config, | ||
| get root() { | ||
| return (0, node_path.resolve)(driver.config.root, driver.config.output.path); | ||
| }, | ||
| getMode(output) { | ||
| return PluginDriver.getMode((0, node_path.resolve)(driver.config.root, driver.config.output.path, output.path)); | ||
| }, | ||
| hooks: driver.hooks, | ||
| plugin, | ||
| getPlugin: driver.getPlugin.bind(driver), | ||
| requirePlugin: driver.requirePlugin.bind(driver), | ||
| getResolver: driver.getResolver.bind(driver), | ||
| driver, | ||
| addFile: async (...files) => { | ||
| driver.fileManager.add(...files); | ||
| }, | ||
| upsertFile: async (...files) => { | ||
| driver.fileManager.upsert(...files); | ||
| }, | ||
| get inputNode() { | ||
| return driver.inputNode; | ||
| }, | ||
| get adapter() { | ||
| return driver.adapter; | ||
| }, | ||
| get resolver() { | ||
| return driver.getResolver(plugin.name); | ||
| }, | ||
| get transformer() { | ||
| return plugin.transformer; | ||
| }, | ||
| warn(message) { | ||
| driver.hooks.emit("kubb:warn", { message }); | ||
| }, | ||
| error(error) { | ||
| driver.hooks.emit("kubb:error", { error: typeof error === "string" ? new Error(error) : error }); | ||
| }, | ||
| info(message) { | ||
| driver.hooks.emit("kubb:info", { message }); | ||
| }, | ||
| openInStudio(options) { | ||
| if (!driver.config.devtools || driver.#studioIsOpen) return; | ||
| if (typeof driver.config.devtools !== "object") throw new Error("Devtools must be an object"); | ||
| if (!driver.inputNode || !driver.adapter) throw new Error("adapter is not defined, make sure you have set the parser in kubb.config.ts"); | ||
| driver.#studioIsOpen = true; | ||
| const studioUrl = driver.config.devtools?.studioUrl ?? "https://studio.kubb.dev"; | ||
| return openInStudio(driver.inputNode, studioUrl, options); | ||
| } | ||
| }; | ||
| } | ||
| getPlugin(pluginName) { | ||
| return this.plugins.get(pluginName); | ||
| } | ||
| requirePlugin(pluginName) { | ||
| const plugin = this.plugins.get(pluginName); | ||
| if (!plugin) throw new Error(`[kubb] Plugin "${pluginName}" is required but not found. Make sure it is included in your Kubb config.`); | ||
| return plugin; | ||
| } | ||
| }; | ||
| //#endregion | ||
| Object.defineProperty(exports, "DEFAULT_BANNER", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return DEFAULT_BANNER; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "DEFAULT_EXTENSION", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return DEFAULT_EXTENSION; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "DEFAULT_STUDIO_URL", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return DEFAULT_STUDIO_URL; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "FileManager", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return FileManager; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "PluginDriver", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return PluginDriver; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "__name", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return __name; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "__toESM", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return __toESM; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "applyHookResult", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return applyHookResult; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "camelCase", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return camelCase; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "defineResolver", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return defineResolver; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "logLevel", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return logLevel; | ||
| } | ||
| }); | ||
| //# sourceMappingURL=PluginDriver-BXibeQk-.cjs.map |
| {"version":3,"file":"PluginDriver-BXibeQk-.cjs","names":["path","#cache","#store","#filesCache","#pluginsWithEventGenerators","#resolvers","#defaultResolvers","#hookListeners","#normalizePlugin","#trackHookListener","#createDefaultResolver","#studioIsOpen","openInStudioFn"],"sources":["../../../internals/utils/src/casing.ts","../src/constants.ts","../src/defineResolver.ts","../src/devtools.ts","../src/FileManager.ts","../src/renderNode.ts","../src/PluginDriver.ts"],"sourcesContent":["type Options = {\n /**\n * When `true`, dot-separated segments are split on `.` and joined with `/` after casing.\n */\n isFile?: boolean\n /**\n * Text prepended before casing is applied.\n */\n prefix?: string\n /**\n * Text appended before casing is applied.\n */\n suffix?: string\n}\n\n/**\n * Shared implementation for camelCase and PascalCase conversion.\n * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)\n * and capitalizes each word according to `pascal`.\n *\n * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.\n */\nfunction toCamelOrPascal(text: string, pascal: boolean): string {\n const normalized = text\n .trim()\n .replace(/([a-z\\d])([A-Z])/g, '$1 $2')\n .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')\n .replace(/(\\d)([a-z])/g, '$1 $2')\n\n const words = normalized.split(/[\\s\\-_./\\\\:]+/).filter(Boolean)\n\n return words\n .map((word, i) => {\n const allUpper = word.length > 1 && word === word.toUpperCase()\n if (allUpper) return word\n if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1)\n return word.charAt(0).toUpperCase() + word.slice(1)\n })\n .join('')\n .replace(/[^a-zA-Z0-9]/g, '')\n}\n\n/**\n * Splits `text` on `.` and applies `transformPart` to each segment.\n * The last segment receives `isLast = true`, all earlier segments receive `false`.\n * Segments are joined with `/` to form a file path.\n *\n * Only splits on dots followed by a letter so that version numbers\n * embedded in operationIds (e.g. `v2025.0`) are kept intact.\n *\n * Empty segments are filtered before joining. They arise when the text starts with\n * a dot followed immediately by a letter (e.g. `..Schema` splits into `['..', 'Schema']`\n * and `'..'` transforms to an empty string). Without this filter the join would produce\n * a leading `/`, which `path.resolve` would interpret as an absolute path, allowing\n * generated files to escape the configured output directory.\n */\nfunction applyToFileParts(text: string, transformPart: (part: string, isLast: boolean) => string): string {\n const parts = text.split(/\\.(?=[a-zA-Z])/)\n return parts\n .map((part, i) => transformPart(part, i === parts.length - 1))\n .filter(Boolean)\n .join('/')\n}\n\n/**\n * Converts `text` to camelCase.\n * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.\n *\n * @example\n * camelCase('hello-world') // 'helloWorld'\n * camelCase('pet.petId', { isFile: true }) // 'pet/petId'\n */\nexport function camelCase(text: string, { isFile, prefix = '', suffix = '' }: Options = {}): string {\n if (isFile) {\n return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? { prefix, suffix } : {}))\n }\n\n return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false)\n}\n\n/**\n * Converts `text` to PascalCase.\n * When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased.\n *\n * @example\n * pascalCase('hello-world') // 'HelloWorld'\n * pascalCase('pet.petId', { isFile: true }) // 'pet/PetId'\n */\nexport function pascalCase(text: string, { isFile, prefix = '', suffix = '' }: Options = {}): string {\n if (isFile) {\n return applyToFileParts(text, (part, isLast) => (isLast ? pascalCase(part, { prefix, suffix }) : camelCase(part)))\n }\n\n return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true)\n}\n","import type { FileNode } from '@kubb/ast'\n\n/**\n * Base URL for the Kubb Studio web app.\n */\nexport const DEFAULT_STUDIO_URL = 'https://studio.kubb.dev' as const\n\n/**\n * Maximum number of files processed in parallel by FileProcessor.\n */\nexport const PARALLEL_CONCURRENCY_LIMIT = 100\n\n/**\n * Default banner style written at the top of every generated file.\n */\nexport const DEFAULT_BANNER = 'simple' as const\n\n/**\n * Default file-extension mapping used when no explicit mapping is configured.\n */\nexport const DEFAULT_EXTENSION: Record<FileNode['extname'], FileNode['extname'] | ''> = { '.ts': '.ts' }\n\n/**\n * Numeric log-level thresholds used internally to compare verbosity.\n *\n * Higher numbers are more verbose.\n */\nexport const logLevel = {\n silent: Number.NEGATIVE_INFINITY,\n error: 0,\n warn: 1,\n info: 3,\n verbose: 4,\n debug: 5,\n} as const\n","import path from 'node:path'\nimport { camelCase, pascalCase } from '@internals/utils'\nimport type { FileNode, InputNode, Node, OperationNode, SchemaNode } from '@kubb/ast'\nimport { createFile, isOperationNode, isSchemaNode } from '@kubb/ast'\nimport { PluginDriver } from './PluginDriver.ts'\nimport type {\n Config,\n PluginFactoryOptions,\n ResolveBannerContext,\n ResolveOptionsContext,\n Resolver,\n ResolverContext,\n ResolverFileParams,\n ResolverPathParams,\n} from './types.ts'\n\n/**\n * Builder type for the plugin-specific resolver fields.\n *\n * `default`, `resolveOptions`, `resolvePath`, `resolveFile`, `resolveBanner`, and `resolveFooter`\n * are optional — built-in fallbacks are injected when omitted.\n *\n * The builder receives `ctx` — a reference to the fully assembled resolver — so methods can\n * call sibling resolver methods without using `this`. Because `ctx` is captured by the closure\n * and the resolver is populated after the builder runs, `ctx` correctly reflects any overrides\n * that were applied by the builder itself.\n */\ntype ResolverBuilder<T extends PluginFactoryOptions> = (ctx: T['resolver']) => Omit<\n T['resolver'],\n 'default' | 'resolveOptions' | 'resolvePath' | 'resolveFile' | 'resolveBanner' | 'resolveFooter' | 'name' | 'pluginName'\n> &\n Partial<Pick<T['resolver'], 'default' | 'resolveOptions' | 'resolvePath' | 'resolveFile' | 'resolveBanner' | 'resolveFooter'>> & {\n name: string\n pluginName: T['name']\n }\n\n// String patterns are compiled lazily and cached — the same filter is reused for every node.\nconst stringPatternCache = new Map<string, RegExp>()\n\nfunction testPattern(value: string, pattern: string | RegExp): boolean {\n if (typeof pattern === 'string') {\n let regex = stringPatternCache.get(pattern)\n if (!regex) {\n regex = new RegExp(pattern)\n stringPatternCache.set(pattern, regex)\n }\n return regex.test(value)\n }\n // Use .match() for user-supplied RegExp to preserve semantics regardless of `g`/`y` flags.\n return value.match(pattern) !== null\n}\n\n/**\n * Checks if an operation matches a pattern for a given filter type (`tag`, `operationId`, `path`, `method`).\n */\nfunction matchesOperationPattern(node: OperationNode, type: string, pattern: string | RegExp): boolean {\n switch (type) {\n case 'tag':\n return node.tags.some((tag) => testPattern(tag, pattern))\n case 'operationId':\n return testPattern(node.operationId, pattern)\n case 'path':\n return testPattern(node.path, pattern)\n case 'method':\n return testPattern(node.method.toLowerCase(), pattern)\n case 'contentType':\n return node.requestBody?.content?.some((c) => testPattern(c.contentType, pattern)) ?? false\n default:\n return false\n }\n}\n\n/**\n * Checks if a schema matches a pattern for a given filter type (`schemaName`).\n *\n * Returns `null` when the filter type doesn't apply to schemas.\n */\nfunction matchesSchemaPattern(node: SchemaNode, type: string, pattern: string | RegExp): boolean | null {\n switch (type) {\n case 'schemaName':\n return node.name ? testPattern(node.name, pattern) : false\n default:\n return null\n }\n}\n\n/**\n * Default name resolver used by `defineResolver`.\n *\n * - `camelCase` for `function` and `file` types.\n * - `PascalCase` for `type`.\n * - `camelCase` for everything else.\n */\nfunction defaultResolver(name: string, type?: 'file' | 'function' | 'type' | 'const'): string {\n let resolvedName = camelCase(name)\n\n if (type === 'file' || type === 'function') {\n resolvedName = camelCase(name, {\n isFile: type === 'file',\n })\n }\n\n if (type === 'type') {\n resolvedName = pascalCase(name)\n }\n\n return resolvedName\n}\n\n/**\n * Default option resolver — applies include/exclude filters and merges matching override options.\n *\n * Returns `null` when the node is filtered out by an `exclude` rule or not matched by any `include` rule.\n *\n * @example Include/exclude filtering\n * ```ts\n * const options = defaultResolveOptions(operationNode, {\n * options: { output: 'types' },\n * exclude: [{ type: 'tag', pattern: 'internal' }],\n * })\n * // → null when node has tag 'internal'\n * ```\n *\n * @example Override merging\n * ```ts\n * const options = defaultResolveOptions(operationNode, {\n * options: { enumType: 'asConst' },\n * override: [{ type: 'operationId', pattern: 'listPets', options: { enumType: 'enum' } }],\n * })\n * // → { enumType: 'enum' } when operationId matches\n * ```\n */\nexport function defaultResolveOptions<TOptions>(\n node: Node,\n { options, exclude = [], include, override = [] }: ResolveOptionsContext<TOptions>,\n): TOptions | null {\n if (isOperationNode(node)) {\n const isExcluded = exclude.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))\n if (isExcluded) {\n return null\n }\n\n if (include && !include.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) {\n return null\n }\n\n const overrideOptions = override.find(({ type, pattern }) => matchesOperationPattern(node, type, pattern))?.options\n\n return { ...options, ...overrideOptions }\n }\n\n if (isSchemaNode(node)) {\n if (exclude.some(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)) {\n return null\n }\n\n if (include) {\n const results = include.map(({ type, pattern }) => matchesSchemaPattern(node, type, pattern))\n const applicable = results.filter((r) => r !== null)\n if (applicable.length > 0 && !applicable.includes(true)) {\n return null\n }\n }\n\n const overrideOptions = override.find(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)?.options\n\n return { ...options, ...overrideOptions }\n }\n\n return options\n}\n\n/**\n * Default path resolver used by `defineResolver`.\n *\n * - Returns the output directory in `single` mode.\n * - Resolves into a tag- or path-based subdirectory when `group` and a `tag`/`path` value are provided.\n * - Falls back to a flat `output/baseName` path otherwise.\n *\n * A custom `group.name` function overrides the default subdirectory naming.\n * For `tag` groups the default is `${camelCase(tag)}Controller`.\n * For `path` groups the default is the first path segment after `/`.\n *\n * @example Flat output\n * ```ts\n * defaultResolvePath({ baseName: 'petTypes.ts' }, { root: '/src', output: { path: 'types' } })\n * // → '/src/types/petTypes.ts'\n * ```\n *\n * @example Tag-based grouping\n * ```ts\n * defaultResolvePath(\n * { baseName: 'petTypes.ts', tag: 'pets' },\n * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },\n * )\n * // → '/src/types/petsController/petTypes.ts'\n * ```\n *\n * @example Path-based grouping\n * ```ts\n * defaultResolvePath(\n * { baseName: 'petTypes.ts', path: '/pets/list' },\n * { root: '/src', output: { path: 'types' }, group: { type: 'path' } },\n * )\n * // → '/src/types/pets/petTypes.ts'\n * ```\n *\n * @example Single-file mode\n * ```ts\n * defaultResolvePath(\n * { baseName: 'petTypes.ts', pathMode: 'single' },\n * { root: '/src', output: { path: 'types' } },\n * )\n * // → '/src/types'\n * ```\n */\nexport function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }: ResolverPathParams, { root, output, group }: ResolverContext): string {\n const mode = pathMode ?? PluginDriver.getMode(path.resolve(root, output.path))\n\n if (mode === 'single') {\n return path.resolve(root, output.path)\n }\n\n let result: string\n\n if (group && (groupPath || tag)) {\n const groupValue = group.type === 'path' ? groupPath! : tag!\n const defaultName =\n group.type === 'tag'\n ? ({ group: g }: { group: string }) => `${camelCase(g)}Controller`\n : ({ group: g }: { group: string }) => {\n // Strip traversal components (empty, '.', '..') before taking the first meaningful segment.\n // When every segment is a traversal component (e.g. '../../') we fall back to '' so the\n // file is placed directly in the output root — the boundary check below ensures safety.\n const segment = g.split('/').filter((s) => s !== '' && s !== '.' && s !== '..')[0]\n return segment ? camelCase(segment) : ''\n }\n const resolveName = group.name ?? defaultName\n result = path.resolve(root, output.path, resolveName({ group: groupValue }), baseName)\n } else {\n result = path.resolve(root, output.path, baseName)\n }\n\n // Ensure the resolved path stays within the configured output directory.\n // This prevents path traversal from malicious OpenAPI specs or custom group.name functions.\n // `result === outputDir` is intentionally permitted: it matches single-file mode paths and\n // edge cases where baseName resolves to the output directory itself.\n const outputDir = path.resolve(root, output.path)\n const outputDirWithSep = outputDir.endsWith(path.sep) ? outputDir : `${outputDir}${path.sep}`\n if (result !== outputDir && !result.startsWith(outputDirWithSep)) {\n throw new Error(\n `[Kubb] Resolved path \"${result}\" is outside the output directory \"${outputDir}\". ` +\n 'This may indicate a path traversal attempt in the OpenAPI specification or a misconfigured group.name function.',\n )\n }\n\n return result\n}\n\n/**\n * Default file resolver used by `defineResolver`.\n *\n * Resolves a `FileNode` by combining name resolution (`resolver.default`) with\n * path resolution (`resolver.resolvePath`). The resolved file always has empty\n * `sources`, `imports`, and `exports` arrays — consumers populate those separately.\n *\n * In `single` mode the name is omitted and the file sits directly in the output directory.\n *\n * @example Resolve a schema file\n * ```ts\n * const file = defaultResolveFile(\n * { name: 'pet', extname: '.ts' },\n * { root: '/src', output: { path: 'types' } },\n * resolver,\n * )\n * // → { baseName: 'pet.ts', path: '/src/types/pet.ts', sources: [], ... }\n * ```\n *\n * @example Resolve an operation file with tag grouping\n * ```ts\n * const file = defaultResolveFile(\n * { name: 'listPets', extname: '.ts', tag: 'pets' },\n * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },\n * resolver,\n * )\n * // → { baseName: 'listPets.ts', path: '/src/types/petsController/listPets.ts', ... }\n * ```\n */\nexport function defaultResolveFile({ name, extname, tag, path: groupPath }: ResolverFileParams, context: ResolverContext, ctx: Resolver): FileNode {\n const pathMode = PluginDriver.getMode(path.resolve(context.root, context.output.path))\n const resolvedName = pathMode === 'single' ? '' : ctx.default(name, 'file')\n const baseName = `${resolvedName}${extname}` as FileNode['baseName']\n const filePath = ctx.resolvePath({ baseName, pathMode, tag, path: groupPath }, context)\n\n return createFile({\n path: filePath,\n baseName: path.basename(filePath) as `${string}.${string}`,\n meta: {\n pluginName: ctx.pluginName,\n },\n sources: [],\n imports: [],\n exports: [],\n })\n}\n\n/**\n * Generates the default \"Generated by Kubb\" banner from config and optional node metadata.\n */\nexport function buildDefaultBanner({\n title,\n description,\n version,\n config,\n}: {\n title?: string\n description?: string\n version?: string\n config: Config\n}): string {\n try {\n let source = ''\n if (Array.isArray(config.input)) {\n const first = config.input[0]\n if (first && 'path' in first) {\n source = path.basename(first.path)\n }\n } else if ('path' in config.input) {\n source = path.basename(config.input.path)\n } else if ('data' in config.input) {\n source = 'text content'\n }\n\n let banner = '/**\\n* Generated by Kubb (https://kubb.dev/).\\n* Do not edit manually.\\n'\n\n if (config.output.defaultBanner === 'simple') {\n banner += '*/\\n'\n return banner\n }\n\n if (source) {\n banner += `* Source: ${source}\\n`\n }\n\n if (title) {\n banner += `* Title: ${title}\\n`\n }\n\n if (description) {\n const formattedDescription = description.replace(/\\n/gm, '\\n* ')\n banner += `* Description: ${formattedDescription}\\n`\n }\n\n if (version) {\n banner += `* OpenAPI spec version: ${version}\\n`\n }\n\n banner += '*/\\n'\n return banner\n } catch (_error) {\n return '/**\\n* Generated by Kubb (https://kubb.dev/).\\n* Do not edit manually.\\n*/'\n }\n}\n\n/**\n * Default banner resolver — returns the banner string for a generated file.\n *\n * A user-supplied `output.banner` overrides the default Kubb \"Generated by Kubb\" notice.\n * When no `output.banner` is set, the Kubb notice is used (including `title` and `version`\n * from the OAS spec when a `node` is provided).\n *\n * - When `output.banner` is a function and `node` is provided, returns `output.banner(node)`.\n * - When `output.banner` is a function and `node` is absent, falls back to the Kubb notice.\n * - When `output.banner` is a string, returns it directly.\n * - When `config.output.defaultBanner` is `false`, returns `undefined`.\n * - Otherwise returns the Kubb \"Generated by Kubb\" notice.\n *\n * @example String banner overrides default\n * ```ts\n * defaultResolveBanner(undefined, { output: { banner: '// my banner' }, config })\n * // → '// my banner'\n * ```\n *\n * @example Function banner with node\n * ```ts\n * defaultResolveBanner(inputNode, { output: { banner: (node) => `// v${node.version}` }, config })\n * // → '// v3.0.0'\n * ```\n *\n * @example No user banner — Kubb notice with OAS metadata\n * ```ts\n * defaultResolveBanner(inputNode, { config })\n * // → '/** Generated by Kubb ... Title: Pet Store ... *\\/'\n * ```\n *\n * @example Disabled default banner\n * ```ts\n * defaultResolveBanner(undefined, { config: { output: { defaultBanner: false }, ...config } })\n * // → undefined\n * ```\n */\nexport function defaultResolveBanner(node: InputNode | undefined, { output, config }: ResolveBannerContext): string | undefined {\n if (typeof output?.banner === 'function') {\n return output.banner(node)\n }\n\n if (typeof output?.banner === 'string') {\n return output.banner\n }\n\n if (config.output.defaultBanner === false) {\n return undefined\n }\n\n return buildDefaultBanner({\n title: node?.meta?.title,\n version: node?.meta?.version,\n config,\n })\n}\n\n/**\n * Default footer resolver — returns the footer string for a generated file.\n *\n * - When `output.footer` is a function and `node` is provided, calls it with the node.\n * - When `output.footer` is a function and `node` is absent, returns `undefined`.\n * - When `output.footer` is a string, returns it directly.\n * - Otherwise returns `undefined`.\n *\n * @example String footer\n * ```ts\n * defaultResolveFooter(undefined, { output: { footer: '// end of file' }, config })\n * // → '// end of file'\n * ```\n *\n * @example Function footer with node\n * ```ts\n * defaultResolveFooter(inputNode, { output: { footer: (node) => `// ${node.title}` }, config })\n * // → '// Pet Store'\n * ```\n */\nexport function defaultResolveFooter(node: InputNode | undefined, { output }: ResolveBannerContext): string | undefined {\n if (typeof output?.footer === 'function') {\n return node ? output.footer(node) : undefined\n }\n if (typeof output?.footer === 'string') {\n return output.footer\n }\n return undefined\n}\n\n/**\n * Defines a resolver for a plugin, injecting built-in defaults for name casing,\n * include/exclude/override filtering, path resolution, and file construction.\n *\n * All four defaults can be overridden by providing them in the builder function:\n * - `default` — name casing strategy (camelCase / PascalCase)\n * - `resolveOptions` — include/exclude/override filtering\n * - `resolvePath` — output path computation\n * - `resolveFile` — full `FileNode` construction\n *\n * The builder receives `ctx` — a reference to the assembled resolver — so methods can\n * call sibling resolver methods using `ctx` instead of `this`.\n *\n * @example Basic resolver with naming helpers\n * ```ts\n * export const resolver = defineResolver<PluginTs>((ctx) => ({\n * name: 'default',\n * resolveName(node) {\n * return ctx.default(node.name, 'function')\n * },\n * resolveTypedName(node) {\n * return ctx.default(node.name, 'type')\n * },\n * }))\n * ```\n *\n * @example Override resolvePath for a custom output structure\n * ```ts\n * export const resolver = defineResolver<PluginTs>((_ctx) => ({\n * name: 'custom',\n * resolvePath({ baseName }, { root, output }) {\n * return path.resolve(root, output.path, 'generated', baseName)\n * },\n * }))\n * ```\n *\n * @example Use ctx.default inside a helper\n * ```ts\n * export const resolver = defineResolver<PluginTs>((ctx) => ({\n * name: 'default',\n * resolveParamName(node, param) {\n * return ctx.default(`${node.operationId} ${param.in} ${param.name}`, 'type')\n * },\n * }))\n * ```\n */\nexport function defineResolver<T extends PluginFactoryOptions>(build: ResolverBuilder<T>): T['resolver'] {\n // Create the resolver shell first. When `build(resolver)` executes below, `resolver` is\n // still empty, but methods returned by the builder capture it by reference. By the time\n // those methods are actually called, `Object.assign` will have already populated all\n // properties (including any overrides from the builder itself).\n const resolver = {} as T['resolver']\n\n Object.assign(resolver, {\n default: defaultResolver,\n resolveOptions: defaultResolveOptions,\n resolvePath: defaultResolvePath,\n // Wire the default resolveFile implementation with a wrapper that passes resolver as ctx.\n // Unlike other defaults which can be assigned directly, defaultResolveFile requires the\n // resolver as its third parameter.\n resolveFile: (params: ResolverFileParams, context: ResolverContext) => defaultResolveFile(params, context, resolver as Resolver),\n resolveBanner: defaultResolveBanner,\n resolveFooter: defaultResolveFooter,\n // Builder overrides are applied last. Any method in the builder can call\n // ctx.xxx() and will see the fully merged resolver (including its own overrides).\n ...build(resolver),\n })\n\n return resolver\n}\n","import type { InputNode } from '@kubb/ast'\nimport { deflateSync, inflateSync } from 'fflate'\nimport { x } from 'tinyexec'\nimport type { DevtoolsOptions } from './types.ts'\n\n/**\n * Encodes an `InputNode` as a compressed, URL-safe string.\n *\n * The JSON representation is deflate-compressed with {@link deflateSync} before\n * base64url encoding, which typically reduces payload size by 70–80 % and\n * keeps URLs well within browser and server path-length limits.\n *\n * Use {@link decodeAst} to reverse.\n */\nexport function encodeAst(input: InputNode): string {\n const compressed = deflateSync(new TextEncoder().encode(JSON.stringify(input)))\n return Buffer.from(compressed).toString('base64url')\n}\n\n/**\n * Decodes an `InputNode` from a string produced by {@link encodeAst}.\n *\n * Works in both Node.js and the browser — no streaming APIs required.\n */\nexport function decodeAst(encoded: string): InputNode {\n const bytes = Buffer.from(encoded, 'base64url')\n return JSON.parse(new TextDecoder().decode(inflateSync(bytes))) as InputNode\n}\n\n/**\n * Constructs the Kubb Studio URL for the given `InputNode`.\n * When `options.ast` is `true`, navigates to the AST inspector (`/ast`).\n * The `input` is encoded and attached as the `?root=` query parameter so Studio\n * can decode and render it without a round-trip to any server.\n */\nexport function getStudioUrl(input: InputNode, studioUrl: string, options: DevtoolsOptions = {}): string {\n const baseUrl = studioUrl.replace(/\\/$/, '')\n const path = options.ast ? '/ast' : ''\n\n return `${baseUrl}${path}?root=${encodeAst(input)}`\n}\n\n/**\n * Opens the Kubb Studio URL for the given `InputNode` in the default browser —\n *\n * Falls back to printing the URL if the browser cannot be launched.\n */\nexport async function openInStudio(input: InputNode, studioUrl: string, options: DevtoolsOptions = {}): Promise<void> {\n const url = getStudioUrl(input, studioUrl, options)\n\n const cmd = process.platform === 'win32' ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open'\n const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url]\n\n try {\n await x(cmd, args)\n } catch {\n console.log(`\\n ${url}\\n`)\n }\n}\n","import type { FileNode } from '@kubb/ast'\nimport { createFile } from '@kubb/ast'\n\nfunction mergeFile<TMeta extends object = object>(a: FileNode<TMeta>, b: FileNode<TMeta>): FileNode<TMeta> {\n return {\n ...a,\n // Incoming file (b) takes precedence for banner/footer so that barrel files,\n // which never carry a banner, can clear banners set by plugin-generated files\n // at the same path.\n banner: b.banner,\n footer: b.footer,\n sources: [...(a.sources || []), ...(b.sources || [])],\n imports: [...(a.imports || []), ...(b.imports || [])],\n exports: [...(a.exports || []), ...(b.exports || [])],\n }\n}\n\n/**\n * Collapses a list of files so that duplicates sharing the same `path` are merged\n * in arrival order. Keeps the original order of first occurrence.\n */\nfunction mergeFilesByPath(files: ReadonlyArray<FileNode>): Map<string, FileNode> {\n const merged = new Map<string, FileNode>()\n for (const file of files) {\n const existing = merged.get(file.path)\n merged.set(file.path, existing ? mergeFile(existing, file) : file)\n }\n return merged\n}\n\n/**\n * In-memory file store for generated files.\n *\n * Files with the same `path` are merged — sources, imports, and exports are concatenated.\n * The `files` getter returns all stored files sorted by path length (shortest first).\n *\n * @example\n * ```ts\n * import { FileManager } from '@kubb/core'\n *\n * const manager = new FileManager()\n * manager.upsert(myFile)\n * console.log(manager.files) // all stored files\n * ```\n */\nexport class FileManager {\n readonly #cache = new Map<string, FileNode>()\n #filesCache: Array<FileNode> | null = null\n\n /**\n * Adds one or more files. Incoming files with the same path are merged\n * (sources/imports/exports concatenated), but existing cache entries are\n * replaced — use {@link upsert} when you want to merge into the cache too.\n */\n add(...files: Array<FileNode>): Array<FileNode> {\n return this.#store(files, false)\n }\n\n /**\n * Adds or merges one or more files.\n * If a file with the same path already exists in the cache, its\n * sources/imports/exports are merged into the incoming file.\n */\n upsert(...files: Array<FileNode>): Array<FileNode> {\n return this.#store(files, true)\n }\n\n #store(files: ReadonlyArray<FileNode>, mergeExisting: boolean): Array<FileNode> {\n const resolvedFiles: Array<FileNode> = []\n for (const file of mergeFilesByPath(files).values()) {\n const existing = mergeExisting ? this.#cache.get(file.path) : undefined\n const resolvedFile = createFile(existing ? mergeFile(existing, file) : file)\n this.#cache.set(resolvedFile.path, resolvedFile)\n resolvedFiles.push(resolvedFile)\n }\n this.#filesCache = null\n return resolvedFiles\n }\n\n getByPath(path: string): FileNode | null {\n return this.#cache.get(path) ?? null\n }\n\n deleteByPath(path: string): void {\n this.#cache.delete(path)\n this.#filesCache = null\n }\n\n clear(): void {\n this.#cache.clear()\n this.#filesCache = null\n }\n\n /**\n * All stored files, sorted by path length (shorter paths first).\n */\n get files(): Array<FileNode> {\n if (this.#filesCache) {\n return this.#filesCache\n }\n\n this.#filesCache = [...this.#cache.values()].sort((a, b) => {\n const lenDiff = a.path.length - b.path.length\n if (lenDiff !== 0) return lenDiff\n // Within the same length bucket, index.ts barrel files go last so other\n // files are always processed before their barrel file.\n const aIsIndex = a.path.endsWith('/index.ts') || a.path === 'index.ts'\n const bIsIndex = b.path.endsWith('/index.ts') || b.path === 'index.ts'\n if (aIsIndex && !bIsIndex) return 1\n if (!aIsIndex && bIsIndex) return -1\n return 0\n })\n return this.#filesCache\n }\n}\n","import type { FileNode } from '@kubb/ast'\nimport type { RendererFactory } from './createRenderer.ts'\nimport type { PluginDriver } from './PluginDriver.ts'\n\n/**\n * Handles the return value of a plugin AST hook or generator method.\n *\n * - Renderer output → rendered via the provided `rendererFactory` (e.g. JSX), files stored in `driver.fileManager`\n * - `Array<FileNode>` → added directly into `driver.fileManager`\n * - `void` / `null` / `undefined` → no-op (plugin handled it via `this.upsertFile`)\n *\n * Pass a `rendererFactory` (e.g. `jsxRenderer` from `@kubb/renderer-jsx`) when the result\n * may be a renderer element. Generators that only return `Array<FileNode>` do not need one.\n */\nexport async function applyHookResult<TElement = unknown>(\n result: TElement | Array<FileNode> | void,\n driver: PluginDriver,\n rendererFactory?: RendererFactory<TElement>,\n): Promise<void> {\n if (!result) return\n\n if (Array.isArray(result)) {\n driver.fileManager.upsert(...(result as Array<FileNode>))\n return\n }\n\n if (!rendererFactory) {\n return\n }\n\n const renderer = rendererFactory()\n await renderer.render(result)\n driver.fileManager.upsert(...renderer.files)\n renderer.unmount()\n}\n","import { extname, resolve } from 'node:path'\nimport type { AsyncEventEmitter } from '@internals/utils'\nimport type { FileNode, InputNode, OperationNode, SchemaNode } from '@kubb/ast'\nimport { createFile } from '@kubb/ast'\nimport { DEFAULT_STUDIO_URL } from './constants.ts'\nimport type { Generator } from './defineGenerator.ts'\nimport type { Plugin } from './definePlugin.ts'\nimport { defineResolver } from './defineResolver.ts'\nimport { openInStudio as openInStudioFn } from './devtools.ts'\nimport { FileManager } from './FileManager.ts'\nimport { applyHookResult } from './renderNode.ts'\n\nimport type {\n Adapter,\n Config,\n DevtoolsOptions,\n GeneratorContext,\n KubbHooks,\n KubbPluginSetupContext,\n NormalizedPlugin,\n PluginFactoryOptions,\n Resolver,\n} from './types.ts'\n\n// inspired by: https://github.com/rollup/rollup/blob/master/src/utils/PluginDriver.ts#\n\ntype Options = {\n hooks: AsyncEventEmitter<KubbHooks>\n}\n\nfunction enforceOrder(enforce: 'pre' | 'post' | undefined): number {\n return enforce === 'pre' ? -1 : enforce === 'post' ? 1 : 0\n}\n\nexport class PluginDriver {\n readonly config: Config\n readonly options: Options\n\n /**\n * Returns `'single'` when `fileOrFolder` has a file extension, `'split'` otherwise.\n *\n * @example\n * ```ts\n * PluginDriver.getMode('src/gen/types.ts') // 'single'\n * PluginDriver.getMode('src/gen/types') // 'split'\n * ```\n */\n static getMode(fileOrFolder: string | undefined | null): 'single' | 'split' {\n if (!fileOrFolder) {\n return 'split'\n }\n return extname(fileOrFolder) ? 'single' : 'split'\n }\n\n /**\n * The universal `@kubb/ast` `InputNode` produced by the adapter, set by\n * the build pipeline after the adapter's `parse()` resolves.\n */\n inputNode: InputNode | undefined = undefined\n adapter: Adapter | undefined = undefined\n #studioIsOpen = false\n\n /**\n * Central file store for all generated files.\n * Plugins should use `this.addFile()` / `this.upsertFile()` (via their context) to\n * add files; this property gives direct read/write access when needed.\n */\n readonly fileManager = new FileManager()\n\n readonly plugins = new Map<string, NormalizedPlugin>()\n\n /**\n * Tracks which plugins have generators registered via `addGenerator()` (event-based path).\n * Used by the build loop to decide whether to emit generator events for a given plugin.\n */\n readonly #pluginsWithEventGenerators = new Set<string>()\n readonly #resolvers = new Map<string, Resolver>()\n readonly #defaultResolvers = new Map<string, Resolver>()\n readonly #hookListeners = new Map<keyof KubbHooks, Set<(...args: never[]) => void | Promise<void>>>()\n\n constructor(config: Config, options: Options) {\n this.config = config\n this.options = options\n config.plugins\n .map((rawPlugin) => this.#normalizePlugin(rawPlugin as Plugin))\n .filter((plugin) => {\n if (typeof plugin.apply === 'function') {\n return plugin.apply(config)\n }\n return true\n })\n .sort((a, b) => {\n if (b.dependencies?.includes(a.name)) return -1\n if (a.dependencies?.includes(b.name)) return 1\n // enforce: 'pre' plugins run first, 'post' plugins run last\n return enforceOrder(a.enforce) - enforceOrder(b.enforce)\n })\n .forEach((plugin) => {\n this.plugins.set(plugin.name, plugin)\n })\n }\n\n get hooks() {\n return this.options.hooks\n }\n\n /**\n * Creates an `NormalizedPlugin` from a hook-style plugin and registers\n * its lifecycle handlers on the `AsyncEventEmitter`.\n */\n #normalizePlugin(hookPlugin: Plugin): NormalizedPlugin {\n const normalizedPlugin = {\n name: hookPlugin.name,\n dependencies: hookPlugin.dependencies,\n enforce: hookPlugin.enforce,\n options: { output: { path: '.' }, exclude: [], override: [] },\n } as unknown as NormalizedPlugin\n\n this.registerPluginHooks(hookPlugin, normalizedPlugin)\n return normalizedPlugin\n }\n\n /**\n * Registers a hook-style plugin's lifecycle handlers on the shared `AsyncEventEmitter`.\n *\n * For `kubb:plugin:setup`, the registered listener wraps the globally emitted context with a\n * plugin-specific one so that `addGenerator`, `setResolver`, `setTransformer`, and\n * `setRenderer` all target the correct `normalizedPlugin` entry in the plugins map.\n *\n * All other hooks are iterated and registered directly as pass-through listeners.\n * Any event key present in the global `KubbHooks` interface can be subscribed to.\n *\n * External tooling can subscribe to any of these events via `hooks.on(...)` to observe\n * the plugin lifecycle without modifying plugin behavior.\n *\n * @internal\n */\n registerPluginHooks(hookPlugin: Plugin, normalizedPlugin: NormalizedPlugin): void {\n const { hooks } = hookPlugin\n\n // kubb:plugin:setup gets special treatment: the globally emitted context is wrapped with\n // plugin-specific implementations so that addGenerator / setResolver / etc. target\n // this plugin's normalizedPlugin entry rather than being no-ops.\n if (hooks['kubb:plugin:setup']) {\n const setupHandler = (globalCtx: KubbPluginSetupContext) => {\n const pluginCtx: KubbPluginSetupContext = {\n ...globalCtx,\n options: hookPlugin.options ?? {},\n addGenerator: (gen) => {\n this.registerGenerator(normalizedPlugin.name, gen)\n },\n setResolver: (resolver) => {\n this.setPluginResolver(normalizedPlugin.name, resolver)\n },\n setTransformer: (visitor) => {\n normalizedPlugin.transformer = visitor\n },\n setRenderer: (renderer) => {\n normalizedPlugin.renderer = renderer\n },\n setOptions: (opts) => {\n normalizedPlugin.options = { ...normalizedPlugin.options, ...opts }\n },\n injectFile: (userFileNode) => {\n this.fileManager.add(createFile(userFileNode))\n },\n }\n return hooks['kubb:plugin:setup']!(pluginCtx)\n }\n\n this.hooks.on('kubb:plugin:setup', setupHandler)\n this.#trackHookListener('kubb:plugin:setup', setupHandler as (...args: never[]) => void | Promise<void>)\n }\n\n // All other hooks are registered as direct pass-through listeners on the shared emitter.\n for (const [event, handler] of Object.entries(hooks) as Array<[keyof KubbHooks, ((...args: never[]) => void | Promise<void>) | undefined]>) {\n if (event === 'kubb:plugin:setup' || !handler) continue\n\n this.hooks.on(event, handler as never)\n this.#trackHookListener(event, handler as (...args: never[]) => void | Promise<void>)\n }\n }\n\n /**\n * Emits the `kubb:plugin:setup` event so that all registered hook-style plugin listeners\n * can configure generators, resolvers, transformers and renderers before `buildStart` runs.\n *\n * Call this once from `safeBuild` before the plugin execution loop begins.\n */\n async emitSetupHooks(): Promise<void> {\n const noop = () => {}\n await this.hooks.emit('kubb:plugin:setup', {\n config: this.config,\n options: {},\n addGenerator: noop,\n setResolver: noop,\n setTransformer: noop,\n setRenderer: noop,\n setOptions: noop,\n injectFile: noop,\n updateConfig: noop,\n })\n }\n\n /**\n * Registers a generator for the given plugin on the shared event emitter.\n *\n * The generator's `schema`, `operation`, and `operations` methods are registered as\n * listeners on `kubb:generate:schema`, `kubb:generate:operation`, and `kubb:generate:operations`\n * respectively. Each listener is scoped to the owning plugin via a `ctx.plugin.name` check\n * so that generators from different plugins do not cross-fire.\n *\n * The renderer resolution chain is: `generator.renderer → plugin.renderer → config.renderer`.\n * Set `generator.renderer = null` to explicitly opt out of rendering even when the plugin\n * declares a renderer.\n *\n * Call this method inside `addGenerator()` (in `kubb:plugin:setup`) to wire up a generator.\n */\n registerGenerator(pluginName: string, gen: Generator): void {\n const resolveRenderer = () => {\n const plugin = this.plugins.get(pluginName)\n return gen.renderer === null ? undefined : (gen.renderer ?? plugin?.renderer ?? this.config.renderer)\n }\n\n if (gen.schema) {\n const schemaHandler = async (node: SchemaNode, ctx: GeneratorContext) => {\n if (ctx.plugin.name !== pluginName) return\n const result = await gen.schema!(node, ctx)\n await applyHookResult(result, this, resolveRenderer())\n }\n\n this.hooks.on('kubb:generate:schema', schemaHandler)\n this.#trackHookListener('kubb:generate:schema', schemaHandler as (...args: never[]) => void | Promise<void>)\n }\n\n if (gen.operation) {\n const operationHandler = async (node: OperationNode, ctx: GeneratorContext) => {\n if (ctx.plugin.name !== pluginName) return\n const result = await gen.operation!(node, ctx)\n await applyHookResult(result, this, resolveRenderer())\n }\n\n this.hooks.on('kubb:generate:operation', operationHandler)\n this.#trackHookListener('kubb:generate:operation', operationHandler as (...args: never[]) => void | Promise<void>)\n }\n\n if (gen.operations) {\n const operationsHandler = async (nodes: Array<OperationNode>, ctx: GeneratorContext) => {\n if (ctx.plugin.name !== pluginName) return\n const result = await gen.operations!(nodes, ctx)\n await applyHookResult(result, this, resolveRenderer())\n }\n\n this.hooks.on('kubb:generate:operations', operationsHandler)\n this.#trackHookListener('kubb:generate:operations', operationsHandler as (...args: never[]) => void | Promise<void>)\n }\n\n this.#pluginsWithEventGenerators.add(pluginName)\n }\n\n /**\n * Returns `true` when at least one generator was registered for the given plugin\n * via `addGenerator()` in `kubb:plugin:setup` (event-based path).\n *\n * Used by the build loop to decide whether to walk the AST and emit generator events\n * for a plugin that has no static `plugin.generators`.\n */\n hasRegisteredGenerators(pluginName: string): boolean {\n return this.#pluginsWithEventGenerators.has(pluginName)\n }\n\n /**\n * Unregisters all plugin lifecycle listeners from the shared event emitter.\n * Called at the end of a build to prevent listener leaks across repeated builds.\n *\n * @internal\n */\n dispose(): void {\n for (const [event, handlers] of this.#hookListeners) {\n for (const handler of handlers) {\n this.hooks.off(event, handler as never)\n }\n }\n this.#hookListeners.clear()\n this.#pluginsWithEventGenerators.clear()\n }\n\n #trackHookListener(event: keyof KubbHooks, handler: (...args: never[]) => void | Promise<void>): void {\n let handlers = this.#hookListeners.get(event)\n if (!handlers) {\n handlers = new Set()\n this.#hookListeners.set(event, handlers)\n }\n handlers.add(handler)\n }\n\n #createDefaultResolver(pluginName: string): Resolver {\n const existingResolver = this.#defaultResolvers.get(pluginName)\n if (existingResolver) {\n return existingResolver\n }\n\n const resolver = defineResolver<PluginFactoryOptions>((_ctx) => ({\n name: 'default',\n pluginName,\n }))\n this.#defaultResolvers.set(pluginName, resolver)\n return resolver\n }\n\n /**\n * Merges `partial` with the plugin's default resolver and stores the result.\n * Also mirrors it onto `plugin.resolver` so callers using `getPlugin(name).resolver`\n * get the up-to-date resolver without going through `getResolver()`.\n */\n setPluginResolver(pluginName: string, partial: Partial<Resolver>): void {\n const defaultResolver = this.#createDefaultResolver(pluginName)\n const merged = { ...defaultResolver, ...partial }\n this.#resolvers.set(pluginName, merged)\n const plugin = this.plugins.get(pluginName)\n if (plugin) {\n plugin.resolver = merged\n }\n }\n\n /**\n * Returns the resolver for the given plugin.\n *\n * Resolution order: dynamic resolver set via `setPluginResolver` → static resolver on the\n * plugin → lazily created default resolver (identity name, no path transforms).\n */\n getResolver<TName extends keyof Kubb.PluginRegistry>(pluginName: TName): Kubb.PluginRegistry[TName]['resolver']\n getResolver<TResolver extends Resolver = Resolver>(pluginName: string): TResolver\n getResolver(pluginName: string): Resolver {\n return this.#resolvers.get(pluginName) ?? this.plugins.get(pluginName)?.resolver ?? this.#createDefaultResolver(pluginName)\n }\n\n getContext<TOptions extends PluginFactoryOptions>(plugin: NormalizedPlugin<TOptions>): GeneratorContext<TOptions> & Record<string, unknown> {\n const driver = this\n\n const baseContext = {\n config: driver.config,\n get root(): string {\n return resolve(driver.config.root, driver.config.output.path)\n },\n getMode(output: { path: string }): 'single' | 'split' {\n return PluginDriver.getMode(resolve(driver.config.root, driver.config.output.path, output.path))\n },\n hooks: driver.hooks,\n plugin,\n getPlugin: driver.getPlugin.bind(driver),\n requirePlugin: driver.requirePlugin.bind(driver),\n getResolver: driver.getResolver.bind(driver),\n driver,\n addFile: async (...files: Array<FileNode>) => {\n driver.fileManager.add(...files)\n },\n upsertFile: async (...files: Array<FileNode>) => {\n driver.fileManager.upsert(...files)\n },\n get inputNode(): InputNode | undefined {\n return driver.inputNode\n },\n get adapter(): Adapter | undefined {\n return driver.adapter\n },\n get resolver() {\n return driver.getResolver(plugin.name)\n },\n get transformer() {\n return plugin.transformer\n },\n warn(message: string) {\n driver.hooks.emit('kubb:warn', { message })\n },\n error(error: string | Error) {\n driver.hooks.emit('kubb:error', { error: typeof error === 'string' ? new Error(error) : error })\n },\n info(message: string) {\n driver.hooks.emit('kubb:info', { message })\n },\n openInStudio(options?: DevtoolsOptions) {\n if (!driver.config.devtools || driver.#studioIsOpen) {\n return\n }\n\n if (typeof driver.config.devtools !== 'object') {\n throw new Error('Devtools must be an object')\n }\n\n if (!driver.inputNode || !driver.adapter) {\n throw new Error('adapter is not defined, make sure you have set the parser in kubb.config.ts')\n }\n\n driver.#studioIsOpen = true\n\n const studioUrl = driver.config.devtools?.studioUrl ?? DEFAULT_STUDIO_URL\n\n return openInStudioFn(driver.inputNode, studioUrl, options)\n },\n } as unknown as GeneratorContext<TOptions>\n\n return baseContext\n }\n\n getPlugin<TName extends keyof Kubb.PluginRegistry>(pluginName: TName): Plugin<Kubb.PluginRegistry[TName]> | undefined\n getPlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions>(pluginName: string): Plugin<TOptions> | undefined\n getPlugin(pluginName: string): Plugin | undefined {\n return this.plugins.get(pluginName)\n }\n\n /**\n * Like `getPlugin` but throws a descriptive error when the plugin is not found.\n */\n requirePlugin<TName extends keyof Kubb.PluginRegistry>(pluginName: TName): Plugin<Kubb.PluginRegistry[TName]>\n requirePlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions>(pluginName: string): Plugin<TOptions>\n requirePlugin(pluginName: string): Plugin {\n const plugin = this.plugins.get(pluginName)\n if (!plugin) {\n throw new Error(`[kubb] Plugin \"${pluginName}\" is required but not found. Make sure it is included in your Kubb config.`)\n }\n return plugin\n }\n}\n"],"mappings":"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"} |
| import "./chunk--u3MIqq1.js"; | ||
| import path, { extname, resolve } from "node:path"; | ||
| import { createFile, isOperationNode, isSchemaNode } from "@kubb/ast"; | ||
| import { deflateSync } from "fflate"; | ||
| import { x } from "tinyexec"; | ||
| //#region ../../internals/utils/src/casing.ts | ||
| /** | ||
| * Shared implementation for camelCase and PascalCase conversion. | ||
| * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons) | ||
| * and capitalizes each word according to `pascal`. | ||
| * | ||
| * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are. | ||
| */ | ||
| function toCamelOrPascal(text, pascal) { | ||
| 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) => { | ||
| if (word.length > 1 && word === word.toUpperCase()) return word; | ||
| if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1); | ||
| return word.charAt(0).toUpperCase() + word.slice(1); | ||
| }).join("").replace(/[^a-zA-Z0-9]/g, ""); | ||
| } | ||
| /** | ||
| * Splits `text` on `.` and applies `transformPart` to each segment. | ||
| * The last segment receives `isLast = true`, all earlier segments receive `false`. | ||
| * Segments are joined with `/` to form a file path. | ||
| * | ||
| * Only splits on dots followed by a letter so that version numbers | ||
| * embedded in operationIds (e.g. `v2025.0`) are kept intact. | ||
| * | ||
| * Empty segments are filtered before joining. They arise when the text starts with | ||
| * a dot followed immediately by a letter (e.g. `..Schema` splits into `['..', 'Schema']` | ||
| * and `'..'` transforms to an empty string). Without this filter the join would produce | ||
| * a leading `/`, which `path.resolve` would interpret as an absolute path, allowing | ||
| * generated files to escape the configured output directory. | ||
| */ | ||
| function applyToFileParts(text, transformPart) { | ||
| const parts = text.split(/\.(?=[a-zA-Z])/); | ||
| return parts.map((part, i) => transformPart(part, i === parts.length - 1)).filter(Boolean).join("/"); | ||
| } | ||
| /** | ||
| * Converts `text` to camelCase. | ||
| * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`. | ||
| * | ||
| * @example | ||
| * camelCase('hello-world') // 'helloWorld' | ||
| * camelCase('pet.petId', { isFile: true }) // 'pet/petId' | ||
| */ | ||
| function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) { | ||
| if (isFile) return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? { | ||
| prefix, | ||
| suffix | ||
| } : {})); | ||
| return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false); | ||
| } | ||
| /** | ||
| * Converts `text` to PascalCase. | ||
| * When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased. | ||
| * | ||
| * @example | ||
| * pascalCase('hello-world') // 'HelloWorld' | ||
| * pascalCase('pet.petId', { isFile: true }) // 'pet/PetId' | ||
| */ | ||
| function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) { | ||
| if (isFile) return applyToFileParts(text, (part, isLast) => isLast ? pascalCase(part, { | ||
| prefix, | ||
| suffix | ||
| }) : camelCase(part)); | ||
| return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true); | ||
| } | ||
| //#endregion | ||
| //#region src/constants.ts | ||
| /** | ||
| * Base URL for the Kubb Studio web app. | ||
| */ | ||
| const DEFAULT_STUDIO_URL = "https://studio.kubb.dev"; | ||
| /** | ||
| * Default banner style written at the top of every generated file. | ||
| */ | ||
| const DEFAULT_BANNER = "simple"; | ||
| /** | ||
| * Default file-extension mapping used when no explicit mapping is configured. | ||
| */ | ||
| const DEFAULT_EXTENSION = { ".ts": ".ts" }; | ||
| /** | ||
| * Numeric log-level thresholds used internally to compare verbosity. | ||
| * | ||
| * Higher numbers are more verbose. | ||
| */ | ||
| const logLevel = { | ||
| silent: Number.NEGATIVE_INFINITY, | ||
| error: 0, | ||
| warn: 1, | ||
| info: 3, | ||
| verbose: 4, | ||
| debug: 5 | ||
| }; | ||
| //#endregion | ||
| //#region src/defineResolver.ts | ||
| const stringPatternCache = /* @__PURE__ */ new Map(); | ||
| function testPattern(value, pattern) { | ||
| if (typeof pattern === "string") { | ||
| let regex = stringPatternCache.get(pattern); | ||
| if (!regex) { | ||
| regex = new RegExp(pattern); | ||
| stringPatternCache.set(pattern, regex); | ||
| } | ||
| return regex.test(value); | ||
| } | ||
| return value.match(pattern) !== null; | ||
| } | ||
| /** | ||
| * Checks if an operation matches a pattern for a given filter type (`tag`, `operationId`, `path`, `method`). | ||
| */ | ||
| function matchesOperationPattern(node, type, pattern) { | ||
| switch (type) { | ||
| case "tag": return node.tags.some((tag) => testPattern(tag, pattern)); | ||
| case "operationId": return testPattern(node.operationId, pattern); | ||
| case "path": return testPattern(node.path, pattern); | ||
| case "method": return testPattern(node.method.toLowerCase(), pattern); | ||
| case "contentType": return node.requestBody?.content?.some((c) => testPattern(c.contentType, pattern)) ?? false; | ||
| default: return false; | ||
| } | ||
| } | ||
| /** | ||
| * Checks if a schema matches a pattern for a given filter type (`schemaName`). | ||
| * | ||
| * Returns `null` when the filter type doesn't apply to schemas. | ||
| */ | ||
| function matchesSchemaPattern(node, type, pattern) { | ||
| switch (type) { | ||
| case "schemaName": return node.name ? testPattern(node.name, pattern) : false; | ||
| default: return null; | ||
| } | ||
| } | ||
| /** | ||
| * Default name resolver used by `defineResolver`. | ||
| * | ||
| * - `camelCase` for `function` and `file` types. | ||
| * - `PascalCase` for `type`. | ||
| * - `camelCase` for everything else. | ||
| */ | ||
| function defaultResolver(name, type) { | ||
| let resolvedName = camelCase(name); | ||
| if (type === "file" || type === "function") resolvedName = camelCase(name, { isFile: type === "file" }); | ||
| if (type === "type") resolvedName = pascalCase(name); | ||
| return resolvedName; | ||
| } | ||
| /** | ||
| * Default option resolver — applies include/exclude filters and merges matching override options. | ||
| * | ||
| * Returns `null` when the node is filtered out by an `exclude` rule or not matched by any `include` rule. | ||
| * | ||
| * @example Include/exclude filtering | ||
| * ```ts | ||
| * const options = defaultResolveOptions(operationNode, { | ||
| * options: { output: 'types' }, | ||
| * exclude: [{ type: 'tag', pattern: 'internal' }], | ||
| * }) | ||
| * // → null when node has tag 'internal' | ||
| * ``` | ||
| * | ||
| * @example Override merging | ||
| * ```ts | ||
| * const options = defaultResolveOptions(operationNode, { | ||
| * options: { enumType: 'asConst' }, | ||
| * override: [{ type: 'operationId', pattern: 'listPets', options: { enumType: 'enum' } }], | ||
| * }) | ||
| * // → { enumType: 'enum' } when operationId matches | ||
| * ``` | ||
| */ | ||
| function defaultResolveOptions(node, { options, exclude = [], include, override = [] }) { | ||
| if (isOperationNode(node)) { | ||
| if (exclude.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null; | ||
| if (include && !include.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null; | ||
| const overrideOptions = override.find(({ type, pattern }) => matchesOperationPattern(node, type, pattern))?.options; | ||
| return { | ||
| ...options, | ||
| ...overrideOptions | ||
| }; | ||
| } | ||
| if (isSchemaNode(node)) { | ||
| if (exclude.some(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)) return null; | ||
| if (include) { | ||
| const applicable = include.map(({ type, pattern }) => matchesSchemaPattern(node, type, pattern)).filter((r) => r !== null); | ||
| if (applicable.length > 0 && !applicable.includes(true)) return null; | ||
| } | ||
| const overrideOptions = override.find(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)?.options; | ||
| return { | ||
| ...options, | ||
| ...overrideOptions | ||
| }; | ||
| } | ||
| return options; | ||
| } | ||
| /** | ||
| * Default path resolver used by `defineResolver`. | ||
| * | ||
| * - Returns the output directory in `single` mode. | ||
| * - Resolves into a tag- or path-based subdirectory when `group` and a `tag`/`path` value are provided. | ||
| * - Falls back to a flat `output/baseName` path otherwise. | ||
| * | ||
| * A custom `group.name` function overrides the default subdirectory naming. | ||
| * For `tag` groups the default is `${camelCase(tag)}Controller`. | ||
| * For `path` groups the default is the first path segment after `/`. | ||
| * | ||
| * @example Flat output | ||
| * ```ts | ||
| * defaultResolvePath({ baseName: 'petTypes.ts' }, { root: '/src', output: { path: 'types' } }) | ||
| * // → '/src/types/petTypes.ts' | ||
| * ``` | ||
| * | ||
| * @example Tag-based grouping | ||
| * ```ts | ||
| * defaultResolvePath( | ||
| * { baseName: 'petTypes.ts', tag: 'pets' }, | ||
| * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } }, | ||
| * ) | ||
| * // → '/src/types/petsController/petTypes.ts' | ||
| * ``` | ||
| * | ||
| * @example Path-based grouping | ||
| * ```ts | ||
| * defaultResolvePath( | ||
| * { baseName: 'petTypes.ts', path: '/pets/list' }, | ||
| * { root: '/src', output: { path: 'types' }, group: { type: 'path' } }, | ||
| * ) | ||
| * // → '/src/types/pets/petTypes.ts' | ||
| * ``` | ||
| * | ||
| * @example Single-file mode | ||
| * ```ts | ||
| * defaultResolvePath( | ||
| * { baseName: 'petTypes.ts', pathMode: 'single' }, | ||
| * { root: '/src', output: { path: 'types' } }, | ||
| * ) | ||
| * // → '/src/types' | ||
| * ``` | ||
| */ | ||
| function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }, { root, output, group }) { | ||
| if ((pathMode ?? PluginDriver.getMode(path.resolve(root, output.path))) === "single") return path.resolve(root, output.path); | ||
| let result; | ||
| if (group && (groupPath || tag)) { | ||
| const groupValue = group.type === "path" ? groupPath : tag; | ||
| const defaultName = group.type === "tag" ? ({ group: g }) => `${camelCase(g)}Controller` : ({ group: g }) => { | ||
| const segment = g.split("/").filter((s) => s !== "" && s !== "." && s !== "..")[0]; | ||
| return segment ? camelCase(segment) : ""; | ||
| }; | ||
| const resolveName = group.name ?? defaultName; | ||
| result = path.resolve(root, output.path, resolveName({ group: groupValue }), baseName); | ||
| } else result = path.resolve(root, output.path, baseName); | ||
| const outputDir = path.resolve(root, output.path); | ||
| const outputDirWithSep = outputDir.endsWith(path.sep) ? outputDir : `${outputDir}${path.sep}`; | ||
| if (result !== outputDir && !result.startsWith(outputDirWithSep)) throw new Error(`[Kubb] Resolved path "${result}" is outside the output directory "${outputDir}". This may indicate a path traversal attempt in the OpenAPI specification or a misconfigured group.name function.`); | ||
| return result; | ||
| } | ||
| /** | ||
| * Default file resolver used by `defineResolver`. | ||
| * | ||
| * Resolves a `FileNode` by combining name resolution (`resolver.default`) with | ||
| * path resolution (`resolver.resolvePath`). The resolved file always has empty | ||
| * `sources`, `imports`, and `exports` arrays — consumers populate those separately. | ||
| * | ||
| * In `single` mode the name is omitted and the file sits directly in the output directory. | ||
| * | ||
| * @example Resolve a schema file | ||
| * ```ts | ||
| * const file = defaultResolveFile( | ||
| * { name: 'pet', extname: '.ts' }, | ||
| * { root: '/src', output: { path: 'types' } }, | ||
| * resolver, | ||
| * ) | ||
| * // → { baseName: 'pet.ts', path: '/src/types/pet.ts', sources: [], ... } | ||
| * ``` | ||
| * | ||
| * @example Resolve an operation file with tag grouping | ||
| * ```ts | ||
| * const file = defaultResolveFile( | ||
| * { name: 'listPets', extname: '.ts', tag: 'pets' }, | ||
| * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } }, | ||
| * resolver, | ||
| * ) | ||
| * // → { baseName: 'listPets.ts', path: '/src/types/petsController/listPets.ts', ... } | ||
| * ``` | ||
| */ | ||
| function defaultResolveFile({ name, extname, tag, path: groupPath }, context, ctx) { | ||
| const pathMode = PluginDriver.getMode(path.resolve(context.root, context.output.path)); | ||
| const baseName = `${pathMode === "single" ? "" : ctx.default(name, "file")}${extname}`; | ||
| const filePath = ctx.resolvePath({ | ||
| baseName, | ||
| pathMode, | ||
| tag, | ||
| path: groupPath | ||
| }, context); | ||
| return createFile({ | ||
| path: filePath, | ||
| baseName: path.basename(filePath), | ||
| meta: { pluginName: ctx.pluginName }, | ||
| sources: [], | ||
| imports: [], | ||
| exports: [] | ||
| }); | ||
| } | ||
| /** | ||
| * Generates the default "Generated by Kubb" banner from config and optional node metadata. | ||
| */ | ||
| function buildDefaultBanner({ title, description, version, config }) { | ||
| try { | ||
| let source = ""; | ||
| if (Array.isArray(config.input)) { | ||
| const first = config.input[0]; | ||
| if (first && "path" in first) source = path.basename(first.path); | ||
| } else if ("path" in config.input) source = path.basename(config.input.path); | ||
| else if ("data" in config.input) source = "text content"; | ||
| let banner = "/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n"; | ||
| if (config.output.defaultBanner === "simple") { | ||
| banner += "*/\n"; | ||
| return banner; | ||
| } | ||
| if (source) banner += `* Source: ${source}\n`; | ||
| if (title) banner += `* Title: ${title}\n`; | ||
| if (description) { | ||
| const formattedDescription = description.replace(/\n/gm, "\n* "); | ||
| banner += `* Description: ${formattedDescription}\n`; | ||
| } | ||
| if (version) banner += `* OpenAPI spec version: ${version}\n`; | ||
| banner += "*/\n"; | ||
| return banner; | ||
| } catch (_error) { | ||
| return "/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n*/"; | ||
| } | ||
| } | ||
| /** | ||
| * Default banner resolver — returns the banner string for a generated file. | ||
| * | ||
| * A user-supplied `output.banner` overrides the default Kubb "Generated by Kubb" notice. | ||
| * When no `output.banner` is set, the Kubb notice is used (including `title` and `version` | ||
| * from the OAS spec when a `node` is provided). | ||
| * | ||
| * - When `output.banner` is a function and `node` is provided, returns `output.banner(node)`. | ||
| * - When `output.banner` is a function and `node` is absent, falls back to the Kubb notice. | ||
| * - When `output.banner` is a string, returns it directly. | ||
| * - When `config.output.defaultBanner` is `false`, returns `undefined`. | ||
| * - Otherwise returns the Kubb "Generated by Kubb" notice. | ||
| * | ||
| * @example String banner overrides default | ||
| * ```ts | ||
| * defaultResolveBanner(undefined, { output: { banner: '// my banner' }, config }) | ||
| * // → '// my banner' | ||
| * ``` | ||
| * | ||
| * @example Function banner with node | ||
| * ```ts | ||
| * defaultResolveBanner(inputNode, { output: { banner: (node) => `// v${node.version}` }, config }) | ||
| * // → '// v3.0.0' | ||
| * ``` | ||
| * | ||
| * @example No user banner — Kubb notice with OAS metadata | ||
| * ```ts | ||
| * defaultResolveBanner(inputNode, { config }) | ||
| * // → '/** Generated by Kubb ... Title: Pet Store ... *\/' | ||
| * ``` | ||
| * | ||
| * @example Disabled default banner | ||
| * ```ts | ||
| * defaultResolveBanner(undefined, { config: { output: { defaultBanner: false }, ...config } }) | ||
| * // → undefined | ||
| * ``` | ||
| */ | ||
| function defaultResolveBanner(node, { output, config }) { | ||
| if (typeof output?.banner === "function") return output.banner(node); | ||
| if (typeof output?.banner === "string") return output.banner; | ||
| if (config.output.defaultBanner === false) return; | ||
| return buildDefaultBanner({ | ||
| title: node?.meta?.title, | ||
| version: node?.meta?.version, | ||
| config | ||
| }); | ||
| } | ||
| /** | ||
| * Default footer resolver — returns the footer string for a generated file. | ||
| * | ||
| * - When `output.footer` is a function and `node` is provided, calls it with the node. | ||
| * - When `output.footer` is a function and `node` is absent, returns `undefined`. | ||
| * - When `output.footer` is a string, returns it directly. | ||
| * - Otherwise returns `undefined`. | ||
| * | ||
| * @example String footer | ||
| * ```ts | ||
| * defaultResolveFooter(undefined, { output: { footer: '// end of file' }, config }) | ||
| * // → '// end of file' | ||
| * ``` | ||
| * | ||
| * @example Function footer with node | ||
| * ```ts | ||
| * defaultResolveFooter(inputNode, { output: { footer: (node) => `// ${node.title}` }, config }) | ||
| * // → '// Pet Store' | ||
| * ``` | ||
| */ | ||
| function defaultResolveFooter(node, { output }) { | ||
| if (typeof output?.footer === "function") return node ? output.footer(node) : void 0; | ||
| if (typeof output?.footer === "string") return output.footer; | ||
| } | ||
| /** | ||
| * Defines a resolver for a plugin, injecting built-in defaults for name casing, | ||
| * include/exclude/override filtering, path resolution, and file construction. | ||
| * | ||
| * All four defaults can be overridden by providing them in the builder function: | ||
| * - `default` — name casing strategy (camelCase / PascalCase) | ||
| * - `resolveOptions` — include/exclude/override filtering | ||
| * - `resolvePath` — output path computation | ||
| * - `resolveFile` — full `FileNode` construction | ||
| * | ||
| * The builder receives `ctx` — a reference to the assembled resolver — so methods can | ||
| * call sibling resolver methods using `ctx` instead of `this`. | ||
| * | ||
| * @example Basic resolver with naming helpers | ||
| * ```ts | ||
| * export const resolver = defineResolver<PluginTs>((ctx) => ({ | ||
| * name: 'default', | ||
| * resolveName(node) { | ||
| * return ctx.default(node.name, 'function') | ||
| * }, | ||
| * resolveTypedName(node) { | ||
| * return ctx.default(node.name, 'type') | ||
| * }, | ||
| * })) | ||
| * ``` | ||
| * | ||
| * @example Override resolvePath for a custom output structure | ||
| * ```ts | ||
| * export const resolver = defineResolver<PluginTs>((_ctx) => ({ | ||
| * name: 'custom', | ||
| * resolvePath({ baseName }, { root, output }) { | ||
| * return path.resolve(root, output.path, 'generated', baseName) | ||
| * }, | ||
| * })) | ||
| * ``` | ||
| * | ||
| * @example Use ctx.default inside a helper | ||
| * ```ts | ||
| * export const resolver = defineResolver<PluginTs>((ctx) => ({ | ||
| * name: 'default', | ||
| * resolveParamName(node, param) { | ||
| * return ctx.default(`${node.operationId} ${param.in} ${param.name}`, 'type') | ||
| * }, | ||
| * })) | ||
| * ``` | ||
| */ | ||
| function defineResolver(build) { | ||
| const resolver = {}; | ||
| Object.assign(resolver, { | ||
| default: defaultResolver, | ||
| resolveOptions: defaultResolveOptions, | ||
| resolvePath: defaultResolvePath, | ||
| resolveFile: (params, context) => defaultResolveFile(params, context, resolver), | ||
| resolveBanner: defaultResolveBanner, | ||
| resolveFooter: defaultResolveFooter, | ||
| ...build(resolver) | ||
| }); | ||
| return resolver; | ||
| } | ||
| //#endregion | ||
| //#region src/devtools.ts | ||
| /** | ||
| * Encodes an `InputNode` as a compressed, URL-safe string. | ||
| * | ||
| * The JSON representation is deflate-compressed with {@link deflateSync} before | ||
| * base64url encoding, which typically reduces payload size by 70–80 % and | ||
| * keeps URLs well within browser and server path-length limits. | ||
| * | ||
| * Use {@link decodeAst} to reverse. | ||
| */ | ||
| function encodeAst(input) { | ||
| const compressed = deflateSync(new TextEncoder().encode(JSON.stringify(input))); | ||
| return Buffer.from(compressed).toString("base64url"); | ||
| } | ||
| /** | ||
| * Constructs the Kubb Studio URL for the given `InputNode`. | ||
| * When `options.ast` is `true`, navigates to the AST inspector (`/ast`). | ||
| * The `input` is encoded and attached as the `?root=` query parameter so Studio | ||
| * can decode and render it without a round-trip to any server. | ||
| */ | ||
| function getStudioUrl(input, studioUrl, options = {}) { | ||
| return `${studioUrl.replace(/\/$/, "")}${options.ast ? "/ast" : ""}?root=${encodeAst(input)}`; | ||
| } | ||
| /** | ||
| * Opens the Kubb Studio URL for the given `InputNode` in the default browser — | ||
| * | ||
| * Falls back to printing the URL if the browser cannot be launched. | ||
| */ | ||
| async function openInStudio(input, studioUrl, options = {}) { | ||
| const url = getStudioUrl(input, studioUrl, options); | ||
| const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open"; | ||
| const args = process.platform === "win32" ? [ | ||
| "/c", | ||
| "start", | ||
| "", | ||
| url | ||
| ] : [url]; | ||
| try { | ||
| await x(cmd, args); | ||
| } catch { | ||
| console.log(`\n ${url}\n`); | ||
| } | ||
| } | ||
| //#endregion | ||
| //#region src/FileManager.ts | ||
| function mergeFile(a, b) { | ||
| return { | ||
| ...a, | ||
| banner: b.banner, | ||
| footer: b.footer, | ||
| sources: [...a.sources || [], ...b.sources || []], | ||
| imports: [...a.imports || [], ...b.imports || []], | ||
| exports: [...a.exports || [], ...b.exports || []] | ||
| }; | ||
| } | ||
| /** | ||
| * Collapses a list of files so that duplicates sharing the same `path` are merged | ||
| * in arrival order. Keeps the original order of first occurrence. | ||
| */ | ||
| function mergeFilesByPath(files) { | ||
| const merged = /* @__PURE__ */ new Map(); | ||
| for (const file of files) { | ||
| const existing = merged.get(file.path); | ||
| merged.set(file.path, existing ? mergeFile(existing, file) : file); | ||
| } | ||
| return merged; | ||
| } | ||
| /** | ||
| * In-memory file store for generated files. | ||
| * | ||
| * Files with the same `path` are merged — sources, imports, and exports are concatenated. | ||
| * The `files` getter returns all stored files sorted by path length (shortest first). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { FileManager } from '@kubb/core' | ||
| * | ||
| * const manager = new FileManager() | ||
| * manager.upsert(myFile) | ||
| * console.log(manager.files) // all stored files | ||
| * ``` | ||
| */ | ||
| var FileManager = class { | ||
| #cache = /* @__PURE__ */ new Map(); | ||
| #filesCache = null; | ||
| /** | ||
| * Adds one or more files. Incoming files with the same path are merged | ||
| * (sources/imports/exports concatenated), but existing cache entries are | ||
| * replaced — use {@link upsert} when you want to merge into the cache too. | ||
| */ | ||
| add(...files) { | ||
| return this.#store(files, false); | ||
| } | ||
| /** | ||
| * Adds or merges one or more files. | ||
| * If a file with the same path already exists in the cache, its | ||
| * sources/imports/exports are merged into the incoming file. | ||
| */ | ||
| upsert(...files) { | ||
| return this.#store(files, true); | ||
| } | ||
| #store(files, mergeExisting) { | ||
| const resolvedFiles = []; | ||
| for (const file of mergeFilesByPath(files).values()) { | ||
| const existing = mergeExisting ? this.#cache.get(file.path) : void 0; | ||
| const resolvedFile = createFile(existing ? mergeFile(existing, file) : file); | ||
| this.#cache.set(resolvedFile.path, resolvedFile); | ||
| resolvedFiles.push(resolvedFile); | ||
| } | ||
| this.#filesCache = null; | ||
| return resolvedFiles; | ||
| } | ||
| getByPath(path) { | ||
| return this.#cache.get(path) ?? null; | ||
| } | ||
| deleteByPath(path) { | ||
| this.#cache.delete(path); | ||
| this.#filesCache = null; | ||
| } | ||
| clear() { | ||
| this.#cache.clear(); | ||
| this.#filesCache = null; | ||
| } | ||
| /** | ||
| * All stored files, sorted by path length (shorter paths first). | ||
| */ | ||
| get files() { | ||
| if (this.#filesCache) return this.#filesCache; | ||
| this.#filesCache = [...this.#cache.values()].sort((a, b) => { | ||
| const lenDiff = a.path.length - b.path.length; | ||
| if (lenDiff !== 0) return lenDiff; | ||
| const aIsIndex = a.path.endsWith("/index.ts") || a.path === "index.ts"; | ||
| const bIsIndex = b.path.endsWith("/index.ts") || b.path === "index.ts"; | ||
| if (aIsIndex && !bIsIndex) return 1; | ||
| if (!aIsIndex && bIsIndex) return -1; | ||
| return 0; | ||
| }); | ||
| return this.#filesCache; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/renderNode.ts | ||
| /** | ||
| * Handles the return value of a plugin AST hook or generator method. | ||
| * | ||
| * - Renderer output → rendered via the provided `rendererFactory` (e.g. JSX), files stored in `driver.fileManager` | ||
| * - `Array<FileNode>` → added directly into `driver.fileManager` | ||
| * - `void` / `null` / `undefined` → no-op (plugin handled it via `this.upsertFile`) | ||
| * | ||
| * Pass a `rendererFactory` (e.g. `jsxRenderer` from `@kubb/renderer-jsx`) when the result | ||
| * may be a renderer element. Generators that only return `Array<FileNode>` do not need one. | ||
| */ | ||
| async function applyHookResult(result, driver, rendererFactory) { | ||
| if (!result) return; | ||
| if (Array.isArray(result)) { | ||
| driver.fileManager.upsert(...result); | ||
| return; | ||
| } | ||
| if (!rendererFactory) return; | ||
| const renderer = rendererFactory(); | ||
| await renderer.render(result); | ||
| driver.fileManager.upsert(...renderer.files); | ||
| renderer.unmount(); | ||
| } | ||
| //#endregion | ||
| //#region src/PluginDriver.ts | ||
| function enforceOrder(enforce) { | ||
| return enforce === "pre" ? -1 : enforce === "post" ? 1 : 0; | ||
| } | ||
| var PluginDriver = class PluginDriver { | ||
| config; | ||
| options; | ||
| /** | ||
| * Returns `'single'` when `fileOrFolder` has a file extension, `'split'` otherwise. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * PluginDriver.getMode('src/gen/types.ts') // 'single' | ||
| * PluginDriver.getMode('src/gen/types') // 'split' | ||
| * ``` | ||
| */ | ||
| static getMode(fileOrFolder) { | ||
| if (!fileOrFolder) return "split"; | ||
| return extname(fileOrFolder) ? "single" : "split"; | ||
| } | ||
| /** | ||
| * The universal `@kubb/ast` `InputNode` produced by the adapter, set by | ||
| * the build pipeline after the adapter's `parse()` resolves. | ||
| */ | ||
| inputNode = void 0; | ||
| adapter = void 0; | ||
| #studioIsOpen = false; | ||
| /** | ||
| * Central file store for all generated files. | ||
| * Plugins should use `this.addFile()` / `this.upsertFile()` (via their context) to | ||
| * add files; this property gives direct read/write access when needed. | ||
| */ | ||
| fileManager = new FileManager(); | ||
| plugins = /* @__PURE__ */ new Map(); | ||
| /** | ||
| * Tracks which plugins have generators registered via `addGenerator()` (event-based path). | ||
| * Used by the build loop to decide whether to emit generator events for a given plugin. | ||
| */ | ||
| #pluginsWithEventGenerators = /* @__PURE__ */ new Set(); | ||
| #resolvers = /* @__PURE__ */ new Map(); | ||
| #defaultResolvers = /* @__PURE__ */ new Map(); | ||
| #hookListeners = /* @__PURE__ */ new Map(); | ||
| constructor(config, options) { | ||
| this.config = config; | ||
| this.options = options; | ||
| config.plugins.map((rawPlugin) => this.#normalizePlugin(rawPlugin)).filter((plugin) => { | ||
| if (typeof plugin.apply === "function") return plugin.apply(config); | ||
| return true; | ||
| }).sort((a, b) => { | ||
| if (b.dependencies?.includes(a.name)) return -1; | ||
| if (a.dependencies?.includes(b.name)) return 1; | ||
| return enforceOrder(a.enforce) - enforceOrder(b.enforce); | ||
| }).forEach((plugin) => { | ||
| this.plugins.set(plugin.name, plugin); | ||
| }); | ||
| } | ||
| get hooks() { | ||
| return this.options.hooks; | ||
| } | ||
| /** | ||
| * Creates an `NormalizedPlugin` from a hook-style plugin and registers | ||
| * its lifecycle handlers on the `AsyncEventEmitter`. | ||
| */ | ||
| #normalizePlugin(hookPlugin) { | ||
| const normalizedPlugin = { | ||
| name: hookPlugin.name, | ||
| dependencies: hookPlugin.dependencies, | ||
| enforce: hookPlugin.enforce, | ||
| options: { | ||
| output: { path: "." }, | ||
| exclude: [], | ||
| override: [] | ||
| } | ||
| }; | ||
| this.registerPluginHooks(hookPlugin, normalizedPlugin); | ||
| return normalizedPlugin; | ||
| } | ||
| /** | ||
| * Registers a hook-style plugin's lifecycle handlers on the shared `AsyncEventEmitter`. | ||
| * | ||
| * For `kubb:plugin:setup`, the registered listener wraps the globally emitted context with a | ||
| * plugin-specific one so that `addGenerator`, `setResolver`, `setTransformer`, and | ||
| * `setRenderer` all target the correct `normalizedPlugin` entry in the plugins map. | ||
| * | ||
| * All other hooks are iterated and registered directly as pass-through listeners. | ||
| * Any event key present in the global `KubbHooks` interface can be subscribed to. | ||
| * | ||
| * External tooling can subscribe to any of these events via `hooks.on(...)` to observe | ||
| * the plugin lifecycle without modifying plugin behavior. | ||
| * | ||
| * @internal | ||
| */ | ||
| registerPluginHooks(hookPlugin, normalizedPlugin) { | ||
| const { hooks } = hookPlugin; | ||
| if (hooks["kubb:plugin:setup"]) { | ||
| const setupHandler = (globalCtx) => { | ||
| const pluginCtx = { | ||
| ...globalCtx, | ||
| options: hookPlugin.options ?? {}, | ||
| addGenerator: (gen) => { | ||
| this.registerGenerator(normalizedPlugin.name, gen); | ||
| }, | ||
| setResolver: (resolver) => { | ||
| this.setPluginResolver(normalizedPlugin.name, resolver); | ||
| }, | ||
| setTransformer: (visitor) => { | ||
| normalizedPlugin.transformer = visitor; | ||
| }, | ||
| setRenderer: (renderer) => { | ||
| normalizedPlugin.renderer = renderer; | ||
| }, | ||
| setOptions: (opts) => { | ||
| normalizedPlugin.options = { | ||
| ...normalizedPlugin.options, | ||
| ...opts | ||
| }; | ||
| }, | ||
| injectFile: (userFileNode) => { | ||
| this.fileManager.add(createFile(userFileNode)); | ||
| } | ||
| }; | ||
| return hooks["kubb:plugin:setup"](pluginCtx); | ||
| }; | ||
| this.hooks.on("kubb:plugin:setup", setupHandler); | ||
| this.#trackHookListener("kubb:plugin:setup", setupHandler); | ||
| } | ||
| for (const [event, handler] of Object.entries(hooks)) { | ||
| if (event === "kubb:plugin:setup" || !handler) continue; | ||
| this.hooks.on(event, handler); | ||
| this.#trackHookListener(event, handler); | ||
| } | ||
| } | ||
| /** | ||
| * Emits the `kubb:plugin:setup` event so that all registered hook-style plugin listeners | ||
| * can configure generators, resolvers, transformers and renderers before `buildStart` runs. | ||
| * | ||
| * Call this once from `safeBuild` before the plugin execution loop begins. | ||
| */ | ||
| async emitSetupHooks() { | ||
| const noop = () => {}; | ||
| await this.hooks.emit("kubb:plugin:setup", { | ||
| config: this.config, | ||
| options: {}, | ||
| addGenerator: noop, | ||
| setResolver: noop, | ||
| setTransformer: noop, | ||
| setRenderer: noop, | ||
| setOptions: noop, | ||
| injectFile: noop, | ||
| updateConfig: noop | ||
| }); | ||
| } | ||
| /** | ||
| * Registers a generator for the given plugin on the shared event emitter. | ||
| * | ||
| * The generator's `schema`, `operation`, and `operations` methods are registered as | ||
| * listeners on `kubb:generate:schema`, `kubb:generate:operation`, and `kubb:generate:operations` | ||
| * respectively. Each listener is scoped to the owning plugin via a `ctx.plugin.name` check | ||
| * so that generators from different plugins do not cross-fire. | ||
| * | ||
| * The renderer resolution chain is: `generator.renderer → plugin.renderer → config.renderer`. | ||
| * Set `generator.renderer = null` to explicitly opt out of rendering even when the plugin | ||
| * declares a renderer. | ||
| * | ||
| * Call this method inside `addGenerator()` (in `kubb:plugin:setup`) to wire up a generator. | ||
| */ | ||
| registerGenerator(pluginName, gen) { | ||
| const resolveRenderer = () => { | ||
| const plugin = this.plugins.get(pluginName); | ||
| return gen.renderer === null ? void 0 : gen.renderer ?? plugin?.renderer ?? this.config.renderer; | ||
| }; | ||
| if (gen.schema) { | ||
| const schemaHandler = async (node, ctx) => { | ||
| if (ctx.plugin.name !== pluginName) return; | ||
| await applyHookResult(await gen.schema(node, ctx), this, resolveRenderer()); | ||
| }; | ||
| this.hooks.on("kubb:generate:schema", schemaHandler); | ||
| this.#trackHookListener("kubb:generate:schema", schemaHandler); | ||
| } | ||
| if (gen.operation) { | ||
| const operationHandler = async (node, ctx) => { | ||
| if (ctx.plugin.name !== pluginName) return; | ||
| await applyHookResult(await gen.operation(node, ctx), this, resolveRenderer()); | ||
| }; | ||
| this.hooks.on("kubb:generate:operation", operationHandler); | ||
| this.#trackHookListener("kubb:generate:operation", operationHandler); | ||
| } | ||
| if (gen.operations) { | ||
| const operationsHandler = async (nodes, ctx) => { | ||
| if (ctx.plugin.name !== pluginName) return; | ||
| await applyHookResult(await gen.operations(nodes, ctx), this, resolveRenderer()); | ||
| }; | ||
| this.hooks.on("kubb:generate:operations", operationsHandler); | ||
| this.#trackHookListener("kubb:generate:operations", operationsHandler); | ||
| } | ||
| this.#pluginsWithEventGenerators.add(pluginName); | ||
| } | ||
| /** | ||
| * Returns `true` when at least one generator was registered for the given plugin | ||
| * via `addGenerator()` in `kubb:plugin:setup` (event-based path). | ||
| * | ||
| * Used by the build loop to decide whether to walk the AST and emit generator events | ||
| * for a plugin that has no static `plugin.generators`. | ||
| */ | ||
| hasRegisteredGenerators(pluginName) { | ||
| return this.#pluginsWithEventGenerators.has(pluginName); | ||
| } | ||
| /** | ||
| * Unregisters all plugin lifecycle listeners from the shared event emitter. | ||
| * Called at the end of a build to prevent listener leaks across repeated builds. | ||
| * | ||
| * @internal | ||
| */ | ||
| dispose() { | ||
| for (const [event, handlers] of this.#hookListeners) for (const handler of handlers) this.hooks.off(event, handler); | ||
| this.#hookListeners.clear(); | ||
| this.#pluginsWithEventGenerators.clear(); | ||
| } | ||
| #trackHookListener(event, handler) { | ||
| let handlers = this.#hookListeners.get(event); | ||
| if (!handlers) { | ||
| handlers = /* @__PURE__ */ new Set(); | ||
| this.#hookListeners.set(event, handlers); | ||
| } | ||
| handlers.add(handler); | ||
| } | ||
| #createDefaultResolver(pluginName) { | ||
| const existingResolver = this.#defaultResolvers.get(pluginName); | ||
| if (existingResolver) return existingResolver; | ||
| const resolver = defineResolver((_ctx) => ({ | ||
| name: "default", | ||
| pluginName | ||
| })); | ||
| this.#defaultResolvers.set(pluginName, resolver); | ||
| return resolver; | ||
| } | ||
| /** | ||
| * Merges `partial` with the plugin's default resolver and stores the result. | ||
| * Also mirrors it onto `plugin.resolver` so callers using `getPlugin(name).resolver` | ||
| * get the up-to-date resolver without going through `getResolver()`. | ||
| */ | ||
| setPluginResolver(pluginName, partial) { | ||
| const merged = { | ||
| ...this.#createDefaultResolver(pluginName), | ||
| ...partial | ||
| }; | ||
| this.#resolvers.set(pluginName, merged); | ||
| const plugin = this.plugins.get(pluginName); | ||
| if (plugin) plugin.resolver = merged; | ||
| } | ||
| getResolver(pluginName) { | ||
| return this.#resolvers.get(pluginName) ?? this.plugins.get(pluginName)?.resolver ?? this.#createDefaultResolver(pluginName); | ||
| } | ||
| getContext(plugin) { | ||
| const driver = this; | ||
| return { | ||
| config: driver.config, | ||
| get root() { | ||
| return resolve(driver.config.root, driver.config.output.path); | ||
| }, | ||
| getMode(output) { | ||
| return PluginDriver.getMode(resolve(driver.config.root, driver.config.output.path, output.path)); | ||
| }, | ||
| hooks: driver.hooks, | ||
| plugin, | ||
| getPlugin: driver.getPlugin.bind(driver), | ||
| requirePlugin: driver.requirePlugin.bind(driver), | ||
| getResolver: driver.getResolver.bind(driver), | ||
| driver, | ||
| addFile: async (...files) => { | ||
| driver.fileManager.add(...files); | ||
| }, | ||
| upsertFile: async (...files) => { | ||
| driver.fileManager.upsert(...files); | ||
| }, | ||
| get inputNode() { | ||
| return driver.inputNode; | ||
| }, | ||
| get adapter() { | ||
| return driver.adapter; | ||
| }, | ||
| get resolver() { | ||
| return driver.getResolver(plugin.name); | ||
| }, | ||
| get transformer() { | ||
| return plugin.transformer; | ||
| }, | ||
| warn(message) { | ||
| driver.hooks.emit("kubb:warn", { message }); | ||
| }, | ||
| error(error) { | ||
| driver.hooks.emit("kubb:error", { error: typeof error === "string" ? new Error(error) : error }); | ||
| }, | ||
| info(message) { | ||
| driver.hooks.emit("kubb:info", { message }); | ||
| }, | ||
| openInStudio(options) { | ||
| if (!driver.config.devtools || driver.#studioIsOpen) return; | ||
| if (typeof driver.config.devtools !== "object") throw new Error("Devtools must be an object"); | ||
| if (!driver.inputNode || !driver.adapter) throw new Error("adapter is not defined, make sure you have set the parser in kubb.config.ts"); | ||
| driver.#studioIsOpen = true; | ||
| const studioUrl = driver.config.devtools?.studioUrl ?? "https://studio.kubb.dev"; | ||
| return openInStudio(driver.inputNode, studioUrl, options); | ||
| } | ||
| }; | ||
| } | ||
| getPlugin(pluginName) { | ||
| return this.plugins.get(pluginName); | ||
| } | ||
| requirePlugin(pluginName) { | ||
| const plugin = this.plugins.get(pluginName); | ||
| if (!plugin) throw new Error(`[kubb] Plugin "${pluginName}" is required but not found. Make sure it is included in your Kubb config.`); | ||
| return plugin; | ||
| } | ||
| }; | ||
| //#endregion | ||
| export { DEFAULT_BANNER as a, logLevel as c, defineResolver as i, camelCase as l, applyHookResult as n, DEFAULT_EXTENSION as o, FileManager as r, DEFAULT_STUDIO_URL as s, PluginDriver as t }; | ||
| //# sourceMappingURL=PluginDriver-DV3p2Hky.js.map |
| {"version":3,"file":"PluginDriver-DV3p2Hky.js","names":["#cache","#store","#filesCache","#pluginsWithEventGenerators","#resolvers","#defaultResolvers","#hookListeners","#normalizePlugin","#trackHookListener","#createDefaultResolver","#studioIsOpen","openInStudioFn"],"sources":["../../../internals/utils/src/casing.ts","../src/constants.ts","../src/defineResolver.ts","../src/devtools.ts","../src/FileManager.ts","../src/renderNode.ts","../src/PluginDriver.ts"],"sourcesContent":["type Options = {\n /**\n * When `true`, dot-separated segments are split on `.` and joined with `/` after casing.\n */\n isFile?: boolean\n /**\n * Text prepended before casing is applied.\n */\n prefix?: string\n /**\n * Text appended before casing is applied.\n */\n suffix?: string\n}\n\n/**\n * Shared implementation for camelCase and PascalCase conversion.\n * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)\n * and capitalizes each word according to `pascal`.\n *\n * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.\n */\nfunction toCamelOrPascal(text: string, pascal: boolean): string {\n const normalized = text\n .trim()\n .replace(/([a-z\\d])([A-Z])/g, '$1 $2')\n .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')\n .replace(/(\\d)([a-z])/g, '$1 $2')\n\n const words = normalized.split(/[\\s\\-_./\\\\:]+/).filter(Boolean)\n\n return words\n .map((word, i) => {\n const allUpper = word.length > 1 && word === word.toUpperCase()\n if (allUpper) return word\n if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1)\n return word.charAt(0).toUpperCase() + word.slice(1)\n })\n .join('')\n .replace(/[^a-zA-Z0-9]/g, '')\n}\n\n/**\n * Splits `text` on `.` and applies `transformPart` to each segment.\n * The last segment receives `isLast = true`, all earlier segments receive `false`.\n * Segments are joined with `/` to form a file path.\n *\n * Only splits on dots followed by a letter so that version numbers\n * embedded in operationIds (e.g. `v2025.0`) are kept intact.\n *\n * Empty segments are filtered before joining. They arise when the text starts with\n * a dot followed immediately by a letter (e.g. `..Schema` splits into `['..', 'Schema']`\n * and `'..'` transforms to an empty string). Without this filter the join would produce\n * a leading `/`, which `path.resolve` would interpret as an absolute path, allowing\n * generated files to escape the configured output directory.\n */\nfunction applyToFileParts(text: string, transformPart: (part: string, isLast: boolean) => string): string {\n const parts = text.split(/\\.(?=[a-zA-Z])/)\n return parts\n .map((part, i) => transformPart(part, i === parts.length - 1))\n .filter(Boolean)\n .join('/')\n}\n\n/**\n * Converts `text` to camelCase.\n * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.\n *\n * @example\n * camelCase('hello-world') // 'helloWorld'\n * camelCase('pet.petId', { isFile: true }) // 'pet/petId'\n */\nexport function camelCase(text: string, { isFile, prefix = '', suffix = '' }: Options = {}): string {\n if (isFile) {\n return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? { prefix, suffix } : {}))\n }\n\n return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false)\n}\n\n/**\n * Converts `text` to PascalCase.\n * When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased.\n *\n * @example\n * pascalCase('hello-world') // 'HelloWorld'\n * pascalCase('pet.petId', { isFile: true }) // 'pet/PetId'\n */\nexport function pascalCase(text: string, { isFile, prefix = '', suffix = '' }: Options = {}): string {\n if (isFile) {\n return applyToFileParts(text, (part, isLast) => (isLast ? pascalCase(part, { prefix, suffix }) : camelCase(part)))\n }\n\n return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true)\n}\n","import type { FileNode } from '@kubb/ast'\n\n/**\n * Base URL for the Kubb Studio web app.\n */\nexport const DEFAULT_STUDIO_URL = 'https://studio.kubb.dev' as const\n\n/**\n * Maximum number of files processed in parallel by FileProcessor.\n */\nexport const PARALLEL_CONCURRENCY_LIMIT = 100\n\n/**\n * Default banner style written at the top of every generated file.\n */\nexport const DEFAULT_BANNER = 'simple' as const\n\n/**\n * Default file-extension mapping used when no explicit mapping is configured.\n */\nexport const DEFAULT_EXTENSION: Record<FileNode['extname'], FileNode['extname'] | ''> = { '.ts': '.ts' }\n\n/**\n * Numeric log-level thresholds used internally to compare verbosity.\n *\n * Higher numbers are more verbose.\n */\nexport const logLevel = {\n silent: Number.NEGATIVE_INFINITY,\n error: 0,\n warn: 1,\n info: 3,\n verbose: 4,\n debug: 5,\n} as const\n","import path from 'node:path'\nimport { camelCase, pascalCase } from '@internals/utils'\nimport type { FileNode, InputNode, Node, OperationNode, SchemaNode } from '@kubb/ast'\nimport { createFile, isOperationNode, isSchemaNode } from '@kubb/ast'\nimport { PluginDriver } from './PluginDriver.ts'\nimport type {\n Config,\n PluginFactoryOptions,\n ResolveBannerContext,\n ResolveOptionsContext,\n Resolver,\n ResolverContext,\n ResolverFileParams,\n ResolverPathParams,\n} from './types.ts'\n\n/**\n * Builder type for the plugin-specific resolver fields.\n *\n * `default`, `resolveOptions`, `resolvePath`, `resolveFile`, `resolveBanner`, and `resolveFooter`\n * are optional — built-in fallbacks are injected when omitted.\n *\n * The builder receives `ctx` — a reference to the fully assembled resolver — so methods can\n * call sibling resolver methods without using `this`. Because `ctx` is captured by the closure\n * and the resolver is populated after the builder runs, `ctx` correctly reflects any overrides\n * that were applied by the builder itself.\n */\ntype ResolverBuilder<T extends PluginFactoryOptions> = (ctx: T['resolver']) => Omit<\n T['resolver'],\n 'default' | 'resolveOptions' | 'resolvePath' | 'resolveFile' | 'resolveBanner' | 'resolveFooter' | 'name' | 'pluginName'\n> &\n Partial<Pick<T['resolver'], 'default' | 'resolveOptions' | 'resolvePath' | 'resolveFile' | 'resolveBanner' | 'resolveFooter'>> & {\n name: string\n pluginName: T['name']\n }\n\n// String patterns are compiled lazily and cached — the same filter is reused for every node.\nconst stringPatternCache = new Map<string, RegExp>()\n\nfunction testPattern(value: string, pattern: string | RegExp): boolean {\n if (typeof pattern === 'string') {\n let regex = stringPatternCache.get(pattern)\n if (!regex) {\n regex = new RegExp(pattern)\n stringPatternCache.set(pattern, regex)\n }\n return regex.test(value)\n }\n // Use .match() for user-supplied RegExp to preserve semantics regardless of `g`/`y` flags.\n return value.match(pattern) !== null\n}\n\n/**\n * Checks if an operation matches a pattern for a given filter type (`tag`, `operationId`, `path`, `method`).\n */\nfunction matchesOperationPattern(node: OperationNode, type: string, pattern: string | RegExp): boolean {\n switch (type) {\n case 'tag':\n return node.tags.some((tag) => testPattern(tag, pattern))\n case 'operationId':\n return testPattern(node.operationId, pattern)\n case 'path':\n return testPattern(node.path, pattern)\n case 'method':\n return testPattern(node.method.toLowerCase(), pattern)\n case 'contentType':\n return node.requestBody?.content?.some((c) => testPattern(c.contentType, pattern)) ?? false\n default:\n return false\n }\n}\n\n/**\n * Checks if a schema matches a pattern for a given filter type (`schemaName`).\n *\n * Returns `null` when the filter type doesn't apply to schemas.\n */\nfunction matchesSchemaPattern(node: SchemaNode, type: string, pattern: string | RegExp): boolean | null {\n switch (type) {\n case 'schemaName':\n return node.name ? testPattern(node.name, pattern) : false\n default:\n return null\n }\n}\n\n/**\n * Default name resolver used by `defineResolver`.\n *\n * - `camelCase` for `function` and `file` types.\n * - `PascalCase` for `type`.\n * - `camelCase` for everything else.\n */\nfunction defaultResolver(name: string, type?: 'file' | 'function' | 'type' | 'const'): string {\n let resolvedName = camelCase(name)\n\n if (type === 'file' || type === 'function') {\n resolvedName = camelCase(name, {\n isFile: type === 'file',\n })\n }\n\n if (type === 'type') {\n resolvedName = pascalCase(name)\n }\n\n return resolvedName\n}\n\n/**\n * Default option resolver — applies include/exclude filters and merges matching override options.\n *\n * Returns `null` when the node is filtered out by an `exclude` rule or not matched by any `include` rule.\n *\n * @example Include/exclude filtering\n * ```ts\n * const options = defaultResolveOptions(operationNode, {\n * options: { output: 'types' },\n * exclude: [{ type: 'tag', pattern: 'internal' }],\n * })\n * // → null when node has tag 'internal'\n * ```\n *\n * @example Override merging\n * ```ts\n * const options = defaultResolveOptions(operationNode, {\n * options: { enumType: 'asConst' },\n * override: [{ type: 'operationId', pattern: 'listPets', options: { enumType: 'enum' } }],\n * })\n * // → { enumType: 'enum' } when operationId matches\n * ```\n */\nexport function defaultResolveOptions<TOptions>(\n node: Node,\n { options, exclude = [], include, override = [] }: ResolveOptionsContext<TOptions>,\n): TOptions | null {\n if (isOperationNode(node)) {\n const isExcluded = exclude.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))\n if (isExcluded) {\n return null\n }\n\n if (include && !include.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) {\n return null\n }\n\n const overrideOptions = override.find(({ type, pattern }) => matchesOperationPattern(node, type, pattern))?.options\n\n return { ...options, ...overrideOptions }\n }\n\n if (isSchemaNode(node)) {\n if (exclude.some(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)) {\n return null\n }\n\n if (include) {\n const results = include.map(({ type, pattern }) => matchesSchemaPattern(node, type, pattern))\n const applicable = results.filter((r) => r !== null)\n if (applicable.length > 0 && !applicable.includes(true)) {\n return null\n }\n }\n\n const overrideOptions = override.find(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)?.options\n\n return { ...options, ...overrideOptions }\n }\n\n return options\n}\n\n/**\n * Default path resolver used by `defineResolver`.\n *\n * - Returns the output directory in `single` mode.\n * - Resolves into a tag- or path-based subdirectory when `group` and a `tag`/`path` value are provided.\n * - Falls back to a flat `output/baseName` path otherwise.\n *\n * A custom `group.name` function overrides the default subdirectory naming.\n * For `tag` groups the default is `${camelCase(tag)}Controller`.\n * For `path` groups the default is the first path segment after `/`.\n *\n * @example Flat output\n * ```ts\n * defaultResolvePath({ baseName: 'petTypes.ts' }, { root: '/src', output: { path: 'types' } })\n * // → '/src/types/petTypes.ts'\n * ```\n *\n * @example Tag-based grouping\n * ```ts\n * defaultResolvePath(\n * { baseName: 'petTypes.ts', tag: 'pets' },\n * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },\n * )\n * // → '/src/types/petsController/petTypes.ts'\n * ```\n *\n * @example Path-based grouping\n * ```ts\n * defaultResolvePath(\n * { baseName: 'petTypes.ts', path: '/pets/list' },\n * { root: '/src', output: { path: 'types' }, group: { type: 'path' } },\n * )\n * // → '/src/types/pets/petTypes.ts'\n * ```\n *\n * @example Single-file mode\n * ```ts\n * defaultResolvePath(\n * { baseName: 'petTypes.ts', pathMode: 'single' },\n * { root: '/src', output: { path: 'types' } },\n * )\n * // → '/src/types'\n * ```\n */\nexport function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }: ResolverPathParams, { root, output, group }: ResolverContext): string {\n const mode = pathMode ?? PluginDriver.getMode(path.resolve(root, output.path))\n\n if (mode === 'single') {\n return path.resolve(root, output.path)\n }\n\n let result: string\n\n if (group && (groupPath || tag)) {\n const groupValue = group.type === 'path' ? groupPath! : tag!\n const defaultName =\n group.type === 'tag'\n ? ({ group: g }: { group: string }) => `${camelCase(g)}Controller`\n : ({ group: g }: { group: string }) => {\n // Strip traversal components (empty, '.', '..') before taking the first meaningful segment.\n // When every segment is a traversal component (e.g. '../../') we fall back to '' so the\n // file is placed directly in the output root — the boundary check below ensures safety.\n const segment = g.split('/').filter((s) => s !== '' && s !== '.' && s !== '..')[0]\n return segment ? camelCase(segment) : ''\n }\n const resolveName = group.name ?? defaultName\n result = path.resolve(root, output.path, resolveName({ group: groupValue }), baseName)\n } else {\n result = path.resolve(root, output.path, baseName)\n }\n\n // Ensure the resolved path stays within the configured output directory.\n // This prevents path traversal from malicious OpenAPI specs or custom group.name functions.\n // `result === outputDir` is intentionally permitted: it matches single-file mode paths and\n // edge cases where baseName resolves to the output directory itself.\n const outputDir = path.resolve(root, output.path)\n const outputDirWithSep = outputDir.endsWith(path.sep) ? outputDir : `${outputDir}${path.sep}`\n if (result !== outputDir && !result.startsWith(outputDirWithSep)) {\n throw new Error(\n `[Kubb] Resolved path \"${result}\" is outside the output directory \"${outputDir}\". ` +\n 'This may indicate a path traversal attempt in the OpenAPI specification or a misconfigured group.name function.',\n )\n }\n\n return result\n}\n\n/**\n * Default file resolver used by `defineResolver`.\n *\n * Resolves a `FileNode` by combining name resolution (`resolver.default`) with\n * path resolution (`resolver.resolvePath`). The resolved file always has empty\n * `sources`, `imports`, and `exports` arrays — consumers populate those separately.\n *\n * In `single` mode the name is omitted and the file sits directly in the output directory.\n *\n * @example Resolve a schema file\n * ```ts\n * const file = defaultResolveFile(\n * { name: 'pet', extname: '.ts' },\n * { root: '/src', output: { path: 'types' } },\n * resolver,\n * )\n * // → { baseName: 'pet.ts', path: '/src/types/pet.ts', sources: [], ... }\n * ```\n *\n * @example Resolve an operation file with tag grouping\n * ```ts\n * const file = defaultResolveFile(\n * { name: 'listPets', extname: '.ts', tag: 'pets' },\n * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },\n * resolver,\n * )\n * // → { baseName: 'listPets.ts', path: '/src/types/petsController/listPets.ts', ... }\n * ```\n */\nexport function defaultResolveFile({ name, extname, tag, path: groupPath }: ResolverFileParams, context: ResolverContext, ctx: Resolver): FileNode {\n const pathMode = PluginDriver.getMode(path.resolve(context.root, context.output.path))\n const resolvedName = pathMode === 'single' ? '' : ctx.default(name, 'file')\n const baseName = `${resolvedName}${extname}` as FileNode['baseName']\n const filePath = ctx.resolvePath({ baseName, pathMode, tag, path: groupPath }, context)\n\n return createFile({\n path: filePath,\n baseName: path.basename(filePath) as `${string}.${string}`,\n meta: {\n pluginName: ctx.pluginName,\n },\n sources: [],\n imports: [],\n exports: [],\n })\n}\n\n/**\n * Generates the default \"Generated by Kubb\" banner from config and optional node metadata.\n */\nexport function buildDefaultBanner({\n title,\n description,\n version,\n config,\n}: {\n title?: string\n description?: string\n version?: string\n config: Config\n}): string {\n try {\n let source = ''\n if (Array.isArray(config.input)) {\n const first = config.input[0]\n if (first && 'path' in first) {\n source = path.basename(first.path)\n }\n } else if ('path' in config.input) {\n source = path.basename(config.input.path)\n } else if ('data' in config.input) {\n source = 'text content'\n }\n\n let banner = '/**\\n* Generated by Kubb (https://kubb.dev/).\\n* Do not edit manually.\\n'\n\n if (config.output.defaultBanner === 'simple') {\n banner += '*/\\n'\n return banner\n }\n\n if (source) {\n banner += `* Source: ${source}\\n`\n }\n\n if (title) {\n banner += `* Title: ${title}\\n`\n }\n\n if (description) {\n const formattedDescription = description.replace(/\\n/gm, '\\n* ')\n banner += `* Description: ${formattedDescription}\\n`\n }\n\n if (version) {\n banner += `* OpenAPI spec version: ${version}\\n`\n }\n\n banner += '*/\\n'\n return banner\n } catch (_error) {\n return '/**\\n* Generated by Kubb (https://kubb.dev/).\\n* Do not edit manually.\\n*/'\n }\n}\n\n/**\n * Default banner resolver — returns the banner string for a generated file.\n *\n * A user-supplied `output.banner` overrides the default Kubb \"Generated by Kubb\" notice.\n * When no `output.banner` is set, the Kubb notice is used (including `title` and `version`\n * from the OAS spec when a `node` is provided).\n *\n * - When `output.banner` is a function and `node` is provided, returns `output.banner(node)`.\n * - When `output.banner` is a function and `node` is absent, falls back to the Kubb notice.\n * - When `output.banner` is a string, returns it directly.\n * - When `config.output.defaultBanner` is `false`, returns `undefined`.\n * - Otherwise returns the Kubb \"Generated by Kubb\" notice.\n *\n * @example String banner overrides default\n * ```ts\n * defaultResolveBanner(undefined, { output: { banner: '// my banner' }, config })\n * // → '// my banner'\n * ```\n *\n * @example Function banner with node\n * ```ts\n * defaultResolveBanner(inputNode, { output: { banner: (node) => `// v${node.version}` }, config })\n * // → '// v3.0.0'\n * ```\n *\n * @example No user banner — Kubb notice with OAS metadata\n * ```ts\n * defaultResolveBanner(inputNode, { config })\n * // → '/** Generated by Kubb ... Title: Pet Store ... *\\/'\n * ```\n *\n * @example Disabled default banner\n * ```ts\n * defaultResolveBanner(undefined, { config: { output: { defaultBanner: false }, ...config } })\n * // → undefined\n * ```\n */\nexport function defaultResolveBanner(node: InputNode | undefined, { output, config }: ResolveBannerContext): string | undefined {\n if (typeof output?.banner === 'function') {\n return output.banner(node)\n }\n\n if (typeof output?.banner === 'string') {\n return output.banner\n }\n\n if (config.output.defaultBanner === false) {\n return undefined\n }\n\n return buildDefaultBanner({\n title: node?.meta?.title,\n version: node?.meta?.version,\n config,\n })\n}\n\n/**\n * Default footer resolver — returns the footer string for a generated file.\n *\n * - When `output.footer` is a function and `node` is provided, calls it with the node.\n * - When `output.footer` is a function and `node` is absent, returns `undefined`.\n * - When `output.footer` is a string, returns it directly.\n * - Otherwise returns `undefined`.\n *\n * @example String footer\n * ```ts\n * defaultResolveFooter(undefined, { output: { footer: '// end of file' }, config })\n * // → '// end of file'\n * ```\n *\n * @example Function footer with node\n * ```ts\n * defaultResolveFooter(inputNode, { output: { footer: (node) => `// ${node.title}` }, config })\n * // → '// Pet Store'\n * ```\n */\nexport function defaultResolveFooter(node: InputNode | undefined, { output }: ResolveBannerContext): string | undefined {\n if (typeof output?.footer === 'function') {\n return node ? output.footer(node) : undefined\n }\n if (typeof output?.footer === 'string') {\n return output.footer\n }\n return undefined\n}\n\n/**\n * Defines a resolver for a plugin, injecting built-in defaults for name casing,\n * include/exclude/override filtering, path resolution, and file construction.\n *\n * All four defaults can be overridden by providing them in the builder function:\n * - `default` — name casing strategy (camelCase / PascalCase)\n * - `resolveOptions` — include/exclude/override filtering\n * - `resolvePath` — output path computation\n * - `resolveFile` — full `FileNode` construction\n *\n * The builder receives `ctx` — a reference to the assembled resolver — so methods can\n * call sibling resolver methods using `ctx` instead of `this`.\n *\n * @example Basic resolver with naming helpers\n * ```ts\n * export const resolver = defineResolver<PluginTs>((ctx) => ({\n * name: 'default',\n * resolveName(node) {\n * return ctx.default(node.name, 'function')\n * },\n * resolveTypedName(node) {\n * return ctx.default(node.name, 'type')\n * },\n * }))\n * ```\n *\n * @example Override resolvePath for a custom output structure\n * ```ts\n * export const resolver = defineResolver<PluginTs>((_ctx) => ({\n * name: 'custom',\n * resolvePath({ baseName }, { root, output }) {\n * return path.resolve(root, output.path, 'generated', baseName)\n * },\n * }))\n * ```\n *\n * @example Use ctx.default inside a helper\n * ```ts\n * export const resolver = defineResolver<PluginTs>((ctx) => ({\n * name: 'default',\n * resolveParamName(node, param) {\n * return ctx.default(`${node.operationId} ${param.in} ${param.name}`, 'type')\n * },\n * }))\n * ```\n */\nexport function defineResolver<T extends PluginFactoryOptions>(build: ResolverBuilder<T>): T['resolver'] {\n // Create the resolver shell first. When `build(resolver)` executes below, `resolver` is\n // still empty, but methods returned by the builder capture it by reference. By the time\n // those methods are actually called, `Object.assign` will have already populated all\n // properties (including any overrides from the builder itself).\n const resolver = {} as T['resolver']\n\n Object.assign(resolver, {\n default: defaultResolver,\n resolveOptions: defaultResolveOptions,\n resolvePath: defaultResolvePath,\n // Wire the default resolveFile implementation with a wrapper that passes resolver as ctx.\n // Unlike other defaults which can be assigned directly, defaultResolveFile requires the\n // resolver as its third parameter.\n resolveFile: (params: ResolverFileParams, context: ResolverContext) => defaultResolveFile(params, context, resolver as Resolver),\n resolveBanner: defaultResolveBanner,\n resolveFooter: defaultResolveFooter,\n // Builder overrides are applied last. Any method in the builder can call\n // ctx.xxx() and will see the fully merged resolver (including its own overrides).\n ...build(resolver),\n })\n\n return resolver\n}\n","import type { InputNode } from '@kubb/ast'\nimport { deflateSync, inflateSync } from 'fflate'\nimport { x } from 'tinyexec'\nimport type { DevtoolsOptions } from './types.ts'\n\n/**\n * Encodes an `InputNode` as a compressed, URL-safe string.\n *\n * The JSON representation is deflate-compressed with {@link deflateSync} before\n * base64url encoding, which typically reduces payload size by 70–80 % and\n * keeps URLs well within browser and server path-length limits.\n *\n * Use {@link decodeAst} to reverse.\n */\nexport function encodeAst(input: InputNode): string {\n const compressed = deflateSync(new TextEncoder().encode(JSON.stringify(input)))\n return Buffer.from(compressed).toString('base64url')\n}\n\n/**\n * Decodes an `InputNode` from a string produced by {@link encodeAst}.\n *\n * Works in both Node.js and the browser — no streaming APIs required.\n */\nexport function decodeAst(encoded: string): InputNode {\n const bytes = Buffer.from(encoded, 'base64url')\n return JSON.parse(new TextDecoder().decode(inflateSync(bytes))) as InputNode\n}\n\n/**\n * Constructs the Kubb Studio URL for the given `InputNode`.\n * When `options.ast` is `true`, navigates to the AST inspector (`/ast`).\n * The `input` is encoded and attached as the `?root=` query parameter so Studio\n * can decode and render it without a round-trip to any server.\n */\nexport function getStudioUrl(input: InputNode, studioUrl: string, options: DevtoolsOptions = {}): string {\n const baseUrl = studioUrl.replace(/\\/$/, '')\n const path = options.ast ? '/ast' : ''\n\n return `${baseUrl}${path}?root=${encodeAst(input)}`\n}\n\n/**\n * Opens the Kubb Studio URL for the given `InputNode` in the default browser —\n *\n * Falls back to printing the URL if the browser cannot be launched.\n */\nexport async function openInStudio(input: InputNode, studioUrl: string, options: DevtoolsOptions = {}): Promise<void> {\n const url = getStudioUrl(input, studioUrl, options)\n\n const cmd = process.platform === 'win32' ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open'\n const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url]\n\n try {\n await x(cmd, args)\n } catch {\n console.log(`\\n ${url}\\n`)\n }\n}\n","import type { FileNode } from '@kubb/ast'\nimport { createFile } from '@kubb/ast'\n\nfunction mergeFile<TMeta extends object = object>(a: FileNode<TMeta>, b: FileNode<TMeta>): FileNode<TMeta> {\n return {\n ...a,\n // Incoming file (b) takes precedence for banner/footer so that barrel files,\n // which never carry a banner, can clear banners set by plugin-generated files\n // at the same path.\n banner: b.banner,\n footer: b.footer,\n sources: [...(a.sources || []), ...(b.sources || [])],\n imports: [...(a.imports || []), ...(b.imports || [])],\n exports: [...(a.exports || []), ...(b.exports || [])],\n }\n}\n\n/**\n * Collapses a list of files so that duplicates sharing the same `path` are merged\n * in arrival order. Keeps the original order of first occurrence.\n */\nfunction mergeFilesByPath(files: ReadonlyArray<FileNode>): Map<string, FileNode> {\n const merged = new Map<string, FileNode>()\n for (const file of files) {\n const existing = merged.get(file.path)\n merged.set(file.path, existing ? mergeFile(existing, file) : file)\n }\n return merged\n}\n\n/**\n * In-memory file store for generated files.\n *\n * Files with the same `path` are merged — sources, imports, and exports are concatenated.\n * The `files` getter returns all stored files sorted by path length (shortest first).\n *\n * @example\n * ```ts\n * import { FileManager } from '@kubb/core'\n *\n * const manager = new FileManager()\n * manager.upsert(myFile)\n * console.log(manager.files) // all stored files\n * ```\n */\nexport class FileManager {\n readonly #cache = new Map<string, FileNode>()\n #filesCache: Array<FileNode> | null = null\n\n /**\n * Adds one or more files. Incoming files with the same path are merged\n * (sources/imports/exports concatenated), but existing cache entries are\n * replaced — use {@link upsert} when you want to merge into the cache too.\n */\n add(...files: Array<FileNode>): Array<FileNode> {\n return this.#store(files, false)\n }\n\n /**\n * Adds or merges one or more files.\n * If a file with the same path already exists in the cache, its\n * sources/imports/exports are merged into the incoming file.\n */\n upsert(...files: Array<FileNode>): Array<FileNode> {\n return this.#store(files, true)\n }\n\n #store(files: ReadonlyArray<FileNode>, mergeExisting: boolean): Array<FileNode> {\n const resolvedFiles: Array<FileNode> = []\n for (const file of mergeFilesByPath(files).values()) {\n const existing = mergeExisting ? this.#cache.get(file.path) : undefined\n const resolvedFile = createFile(existing ? mergeFile(existing, file) : file)\n this.#cache.set(resolvedFile.path, resolvedFile)\n resolvedFiles.push(resolvedFile)\n }\n this.#filesCache = null\n return resolvedFiles\n }\n\n getByPath(path: string): FileNode | null {\n return this.#cache.get(path) ?? null\n }\n\n deleteByPath(path: string): void {\n this.#cache.delete(path)\n this.#filesCache = null\n }\n\n clear(): void {\n this.#cache.clear()\n this.#filesCache = null\n }\n\n /**\n * All stored files, sorted by path length (shorter paths first).\n */\n get files(): Array<FileNode> {\n if (this.#filesCache) {\n return this.#filesCache\n }\n\n this.#filesCache = [...this.#cache.values()].sort((a, b) => {\n const lenDiff = a.path.length - b.path.length\n if (lenDiff !== 0) return lenDiff\n // Within the same length bucket, index.ts barrel files go last so other\n // files are always processed before their barrel file.\n const aIsIndex = a.path.endsWith('/index.ts') || a.path === 'index.ts'\n const bIsIndex = b.path.endsWith('/index.ts') || b.path === 'index.ts'\n if (aIsIndex && !bIsIndex) return 1\n if (!aIsIndex && bIsIndex) return -1\n return 0\n })\n return this.#filesCache\n }\n}\n","import type { FileNode } from '@kubb/ast'\nimport type { RendererFactory } from './createRenderer.ts'\nimport type { PluginDriver } from './PluginDriver.ts'\n\n/**\n * Handles the return value of a plugin AST hook or generator method.\n *\n * - Renderer output → rendered via the provided `rendererFactory` (e.g. JSX), files stored in `driver.fileManager`\n * - `Array<FileNode>` → added directly into `driver.fileManager`\n * - `void` / `null` / `undefined` → no-op (plugin handled it via `this.upsertFile`)\n *\n * Pass a `rendererFactory` (e.g. `jsxRenderer` from `@kubb/renderer-jsx`) when the result\n * may be a renderer element. Generators that only return `Array<FileNode>` do not need one.\n */\nexport async function applyHookResult<TElement = unknown>(\n result: TElement | Array<FileNode> | void,\n driver: PluginDriver,\n rendererFactory?: RendererFactory<TElement>,\n): Promise<void> {\n if (!result) return\n\n if (Array.isArray(result)) {\n driver.fileManager.upsert(...(result as Array<FileNode>))\n return\n }\n\n if (!rendererFactory) {\n return\n }\n\n const renderer = rendererFactory()\n await renderer.render(result)\n driver.fileManager.upsert(...renderer.files)\n renderer.unmount()\n}\n","import { extname, resolve } from 'node:path'\nimport type { AsyncEventEmitter } from '@internals/utils'\nimport type { FileNode, InputNode, OperationNode, SchemaNode } from '@kubb/ast'\nimport { createFile } from '@kubb/ast'\nimport { DEFAULT_STUDIO_URL } from './constants.ts'\nimport type { Generator } from './defineGenerator.ts'\nimport type { Plugin } from './definePlugin.ts'\nimport { defineResolver } from './defineResolver.ts'\nimport { openInStudio as openInStudioFn } from './devtools.ts'\nimport { FileManager } from './FileManager.ts'\nimport { applyHookResult } from './renderNode.ts'\n\nimport type {\n Adapter,\n Config,\n DevtoolsOptions,\n GeneratorContext,\n KubbHooks,\n KubbPluginSetupContext,\n NormalizedPlugin,\n PluginFactoryOptions,\n Resolver,\n} from './types.ts'\n\n// inspired by: https://github.com/rollup/rollup/blob/master/src/utils/PluginDriver.ts#\n\ntype Options = {\n hooks: AsyncEventEmitter<KubbHooks>\n}\n\nfunction enforceOrder(enforce: 'pre' | 'post' | undefined): number {\n return enforce === 'pre' ? -1 : enforce === 'post' ? 1 : 0\n}\n\nexport class PluginDriver {\n readonly config: Config\n readonly options: Options\n\n /**\n * Returns `'single'` when `fileOrFolder` has a file extension, `'split'` otherwise.\n *\n * @example\n * ```ts\n * PluginDriver.getMode('src/gen/types.ts') // 'single'\n * PluginDriver.getMode('src/gen/types') // 'split'\n * ```\n */\n static getMode(fileOrFolder: string | undefined | null): 'single' | 'split' {\n if (!fileOrFolder) {\n return 'split'\n }\n return extname(fileOrFolder) ? 'single' : 'split'\n }\n\n /**\n * The universal `@kubb/ast` `InputNode` produced by the adapter, set by\n * the build pipeline after the adapter's `parse()` resolves.\n */\n inputNode: InputNode | undefined = undefined\n adapter: Adapter | undefined = undefined\n #studioIsOpen = false\n\n /**\n * Central file store for all generated files.\n * Plugins should use `this.addFile()` / `this.upsertFile()` (via their context) to\n * add files; this property gives direct read/write access when needed.\n */\n readonly fileManager = new FileManager()\n\n readonly plugins = new Map<string, NormalizedPlugin>()\n\n /**\n * Tracks which plugins have generators registered via `addGenerator()` (event-based path).\n * Used by the build loop to decide whether to emit generator events for a given plugin.\n */\n readonly #pluginsWithEventGenerators = new Set<string>()\n readonly #resolvers = new Map<string, Resolver>()\n readonly #defaultResolvers = new Map<string, Resolver>()\n readonly #hookListeners = new Map<keyof KubbHooks, Set<(...args: never[]) => void | Promise<void>>>()\n\n constructor(config: Config, options: Options) {\n this.config = config\n this.options = options\n config.plugins\n .map((rawPlugin) => this.#normalizePlugin(rawPlugin as Plugin))\n .filter((plugin) => {\n if (typeof plugin.apply === 'function') {\n return plugin.apply(config)\n }\n return true\n })\n .sort((a, b) => {\n if (b.dependencies?.includes(a.name)) return -1\n if (a.dependencies?.includes(b.name)) return 1\n // enforce: 'pre' plugins run first, 'post' plugins run last\n return enforceOrder(a.enforce) - enforceOrder(b.enforce)\n })\n .forEach((plugin) => {\n this.plugins.set(plugin.name, plugin)\n })\n }\n\n get hooks() {\n return this.options.hooks\n }\n\n /**\n * Creates an `NormalizedPlugin` from a hook-style plugin and registers\n * its lifecycle handlers on the `AsyncEventEmitter`.\n */\n #normalizePlugin(hookPlugin: Plugin): NormalizedPlugin {\n const normalizedPlugin = {\n name: hookPlugin.name,\n dependencies: hookPlugin.dependencies,\n enforce: hookPlugin.enforce,\n options: { output: { path: '.' }, exclude: [], override: [] },\n } as unknown as NormalizedPlugin\n\n this.registerPluginHooks(hookPlugin, normalizedPlugin)\n return normalizedPlugin\n }\n\n /**\n * Registers a hook-style plugin's lifecycle handlers on the shared `AsyncEventEmitter`.\n *\n * For `kubb:plugin:setup`, the registered listener wraps the globally emitted context with a\n * plugin-specific one so that `addGenerator`, `setResolver`, `setTransformer`, and\n * `setRenderer` all target the correct `normalizedPlugin` entry in the plugins map.\n *\n * All other hooks are iterated and registered directly as pass-through listeners.\n * Any event key present in the global `KubbHooks` interface can be subscribed to.\n *\n * External tooling can subscribe to any of these events via `hooks.on(...)` to observe\n * the plugin lifecycle without modifying plugin behavior.\n *\n * @internal\n */\n registerPluginHooks(hookPlugin: Plugin, normalizedPlugin: NormalizedPlugin): void {\n const { hooks } = hookPlugin\n\n // kubb:plugin:setup gets special treatment: the globally emitted context is wrapped with\n // plugin-specific implementations so that addGenerator / setResolver / etc. target\n // this plugin's normalizedPlugin entry rather than being no-ops.\n if (hooks['kubb:plugin:setup']) {\n const setupHandler = (globalCtx: KubbPluginSetupContext) => {\n const pluginCtx: KubbPluginSetupContext = {\n ...globalCtx,\n options: hookPlugin.options ?? {},\n addGenerator: (gen) => {\n this.registerGenerator(normalizedPlugin.name, gen)\n },\n setResolver: (resolver) => {\n this.setPluginResolver(normalizedPlugin.name, resolver)\n },\n setTransformer: (visitor) => {\n normalizedPlugin.transformer = visitor\n },\n setRenderer: (renderer) => {\n normalizedPlugin.renderer = renderer\n },\n setOptions: (opts) => {\n normalizedPlugin.options = { ...normalizedPlugin.options, ...opts }\n },\n injectFile: (userFileNode) => {\n this.fileManager.add(createFile(userFileNode))\n },\n }\n return hooks['kubb:plugin:setup']!(pluginCtx)\n }\n\n this.hooks.on('kubb:plugin:setup', setupHandler)\n this.#trackHookListener('kubb:plugin:setup', setupHandler as (...args: never[]) => void | Promise<void>)\n }\n\n // All other hooks are registered as direct pass-through listeners on the shared emitter.\n for (const [event, handler] of Object.entries(hooks) as Array<[keyof KubbHooks, ((...args: never[]) => void | Promise<void>) | undefined]>) {\n if (event === 'kubb:plugin:setup' || !handler) continue\n\n this.hooks.on(event, handler as never)\n this.#trackHookListener(event, handler as (...args: never[]) => void | Promise<void>)\n }\n }\n\n /**\n * Emits the `kubb:plugin:setup` event so that all registered hook-style plugin listeners\n * can configure generators, resolvers, transformers and renderers before `buildStart` runs.\n *\n * Call this once from `safeBuild` before the plugin execution loop begins.\n */\n async emitSetupHooks(): Promise<void> {\n const noop = () => {}\n await this.hooks.emit('kubb:plugin:setup', {\n config: this.config,\n options: {},\n addGenerator: noop,\n setResolver: noop,\n setTransformer: noop,\n setRenderer: noop,\n setOptions: noop,\n injectFile: noop,\n updateConfig: noop,\n })\n }\n\n /**\n * Registers a generator for the given plugin on the shared event emitter.\n *\n * The generator's `schema`, `operation`, and `operations` methods are registered as\n * listeners on `kubb:generate:schema`, `kubb:generate:operation`, and `kubb:generate:operations`\n * respectively. Each listener is scoped to the owning plugin via a `ctx.plugin.name` check\n * so that generators from different plugins do not cross-fire.\n *\n * The renderer resolution chain is: `generator.renderer → plugin.renderer → config.renderer`.\n * Set `generator.renderer = null` to explicitly opt out of rendering even when the plugin\n * declares a renderer.\n *\n * Call this method inside `addGenerator()` (in `kubb:plugin:setup`) to wire up a generator.\n */\n registerGenerator(pluginName: string, gen: Generator): void {\n const resolveRenderer = () => {\n const plugin = this.plugins.get(pluginName)\n return gen.renderer === null ? undefined : (gen.renderer ?? plugin?.renderer ?? this.config.renderer)\n }\n\n if (gen.schema) {\n const schemaHandler = async (node: SchemaNode, ctx: GeneratorContext) => {\n if (ctx.plugin.name !== pluginName) return\n const result = await gen.schema!(node, ctx)\n await applyHookResult(result, this, resolveRenderer())\n }\n\n this.hooks.on('kubb:generate:schema', schemaHandler)\n this.#trackHookListener('kubb:generate:schema', schemaHandler as (...args: never[]) => void | Promise<void>)\n }\n\n if (gen.operation) {\n const operationHandler = async (node: OperationNode, ctx: GeneratorContext) => {\n if (ctx.plugin.name !== pluginName) return\n const result = await gen.operation!(node, ctx)\n await applyHookResult(result, this, resolveRenderer())\n }\n\n this.hooks.on('kubb:generate:operation', operationHandler)\n this.#trackHookListener('kubb:generate:operation', operationHandler as (...args: never[]) => void | Promise<void>)\n }\n\n if (gen.operations) {\n const operationsHandler = async (nodes: Array<OperationNode>, ctx: GeneratorContext) => {\n if (ctx.plugin.name !== pluginName) return\n const result = await gen.operations!(nodes, ctx)\n await applyHookResult(result, this, resolveRenderer())\n }\n\n this.hooks.on('kubb:generate:operations', operationsHandler)\n this.#trackHookListener('kubb:generate:operations', operationsHandler as (...args: never[]) => void | Promise<void>)\n }\n\n this.#pluginsWithEventGenerators.add(pluginName)\n }\n\n /**\n * Returns `true` when at least one generator was registered for the given plugin\n * via `addGenerator()` in `kubb:plugin:setup` (event-based path).\n *\n * Used by the build loop to decide whether to walk the AST and emit generator events\n * for a plugin that has no static `plugin.generators`.\n */\n hasRegisteredGenerators(pluginName: string): boolean {\n return this.#pluginsWithEventGenerators.has(pluginName)\n }\n\n /**\n * Unregisters all plugin lifecycle listeners from the shared event emitter.\n * Called at the end of a build to prevent listener leaks across repeated builds.\n *\n * @internal\n */\n dispose(): void {\n for (const [event, handlers] of this.#hookListeners) {\n for (const handler of handlers) {\n this.hooks.off(event, handler as never)\n }\n }\n this.#hookListeners.clear()\n this.#pluginsWithEventGenerators.clear()\n }\n\n #trackHookListener(event: keyof KubbHooks, handler: (...args: never[]) => void | Promise<void>): void {\n let handlers = this.#hookListeners.get(event)\n if (!handlers) {\n handlers = new Set()\n this.#hookListeners.set(event, handlers)\n }\n handlers.add(handler)\n }\n\n #createDefaultResolver(pluginName: string): Resolver {\n const existingResolver = this.#defaultResolvers.get(pluginName)\n if (existingResolver) {\n return existingResolver\n }\n\n const resolver = defineResolver<PluginFactoryOptions>((_ctx) => ({\n name: 'default',\n pluginName,\n }))\n this.#defaultResolvers.set(pluginName, resolver)\n return resolver\n }\n\n /**\n * Merges `partial` with the plugin's default resolver and stores the result.\n * Also mirrors it onto `plugin.resolver` so callers using `getPlugin(name).resolver`\n * get the up-to-date resolver without going through `getResolver()`.\n */\n setPluginResolver(pluginName: string, partial: Partial<Resolver>): void {\n const defaultResolver = this.#createDefaultResolver(pluginName)\n const merged = { ...defaultResolver, ...partial }\n this.#resolvers.set(pluginName, merged)\n const plugin = this.plugins.get(pluginName)\n if (plugin) {\n plugin.resolver = merged\n }\n }\n\n /**\n * Returns the resolver for the given plugin.\n *\n * Resolution order: dynamic resolver set via `setPluginResolver` → static resolver on the\n * plugin → lazily created default resolver (identity name, no path transforms).\n */\n getResolver<TName extends keyof Kubb.PluginRegistry>(pluginName: TName): Kubb.PluginRegistry[TName]['resolver']\n getResolver<TResolver extends Resolver = Resolver>(pluginName: string): TResolver\n getResolver(pluginName: string): Resolver {\n return this.#resolvers.get(pluginName) ?? this.plugins.get(pluginName)?.resolver ?? this.#createDefaultResolver(pluginName)\n }\n\n getContext<TOptions extends PluginFactoryOptions>(plugin: NormalizedPlugin<TOptions>): GeneratorContext<TOptions> & Record<string, unknown> {\n const driver = this\n\n const baseContext = {\n config: driver.config,\n get root(): string {\n return resolve(driver.config.root, driver.config.output.path)\n },\n getMode(output: { path: string }): 'single' | 'split' {\n return PluginDriver.getMode(resolve(driver.config.root, driver.config.output.path, output.path))\n },\n hooks: driver.hooks,\n plugin,\n getPlugin: driver.getPlugin.bind(driver),\n requirePlugin: driver.requirePlugin.bind(driver),\n getResolver: driver.getResolver.bind(driver),\n driver,\n addFile: async (...files: Array<FileNode>) => {\n driver.fileManager.add(...files)\n },\n upsertFile: async (...files: Array<FileNode>) => {\n driver.fileManager.upsert(...files)\n },\n get inputNode(): InputNode | undefined {\n return driver.inputNode\n },\n get adapter(): Adapter | undefined {\n return driver.adapter\n },\n get resolver() {\n return driver.getResolver(plugin.name)\n },\n get transformer() {\n return plugin.transformer\n },\n warn(message: string) {\n driver.hooks.emit('kubb:warn', { message })\n },\n error(error: string | Error) {\n driver.hooks.emit('kubb:error', { error: typeof error === 'string' ? new Error(error) : error })\n },\n info(message: string) {\n driver.hooks.emit('kubb:info', { message })\n },\n openInStudio(options?: DevtoolsOptions) {\n if (!driver.config.devtools || driver.#studioIsOpen) {\n return\n }\n\n if (typeof driver.config.devtools !== 'object') {\n throw new Error('Devtools must be an object')\n }\n\n if (!driver.inputNode || !driver.adapter) {\n throw new Error('adapter is not defined, make sure you have set the parser in kubb.config.ts')\n }\n\n driver.#studioIsOpen = true\n\n const studioUrl = driver.config.devtools?.studioUrl ?? DEFAULT_STUDIO_URL\n\n return openInStudioFn(driver.inputNode, studioUrl, options)\n },\n } as unknown as GeneratorContext<TOptions>\n\n return baseContext\n }\n\n getPlugin<TName extends keyof Kubb.PluginRegistry>(pluginName: TName): Plugin<Kubb.PluginRegistry[TName]> | undefined\n getPlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions>(pluginName: string): Plugin<TOptions> | undefined\n getPlugin(pluginName: string): Plugin | undefined {\n return this.plugins.get(pluginName)\n }\n\n /**\n * Like `getPlugin` but throws a descriptive error when the plugin is not found.\n */\n requirePlugin<TName extends keyof Kubb.PluginRegistry>(pluginName: TName): Plugin<Kubb.PluginRegistry[TName]>\n requirePlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions>(pluginName: string): Plugin<TOptions>\n requirePlugin(pluginName: string): Plugin {\n const plugin = this.plugins.get(pluginName)\n if (!plugin) {\n throw new Error(`[kubb] Plugin \"${pluginName}\" is required but not found. Make sure it is included in your Kubb config.`)\n }\n return plugin\n }\n}\n"],"mappings":"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"} |
Sorry, the diff of this file is too big to display
| import type { FileNode } from '@kubb/ast' | ||
| /** | ||
| * Base URL for the Kubb Studio web app. | ||
| */ | ||
| export const DEFAULT_STUDIO_URL = 'https://studio.kubb.dev' as const | ||
| /** | ||
| * Maximum number of files processed in parallel by FileProcessor. | ||
| */ | ||
| export const PARALLEL_CONCURRENCY_LIMIT = 100 | ||
| /** | ||
| * Default banner style written at the top of every generated file. | ||
| */ | ||
| export const DEFAULT_BANNER = 'simple' as const | ||
| /** | ||
| * Default file-extension mapping used when no explicit mapping is configured. | ||
| */ | ||
| export const DEFAULT_EXTENSION: Record<FileNode['extname'], FileNode['extname'] | ''> = { '.ts': '.ts' } | ||
| /** | ||
| * Numeric log-level thresholds used internally to compare verbosity. | ||
| * | ||
| * Higher numbers are more verbose. | ||
| */ | ||
| export const logLevel = { | ||
| silent: Number.NEGATIVE_INFINITY, | ||
| error: 0, | ||
| warn: 1, | ||
| info: 3, | ||
| verbose: 4, | ||
| debug: 5, | ||
| } as const |
| import type { Adapter, AdapterFactoryOptions } from './types.ts' | ||
| type AdapterBuilder<T extends AdapterFactoryOptions> = (options: T['options']) => Adapter<T> | ||
| /** | ||
| * Factory for implementing custom adapters that translate non-OpenAPI specs into Kubb's AST. | ||
| * | ||
| * Use this to support GraphQL schemas, gRPC definitions, AsyncAPI, or custom domain-specific languages. | ||
| * Built-in adapters include `@kubb/adapter-oas` for OpenAPI and Swagger documents. | ||
| * | ||
| * @note Adapters must parse their input format to Kubb's `InputNode` structure. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * export const myAdapter = createAdapter<MyAdapter>((options) => { | ||
| * return { | ||
| * name: 'my-adapter', | ||
| * options, | ||
| * async parse(source) { | ||
| * // Transform source format to InputNode | ||
| * return { ... } | ||
| * }, | ||
| * } | ||
| * }) | ||
| * | ||
| * // Instantiate: | ||
| * const adapter = myAdapter({ validate: true }) | ||
| * ``` | ||
| */ | ||
| export function createAdapter<T extends AdapterFactoryOptions = AdapterFactoryOptions>(build: AdapterBuilder<T>): (options?: T['options']) => Adapter<T> { | ||
| return (options) => build(options ?? ({} as T['options'])) | ||
| } |
| import { resolve } from 'node:path' | ||
| import { AsyncEventEmitter, BuildError, exists, formatMs, getElapsedMs, URLPath } from '@internals/utils' | ||
| import type { FileNode, OperationNode } from '@kubb/ast' | ||
| import { transform, walk } from '@kubb/ast' | ||
| import { DEFAULT_BANNER, DEFAULT_EXTENSION, DEFAULT_STUDIO_URL } from './constants.ts' | ||
| import type { RendererFactory } from './createRenderer.ts' | ||
| import type { Generator } from './defineGenerator.ts' | ||
| import type { Parser } from './defineParser.ts' | ||
| import type { Plugin } from './definePlugin.ts' | ||
| import { FileProcessor } from './FileProcessor.ts' | ||
| import type { Kubb } from './Kubb.ts' | ||
| import { PluginDriver } from './PluginDriver.ts' | ||
| import { applyHookResult } from './renderNode.ts' | ||
| import { fsStorage } from './storages/fsStorage.ts' | ||
| import type { AdapterSource, Config, GeneratorContext, KubbHooks, Middleware, NormalizedPlugin, Storage, UserConfig } from './types.ts' | ||
| import { getDiagnosticInfo } from './utils/diagnostics.ts' | ||
| import { isInputPath } from './utils/isInputPath.ts' | ||
| type SetupOptions = { | ||
| hooks?: AsyncEventEmitter<KubbHooks> | ||
| } | ||
| /** | ||
| * Full output produced by a successful or failed build. | ||
| */ | ||
| export type BuildOutput = { | ||
| /** | ||
| * Plugins that threw during installation, paired with the caught error. | ||
| */ | ||
| failedPlugins: Set<{ plugin: Plugin; error: Error }> | ||
| files: Array<FileNode> | ||
| driver: PluginDriver | ||
| /** | ||
| * Elapsed time in milliseconds for each plugin, keyed by plugin name. | ||
| */ | ||
| pluginTimings: Map<string, number> | ||
| error?: Error | ||
| /** | ||
| * Raw generated source, keyed by absolute file path. | ||
| */ | ||
| sources: Map<string, string> | ||
| } | ||
| type SetupResult = { | ||
| hooks: AsyncEventEmitter<KubbHooks> | ||
| driver: PluginDriver | ||
| sources: Map<string, string> | ||
| config: Config | ||
| storage: Storage | null | ||
| } | ||
| async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promise<SetupResult> { | ||
| const hooks = options.hooks ?? new AsyncEventEmitter<KubbHooks>() | ||
| const sources: Map<string, string> = new Map<string, string>() | ||
| const diagnosticInfo = getDiagnosticInfo() | ||
| if (Array.isArray(userConfig.input)) { | ||
| await hooks.emit('kubb:warn', { message: 'This feature is still under development — use with caution' }) | ||
| } | ||
| await hooks.emit('kubb:debug', { | ||
| date: new Date(), | ||
| logs: [ | ||
| 'Configuration:', | ||
| ` • Name: ${userConfig.name || 'unnamed'}`, | ||
| ` • Root: ${userConfig.root || process.cwd()}`, | ||
| ` • Output: ${userConfig.output?.path || 'not specified'}`, | ||
| ` • Plugins: ${userConfig.plugins?.length || 0}`, | ||
| 'Output Settings:', | ||
| ` • Storage: ${userConfig.storage ? `custom(${userConfig.storage.name})` : userConfig.output?.write === false ? 'disabled' : 'filesystem (default)'}`, | ||
| ` • Formatter: ${userConfig.output?.format || 'none'}`, | ||
| ` • Linter: ${userConfig.output?.lint || 'none'}`, | ||
| 'Environment:', | ||
| Object.entries(diagnosticInfo) | ||
| .map(([key, value]) => ` • ${key}: ${value}`) | ||
| .join('\n'), | ||
| ], | ||
| }) | ||
| try { | ||
| if (isInputPath(userConfig) && !new URLPath(userConfig.input.path).isURL) { | ||
| await exists(userConfig.input.path) | ||
| await hooks.emit('kubb:debug', { | ||
| date: new Date(), | ||
| logs: [`✓ Input file validated: ${userConfig.input.path}`], | ||
| }) | ||
| } | ||
| } catch (caughtError) { | ||
| if (isInputPath(userConfig)) { | ||
| const error = caughtError as Error | ||
| throw new Error( | ||
| `Cannot read file/URL defined in \`input.path\` or set with \`kubb generate PATH\` in the CLI of your Kubb config ${userConfig.input.path}`, | ||
| { | ||
| cause: error, | ||
| }, | ||
| ) | ||
| } | ||
| } | ||
| if (!userConfig.adapter) { | ||
| throw new Error('Adapter should be defined') | ||
| } | ||
| const config: Config = { | ||
| ...userConfig, | ||
| root: userConfig.root || process.cwd(), | ||
| parsers: userConfig.parsers ?? [], | ||
| adapter: userConfig.adapter, | ||
| output: { | ||
| format: false, | ||
| lint: false, | ||
| write: true, | ||
| extension: DEFAULT_EXTENSION, | ||
| defaultBanner: DEFAULT_BANNER, | ||
| ...userConfig.output, | ||
| }, | ||
| devtools: userConfig.devtools | ||
| ? { | ||
| studioUrl: DEFAULT_STUDIO_URL, | ||
| ...(typeof userConfig.devtools === 'boolean' ? {} : userConfig.devtools), | ||
| } | ||
| : undefined, | ||
| plugins: userConfig.plugins as unknown as Config['plugins'], | ||
| } | ||
| const storage: Storage | null = config.output.write === false ? null : (config.storage ?? fsStorage()) | ||
| if (config.output.clean) { | ||
| await hooks.emit('kubb:debug', { | ||
| date: new Date(), | ||
| logs: ['Cleaning output directories', ` • Output: ${config.output.path}`], | ||
| }) | ||
| await storage?.clear(resolve(config.root, config.output.path)) | ||
| } | ||
| const driver = new PluginDriver(config, { | ||
| hooks, | ||
| }) | ||
| // Register middleware hooks after all plugin hooks are registered. | ||
| // Because AsyncEventEmitter calls listeners in registration order, | ||
| // middleware hooks for any event fire after all plugin hooks for that event. | ||
| function registerMiddlewareHook<K extends keyof KubbHooks & string>(event: K, middlewareHooks: Middleware['hooks']) { | ||
| const handler = middlewareHooks[event] | ||
| if (handler) { | ||
| hooks.on(event, handler) | ||
| } | ||
| } | ||
| for (const middleware of config.middleware ?? []) { | ||
| for (const event of Object.keys(middleware.hooks) as Array<keyof KubbHooks & string>) { | ||
| registerMiddlewareHook(event, middleware.hooks) | ||
| } | ||
| } | ||
| const adapter = config.adapter | ||
| if (!adapter) { | ||
| throw new Error('No adapter configured. Please provide an adapter in your kubb.config.ts.') | ||
| } | ||
| const source = inputToAdapterSource(config) | ||
| await hooks.emit('kubb:debug', { | ||
| date: new Date(), | ||
| logs: [`Running adapter: ${adapter.name}`], | ||
| }) | ||
| driver.adapter = adapter | ||
| driver.inputNode = await adapter.parse(source) | ||
| await hooks.emit('kubb:debug', { | ||
| date: new Date(), | ||
| logs: [ | ||
| `✓ Adapter '${adapter.name}' resolved InputNode`, | ||
| ` • Schemas: ${driver.inputNode.schemas.length}`, | ||
| ` • Operations: ${driver.inputNode.operations.length}`, | ||
| ], | ||
| }) | ||
| return { | ||
| config, | ||
| hooks, | ||
| driver, | ||
| sources, | ||
| storage, | ||
| } | ||
| } | ||
| /** | ||
| * Walks the AST and dispatches nodes to a plugin's direct AST hooks | ||
| * (`schema`, `operation`, `operations`). | ||
| */ | ||
| async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorContext): Promise<void> { | ||
| const { adapter, inputNode, resolver, driver } = context | ||
| const { exclude, include, override } = plugin.options | ||
| if (!adapter || !inputNode) { | ||
| throw new Error(`[${plugin.name}] No adapter found. Add an OAS adapter (e.g. pluginOas()) before this plugin in your Kubb config.`) | ||
| } | ||
| function resolveRenderer(gen: Generator): RendererFactory | undefined { | ||
| return gen.renderer === null ? undefined : (gen.renderer ?? plugin.renderer ?? context.config.renderer) | ||
| } | ||
| const generators = plugin.generators ?? [] | ||
| const collectedOperations: Array<OperationNode> = [] | ||
| const generatorContext = { | ||
| ...context, | ||
| resolver: driver.getResolver(plugin.name), | ||
| } | ||
| await walk(inputNode, { | ||
| depth: 'shallow', | ||
| async schema(node) { | ||
| const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node | ||
| const options = resolver.resolveOptions(transformedNode, { | ||
| options: plugin.options, | ||
| exclude, | ||
| include, | ||
| override, | ||
| }) | ||
| if (options === null) return | ||
| const ctx = { ...generatorContext, options } | ||
| for (const gen of generators) { | ||
| if (!gen.schema) continue | ||
| const result = await gen.schema(transformedNode, ctx) | ||
| await applyHookResult(result, driver, resolveRenderer(gen)) | ||
| } | ||
| await driver.hooks.emit('kubb:generate:schema', transformedNode, ctx) | ||
| }, | ||
| async operation(node) { | ||
| const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node | ||
| const options = resolver.resolveOptions(transformedNode, { | ||
| options: plugin.options, | ||
| exclude, | ||
| include, | ||
| override, | ||
| }) | ||
| if (options !== null) { | ||
| collectedOperations.push(transformedNode) | ||
| const ctx = { ...generatorContext, options } | ||
| for (const gen of generators) { | ||
| if (!gen.operation) continue | ||
| const result = await gen.operation(transformedNode, ctx) | ||
| await applyHookResult(result, driver, resolveRenderer(gen)) | ||
| } | ||
| await driver.hooks.emit('kubb:generate:operation', transformedNode, ctx) | ||
| } | ||
| }, | ||
| }) | ||
| if (collectedOperations.length > 0) { | ||
| const ctx = { ...generatorContext, options: plugin.options } | ||
| for (const gen of generators) { | ||
| if (!gen.operations) continue | ||
| const result = await gen.operations(collectedOperations, ctx) | ||
| await applyHookResult(result, driver, resolveRenderer(gen)) | ||
| } | ||
| await driver.hooks.emit('kubb:generate:operations', collectedOperations, ctx) | ||
| } | ||
| } | ||
| async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> { | ||
| const { driver, hooks, sources, storage } = setupResult | ||
| const failedPlugins = new Set<{ plugin: Plugin; error: Error }>() | ||
| const pluginTimings = new Map<string, number>() | ||
| const config = driver.config | ||
| try { | ||
| await driver.emitSetupHooks() | ||
| if (driver.adapter && driver.inputNode) { | ||
| await hooks.emit('kubb:build:start', { | ||
| config, | ||
| adapter: driver.adapter, | ||
| inputNode: driver.inputNode, | ||
| getPlugin: driver.getPlugin.bind(driver), | ||
| get files() { | ||
| return driver.fileManager.files | ||
| }, | ||
| upsertFile: (...files) => driver.fileManager.upsert(...files), | ||
| }) | ||
| } | ||
| for (const plugin of driver.plugins.values()) { | ||
| const context = driver.getContext(plugin) | ||
| const hrStart = process.hrtime() | ||
| try { | ||
| const timestamp = new Date() | ||
| await hooks.emit('kubb:plugin:start', { plugin }) | ||
| await hooks.emit('kubb:debug', { | ||
| date: timestamp, | ||
| logs: ['Starting plugin...', ` • Plugin Name: ${plugin.name}`], | ||
| }) | ||
| if (plugin.generators?.length || driver.hasRegisteredGenerators(plugin.name)) { | ||
| await runPluginAstHooks(plugin, context) | ||
| } | ||
| const duration = getElapsedMs(hrStart) | ||
| pluginTimings.set(plugin.name, duration) | ||
| await hooks.emit('kubb:plugin:end', { | ||
| plugin, | ||
| duration, | ||
| success: true, | ||
| config, | ||
| get files() { | ||
| return driver.fileManager.files | ||
| }, | ||
| upsertFile: (...files) => driver.fileManager.upsert(...files), | ||
| }) | ||
| await hooks.emit('kubb:debug', { | ||
| date: new Date(), | ||
| logs: [`✓ Plugin started successfully (${formatMs(duration)})`], | ||
| }) | ||
| } catch (caughtError) { | ||
| const error = caughtError as Error | ||
| const errorTimestamp = new Date() | ||
| const duration = getElapsedMs(hrStart) | ||
| await hooks.emit('kubb:plugin:end', { | ||
| plugin, | ||
| duration, | ||
| success: false, | ||
| error, | ||
| config, | ||
| get files() { | ||
| return driver.fileManager.files | ||
| }, | ||
| upsertFile: (...files) => driver.fileManager.upsert(...files), | ||
| }) | ||
| await hooks.emit('kubb:debug', { | ||
| date: errorTimestamp, | ||
| logs: [ | ||
| '✗ Plugin start failed', | ||
| ` • Plugin Name: ${plugin.name}`, | ||
| ` • Error: ${error.constructor.name} - ${error.message}`, | ||
| ' • Stack Trace:', | ||
| error.stack || 'No stack trace available', | ||
| ], | ||
| }) | ||
| failedPlugins.add({ plugin, error }) | ||
| } | ||
| } | ||
| await hooks.emit('kubb:plugins:end', { | ||
| config, | ||
| get files() { | ||
| return driver.fileManager.files | ||
| }, | ||
| upsertFile: (...files) => driver.fileManager.upsert(...files), | ||
| }) | ||
| const files = driver.fileManager.files | ||
| const parsersMap = new Map<FileNode['extname'], Parser>() | ||
| for (const parser of config.parsers) { | ||
| if (parser.extNames) { | ||
| for (const extname of parser.extNames) { | ||
| parsersMap.set(extname, parser) | ||
| } | ||
| } | ||
| } | ||
| const fileProcessor = new FileProcessor() | ||
| await hooks.emit('kubb:debug', { | ||
| date: new Date(), | ||
| logs: [`Writing ${files.length} files...`], | ||
| }) | ||
| await fileProcessor.run(files, { | ||
| parsers: parsersMap, | ||
| extension: config.output.extension, | ||
| onStart: async (processingFiles) => { | ||
| await hooks.emit('kubb:files:processing:start', { files: processingFiles }) | ||
| }, | ||
| onUpdate: async ({ file, source, processed, total, percentage }) => { | ||
| await hooks.emit('kubb:file:processing:update', { | ||
| file, | ||
| source, | ||
| processed, | ||
| total, | ||
| percentage, | ||
| config, | ||
| }) | ||
| if (source) { | ||
| await storage?.setItem(file.path, source) | ||
| sources.set(file.path, source) | ||
| } | ||
| }, | ||
| onEnd: async (processedFiles) => { | ||
| await hooks.emit('kubb:files:processing:end', { files: processedFiles }) | ||
| await hooks.emit('kubb:debug', { | ||
| date: new Date(), | ||
| logs: [`✓ File write process completed for ${processedFiles.length} files`], | ||
| }) | ||
| }, | ||
| }) | ||
| await hooks.emit('kubb:build:end', { | ||
| files, | ||
| config, | ||
| outputDir: resolve(config.root, config.output.path), | ||
| }) | ||
| return { | ||
| failedPlugins, | ||
| files, | ||
| driver, | ||
| pluginTimings, | ||
| sources, | ||
| } | ||
| } catch (error) { | ||
| return { | ||
| failedPlugins, | ||
| files: [], | ||
| driver, | ||
| pluginTimings, | ||
| error: error as Error, | ||
| sources, | ||
| } | ||
| } finally { | ||
| driver.dispose() | ||
| } | ||
| } | ||
| async function build(setupResult: SetupResult): Promise<BuildOutput> { | ||
| const { files, driver, failedPlugins, pluginTimings, error, sources } = await safeBuild(setupResult) | ||
| if (error) { | ||
| throw error | ||
| } | ||
| if (failedPlugins.size > 0) { | ||
| const errors = [...failedPlugins].map(({ error }) => error) | ||
| throw new BuildError(`Build Error with ${failedPlugins.size} failed plugins`, { errors }) | ||
| } | ||
| return { | ||
| failedPlugins, | ||
| files, | ||
| driver, | ||
| pluginTimings, | ||
| error: undefined, | ||
| sources, | ||
| } | ||
| } | ||
| function inputToAdapterSource(config: Config): AdapterSource { | ||
| if (Array.isArray(config.input)) { | ||
| return { | ||
| type: 'paths', | ||
| paths: config.input.map((i) => (new URLPath(i.path).isURL ? i.path : resolve(config.root, i.path))), | ||
| } | ||
| } | ||
| if ('data' in config.input) { | ||
| return { type: 'data', data: config.input.data } | ||
| } | ||
| if (new URLPath(config.input.path).isURL) { | ||
| return { type: 'path', path: config.input.path } | ||
| } | ||
| const resolved = resolve(config.root, config.input.path) | ||
| return { type: 'path', path: resolved } | ||
| } | ||
| type CreateKubbOptions = { | ||
| hooks?: AsyncEventEmitter<KubbHooks> | ||
| } | ||
| /** | ||
| * Creates a Kubb instance bound to a single config entry. | ||
| * | ||
| * Accepts a user-facing config shape and resolves it to a full {@link Config} during | ||
| * `setup()`. The instance then holds shared state (`hooks`, `sources`, `driver`, `config`) | ||
| * across the `setup → build` lifecycle. Attach event listeners to `kubb.hooks` before | ||
| * calling `setup()` or `build()`. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const kubb = createKubb(userConfig) | ||
| * | ||
| * kubb.hooks.on('kubb:plugin:end', ({ plugin, duration }) => { | ||
| * console.log(`${plugin.name} completed in ${duration}ms`) | ||
| * }) | ||
| * | ||
| * const { files, failedPlugins } = await kubb.safeBuild() | ||
| * ``` | ||
| */ | ||
| export function createKubb(userConfig: UserConfig, options: CreateKubbOptions = {}): Kubb { | ||
| const hooks = options.hooks ?? new AsyncEventEmitter<KubbHooks>() | ||
| let setupResult: SetupResult | undefined | ||
| const instance: Kubb = { | ||
| get hooks() { | ||
| return hooks | ||
| }, | ||
| get sources() { | ||
| return setupResult?.sources ?? new Map() | ||
| }, | ||
| get driver() { | ||
| return setupResult?.driver | ||
| }, | ||
| get config() { | ||
| return setupResult?.config | ||
| }, | ||
| async setup() { | ||
| setupResult = await setup(userConfig, { hooks }) | ||
| }, | ||
| async build() { | ||
| if (!setupResult) { | ||
| await instance.setup() | ||
| } | ||
| return build(setupResult!) | ||
| }, | ||
| async safeBuild() { | ||
| if (!setupResult) { | ||
| await instance.setup() | ||
| } | ||
| return safeBuild(setupResult!) | ||
| }, | ||
| } | ||
| return instance | ||
| } |
| import type { FileNode } from '@kubb/ast' | ||
| /** | ||
| * Minimal interface any Kubb renderer must satisfy. | ||
| * | ||
| * The generic `TElement` is the type of the element the renderer accepts — | ||
| * e.g. `KubbReactElement` for `@kubb/renderer-jsx`, or a custom type for | ||
| * your own renderer. Defaults to `unknown` so that generators which do not | ||
| * care about the element type continue to work without specifying it. | ||
| * | ||
| * This allows core to drive rendering without a hard dependency on | ||
| * `@kubb/renderer-jsx` or any specific renderer implementation. | ||
| */ | ||
| export type Renderer<TElement = unknown> = { | ||
| render(element: TElement): Promise<void> | ||
| unmount(error?: Error | number | null): void | ||
| readonly files: Array<FileNode> | ||
| } | ||
| /** | ||
| * A factory function that produces a fresh {@link Renderer} per render. | ||
| * | ||
| * Generators use this to declare which renderer handles their output. | ||
| */ | ||
| export type RendererFactory<TElement = unknown> = () => Renderer<TElement> | ||
| /** | ||
| * Creates a renderer factory for use in generator definitions. | ||
| * | ||
| * Wrap your renderer factory function with this helper to register it as the | ||
| * renderer for a generator. Core will call this factory once per render cycle | ||
| * to obtain a fresh renderer instance. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // packages/renderer-jsx/src/index.ts | ||
| * export const jsxRenderer = createRenderer(() => { | ||
| * const runtime = new Runtime() | ||
| * return { | ||
| * async render(element) { await runtime.render(element) }, | ||
| * get files() { return runtime.nodes }, | ||
| * unmount(error) { runtime.unmount(error) }, | ||
| * } | ||
| * }) | ||
| * | ||
| * // packages/plugin-zod/src/generators/zodGenerator.tsx | ||
| * import { jsxRenderer } from '@kubb/renderer-jsx' | ||
| * export const zodGenerator = defineGenerator<PluginZod>({ | ||
| * name: 'zod', | ||
| * renderer: jsxRenderer, | ||
| * schema(node, options) { return <File ...>...</File> }, | ||
| * }) | ||
| * ``` | ||
| */ | ||
| export function createRenderer<TElement = unknown>(factory: RendererFactory<TElement>): RendererFactory<TElement> { | ||
| return factory | ||
| } |
| export type Storage = { | ||
| /** | ||
| * Identifier used for logging and debugging (e.g. `'fs'`, `'s3'`). | ||
| */ | ||
| readonly name: string | ||
| /** | ||
| * Returns `true` when an entry for `key` exists in storage. | ||
| */ | ||
| hasItem(key: string): Promise<boolean> | ||
| /** | ||
| * Returns the stored string value, or `null` when `key` does not exist. | ||
| */ | ||
| getItem(key: string): Promise<string | null> | ||
| /** | ||
| * Persists `value` under `key`, creating any required structure. | ||
| */ | ||
| setItem(key: string, value: string): Promise<void> | ||
| /** | ||
| * Removes the entry for `key`. No-ops when the key does not exist. | ||
| */ | ||
| removeItem(key: string): Promise<void> | ||
| /** | ||
| * Returns all keys, optionally filtered to those starting with `base`. | ||
| */ | ||
| getKeys(base?: string): Promise<Array<string>> | ||
| /** | ||
| * Removes all entries, optionally scoped to those starting with `base`. | ||
| */ | ||
| clear(base?: string): Promise<void> | ||
| /** | ||
| * Optional teardown hook called after the build completes. | ||
| */ | ||
| dispose?(): Promise<void> | ||
| } | ||
| /** | ||
| * Factory for implementing custom storage backends that control where generated files are written. | ||
| * | ||
| * Takes a builder function `(options: TOptions) => Storage` and returns a factory `(options?: TOptions) => Storage`. | ||
| * Kubb provides filesystem and in-memory implementations out of the box. | ||
| * | ||
| * @note Call the returned factory with optional options to instantiate the storage adapter. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { createStorage } from '@kubb/core' | ||
| * | ||
| * export const memoryStorage = createStorage(() => { | ||
| * const store = new Map<string, string>() | ||
| * return { | ||
| * name: 'memory', | ||
| * async hasItem(key) { return store.has(key) }, | ||
| * async getItem(key) { return store.get(key) ?? null }, | ||
| * async setItem(key, value) { store.set(key, value) }, | ||
| * async removeItem(key) { store.delete(key) }, | ||
| * async getKeys(base) { | ||
| * const keys = [...store.keys()] | ||
| * return base ? keys.filter((k) => k.startsWith(base)) : keys | ||
| * }, | ||
| * async clear(base) { if (!base) store.clear() }, | ||
| * } | ||
| * }) | ||
| * | ||
| * // Instantiate: | ||
| * const storage = memoryStorage() | ||
| * ``` | ||
| */ | ||
| export function createStorage<TOptions = Record<string, never>>(build: (options: TOptions) => Storage): (options?: TOptions) => Storage { | ||
| return (options) => build(options ?? ({} as TOptions)) | ||
| } |
| import type { PossiblePromise } from '@internals/utils' | ||
| import type { FileNode, OperationNode, SchemaNode } from '@kubb/ast' | ||
| import type { RendererFactory } from './createRenderer.ts' | ||
| import type { GeneratorContext, PluginFactoryOptions } from './types.ts' | ||
| export type { GeneratorContext } from './types.ts' | ||
| /** | ||
| * Declares a named generator unit that walks the AST and emits files. | ||
| * | ||
| * Each method (`schema`, `operation`, `operations`) is called for the matching node type. | ||
| * Each method returns `TElement | Array<FileNode> | void`. JSX-based generators require a `renderer` factory. | ||
| * Return `Array<FileNode>` directly or call `ctx.upsertFile()` manually and return `void` to bypass rendering. | ||
| * | ||
| * @note Generators are consumed by plugins and registered via `ctx.addGenerator()` in `kubb:plugin:setup`. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { defineGenerator } from '@kubb/core' | ||
| * import { jsxRenderer } from '@kubb/renderer-jsx' | ||
| * | ||
| * export const typeGenerator = defineGenerator({ | ||
| * name: 'typescript', | ||
| * renderer: jsxRenderer, | ||
| * schema(node, ctx) { | ||
| * const { adapter, resolver, root, options } = ctx | ||
| * return <File ...><Type node={node} resolver={resolver} /></File> | ||
| * }, | ||
| * }) | ||
| * ``` | ||
| */ | ||
| export type Generator<TOptions extends PluginFactoryOptions = PluginFactoryOptions, TElement = unknown> = { | ||
| /** | ||
| * Used in diagnostic messages and debug output. | ||
| */ | ||
| name: string | ||
| /** | ||
| * Optional renderer factory that produces a {@link Renderer} for each render cycle. | ||
| * | ||
| * Generators that return renderer elements (e.g. JSX via `@kubb/renderer-jsx`) must set this | ||
| * to the matching renderer factory (e.g. `jsxRenderer` from `@kubb/renderer-jsx`). | ||
| * | ||
| * Generators that only return `Array<FileNode>` or `void` do not need to set this. | ||
| * | ||
| * Set `renderer: null` to explicitly opt out of rendering even when the parent plugin | ||
| * declares a `renderer` (overrides the plugin-level fallback). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { jsxRenderer } from '@kubb/renderer-jsx' | ||
| * export const myGenerator = defineGenerator<PluginTs>({ | ||
| * renderer: jsxRenderer, | ||
| * schema(node, ctx) { return <File ...>...</File> }, | ||
| * }) | ||
| * ``` | ||
| */ | ||
| renderer?: RendererFactory<TElement> | null | ||
| /** | ||
| * Called for each schema node in the AST walk. | ||
| * `ctx` carries the plugin context with `adapter` and `inputNode` guaranteed present, | ||
| * plus `ctx.options` with the per-node resolved options (after exclude/include/override). | ||
| */ | ||
| schema?: (node: SchemaNode, ctx: GeneratorContext<TOptions>) => PossiblePromise<TElement | Array<FileNode> | void> | ||
| /** | ||
| * Called for each operation node in the AST walk. | ||
| * `ctx` carries the plugin context with `adapter` and `inputNode` guaranteed present, | ||
| * plus `ctx.options` with the per-node resolved options (after exclude/include/override). | ||
| */ | ||
| operation?: (node: OperationNode, ctx: GeneratorContext<TOptions>) => PossiblePromise<TElement | Array<FileNode> | void> | ||
| /** | ||
| * Called once after all operations have been walked. | ||
| * `ctx` carries the plugin context with `adapter` and `inputNode` guaranteed present, | ||
| * plus `ctx.options` with the plugin-level options for the batch call. | ||
| */ | ||
| operations?: (nodes: Array<OperationNode>, ctx: GeneratorContext<TOptions>) => PossiblePromise<TElement | Array<FileNode> | void> | ||
| } | ||
| /** | ||
| * Defines a generator. Returns the object as-is with correct `this` typings. | ||
| * `applyHookResult` handles renderer elements and `File[]` uniformly using | ||
| * the generator's declared `renderer` factory. | ||
| */ | ||
| export function defineGenerator<TOptions extends PluginFactoryOptions = PluginFactoryOptions, TElement = unknown>( | ||
| generator: Generator<TOptions, TElement>, | ||
| ): Generator<TOptions, TElement> { | ||
| return generator | ||
| } |
| import type { Logger, LoggerOptions, UserLogger } from './types.ts' | ||
| /** | ||
| * Wraps a logger definition into a typed {@link Logger}. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * export const myLogger = defineLogger({ | ||
| * name: 'my-logger', | ||
| * install(context, options) { | ||
| * context.on('kubb:info', (message) => console.log('ℹ', message)) | ||
| * context.on('kubb:error', (error) => console.error('✗', error.message)) | ||
| * }, | ||
| * }) | ||
| * ``` | ||
| */ | ||
| export function defineLogger<Options extends LoggerOptions = LoggerOptions>(logger: UserLogger<Options>): Logger<Options> { | ||
| return logger | ||
| } |
| import type { KubbHooks } from './Kubb.ts' | ||
| /** | ||
| * A middleware instance produced by calling a factory created with `defineMiddleware`. | ||
| * It declares event handlers under a `hooks` object which are registered on the | ||
| * shared emitter after all plugin hooks, so middleware handlers for any event | ||
| * always fire last. | ||
| */ | ||
| export type Middleware = { | ||
| /** | ||
| * Unique identifier for this middleware. | ||
| */ | ||
| name: string | ||
| /** | ||
| * Lifecycle event handlers for this middleware. | ||
| * Any event from the global `KubbHooks` map can be subscribed to here. | ||
| * Handlers are registered after all plugin handlers, so they always fire last. | ||
| */ | ||
| hooks: { | ||
| [K in keyof KubbHooks]?: (...args: KubbHooks[K]) => void | Promise<void> | ||
| } | ||
| } | ||
| /** | ||
| * Creates a middleware factory using the hook-style `hooks` API. | ||
| * | ||
| * Middleware handlers fire after all plugin handlers for any given event, making them ideal for post-processing, logging, and auditing. | ||
| * Per-build state (such as accumulators) belongs inside the factory closure so each `createKubb` invocation gets its own isolated instance. | ||
| * | ||
| * @note The factory can accept typed options. See examples for using options and per-build state patterns. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { defineMiddleware } from '@kubb/core' | ||
| * | ||
| * // Stateless middleware | ||
| * export const logMiddleware = defineMiddleware(() => ({ | ||
| * name: 'log-middleware', | ||
| * hooks: { | ||
| * 'kubb:build:end'({ files }) { | ||
| * console.log(`Build complete with ${files.length} files`) | ||
| * }, | ||
| * }, | ||
| * })) | ||
| * | ||
| * // Middleware with options and per-build state | ||
| * export const prefixMiddleware = defineMiddleware((options: { prefix: string } = { prefix: '' }) => { | ||
| * const seen = new Set<string>() | ||
| * return { | ||
| * name: 'prefix-middleware', | ||
| * hooks: { | ||
| * 'kubb:plugin:end'({ plugin }) { | ||
| * seen.add(`${options.prefix}${plugin.name}`) | ||
| * }, | ||
| * }, | ||
| * } | ||
| * }) | ||
| * ``` | ||
| */ | ||
| export function defineMiddleware<TOptions extends object = object>(factory: (options: TOptions) => Middleware): (options?: TOptions) => Middleware { | ||
| return (options) => factory(options ?? ({} as TOptions)) | ||
| } |
| import type { FileNode } from '@kubb/ast' | ||
| type PrintOptions = { | ||
| extname?: FileNode['extname'] | ||
| } | ||
| export type Parser<TMeta extends object = any> = { | ||
| name: string | ||
| /** | ||
| * File extensions this parser handles. | ||
| * Use `undefined` to create a catch-all fallback parser. | ||
| * | ||
| * @example Handled extensions | ||
| * `['.ts', '.js']` | ||
| */ | ||
| extNames: Array<FileNode['extname']> | undefined | ||
| /** | ||
| * Convert a resolved file to a string. | ||
| */ | ||
| parse(file: FileNode<TMeta>, options?: PrintOptions): Promise<string> | string | ||
| } | ||
| /** | ||
| * Defines a parser with type safety. Creates parsers that transform generated files to strings based on their extension. | ||
| * | ||
| * @note Call the returned factory with optional options to instantiate the parser. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { defineParser } from '@kubb/core' | ||
| * | ||
| * export const jsonParser = defineParser({ | ||
| * name: 'json', | ||
| * extNames: ['.json'], | ||
| * parse(file) { | ||
| * const { extractStringsFromNodes } = await import('@kubb/ast') | ||
| * return file.sources.map((s) => extractStringsFromNodes(s.nodes ?? [])).join('\n') | ||
| * }, | ||
| * }) | ||
| * ``` | ||
| */ | ||
| export function defineParser<TMeta extends object = any>(parser: Parser<TMeta>): Parser<TMeta> { | ||
| return parser | ||
| } |
| import type { KubbHooks } from './Kubb.ts' | ||
| import type { KubbPluginSetupContext, PluginFactoryOptions } from './types.ts' | ||
| /** | ||
| * A plugin object produced by `definePlugin`. | ||
| * Instead of flat lifecycle methods, it groups all handlers under a `hooks:` property | ||
| * (matching Astro's integration naming convention). | ||
| * | ||
| * @template TFactory - The plugin's `PluginFactoryOptions` type. | ||
| */ | ||
| export type Plugin<TFactory extends PluginFactoryOptions = PluginFactoryOptions> = { | ||
| /** | ||
| * Unique name for the plugin, following the same naming convention as `createPlugin`. | ||
| */ | ||
| name: string | ||
| /** | ||
| * Plugins that must be registered before this plugin executes. | ||
| * An error is thrown at startup when any listed dependency is missing. | ||
| */ | ||
| dependencies?: Array<string> | ||
| /** | ||
| * Controls the execution order of this plugin relative to others. | ||
| * | ||
| * - `'pre'` — runs before all normal plugins. | ||
| * - `'post'` — runs after all normal plugins. | ||
| * - `undefined` (default) — runs in declaration order among normal plugins. | ||
| * | ||
| * Dependency constraints always take precedence over `enforce`. | ||
| */ | ||
| enforce?: 'pre' | 'post' | ||
| /** | ||
| * The options passed by the user when calling the plugin factory. | ||
| */ | ||
| options?: TFactory['options'] | ||
| /** | ||
| * Lifecycle event handlers for this plugin. | ||
| * Any event from the global `KubbHooks` map can be subscribed to here. | ||
| */ | ||
| hooks: { | ||
| [K in Exclude<keyof KubbHooks, 'kubb:plugin:setup'>]?: (...args: KubbHooks[K]) => void | Promise<void> | ||
| } & { | ||
| 'kubb:plugin:setup'?(ctx: KubbPluginSetupContext<TFactory>): void | Promise<void> | ||
| } | ||
| } | ||
| /** | ||
| * Returns `true` when `plugin` is a hook-style plugin created with `definePlugin`. | ||
| * | ||
| * Used by `PluginDriver` to distinguish hook-style plugins from legacy `createPlugin` plugins | ||
| * so it can normalize them and register their handlers on the `AsyncEventEmitter`. | ||
| */ | ||
| export function isPlugin(plugin: unknown): plugin is Plugin { | ||
| return typeof plugin === 'object' && plugin !== null && 'hooks' in plugin | ||
| } | ||
| /** | ||
| * Wraps a factory function and returns a typed `Plugin` with lifecycle handlers grouped under `hooks`. | ||
| * | ||
| * Handlers live in a single `hooks` object (inspired by Astro integrations). | ||
| * All lifecycle events from `KubbHooks` are available for subscription. | ||
| * | ||
| * @note For real plugins, use a `PluginFactoryOptions` type parameter to get type-safe context in `kubb:plugin:setup`. | ||
| * Plugin names should follow the convention `plugin-<feature>` (e.g., `plugin-react-query`, `plugin-zod`). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { definePlugin } from '@kubb/core' | ||
| * | ||
| * export const pluginTs = definePlugin((options: { prefix?: string } = {}) => ({ | ||
| * name: 'plugin-ts', | ||
| * hooks: { | ||
| * 'kubb:plugin:setup'(ctx) { | ||
| * ctx.setResolver(resolverTs) | ||
| * }, | ||
| * }, | ||
| * })) | ||
| * ``` | ||
| */ | ||
| export function definePlugin<TFactory extends PluginFactoryOptions = PluginFactoryOptions>( | ||
| factory: (options: TFactory['options']) => Plugin<TFactory>, | ||
| ): (options?: TFactory['options']) => Plugin<TFactory> { | ||
| return (options) => factory(options ?? ({} as TFactory['options'])) | ||
| } |
| import path from 'node:path' | ||
| import { camelCase, pascalCase } from '@internals/utils' | ||
| import type { FileNode, InputNode, Node, OperationNode, SchemaNode } from '@kubb/ast' | ||
| import { createFile, isOperationNode, isSchemaNode } from '@kubb/ast' | ||
| import { PluginDriver } from './PluginDriver.ts' | ||
| import type { | ||
| Config, | ||
| PluginFactoryOptions, | ||
| ResolveBannerContext, | ||
| ResolveOptionsContext, | ||
| Resolver, | ||
| ResolverContext, | ||
| ResolverFileParams, | ||
| ResolverPathParams, | ||
| } from './types.ts' | ||
| /** | ||
| * Builder type for the plugin-specific resolver fields. | ||
| * | ||
| * `default`, `resolveOptions`, `resolvePath`, `resolveFile`, `resolveBanner`, and `resolveFooter` | ||
| * are optional — built-in fallbacks are injected when omitted. | ||
| * | ||
| * The builder receives `ctx` — a reference to the fully assembled resolver — so methods can | ||
| * call sibling resolver methods without using `this`. Because `ctx` is captured by the closure | ||
| * and the resolver is populated after the builder runs, `ctx` correctly reflects any overrides | ||
| * that were applied by the builder itself. | ||
| */ | ||
| type ResolverBuilder<T extends PluginFactoryOptions> = (ctx: T['resolver']) => Omit< | ||
| T['resolver'], | ||
| 'default' | 'resolveOptions' | 'resolvePath' | 'resolveFile' | 'resolveBanner' | 'resolveFooter' | 'name' | 'pluginName' | ||
| > & | ||
| Partial<Pick<T['resolver'], 'default' | 'resolveOptions' | 'resolvePath' | 'resolveFile' | 'resolveBanner' | 'resolveFooter'>> & { | ||
| name: string | ||
| pluginName: T['name'] | ||
| } | ||
| // String patterns are compiled lazily and cached — the same filter is reused for every node. | ||
| const stringPatternCache = new Map<string, RegExp>() | ||
| function testPattern(value: string, pattern: string | RegExp): boolean { | ||
| if (typeof pattern === 'string') { | ||
| let regex = stringPatternCache.get(pattern) | ||
| if (!regex) { | ||
| regex = new RegExp(pattern) | ||
| stringPatternCache.set(pattern, regex) | ||
| } | ||
| return regex.test(value) | ||
| } | ||
| // Use .match() for user-supplied RegExp to preserve semantics regardless of `g`/`y` flags. | ||
| return value.match(pattern) !== null | ||
| } | ||
| /** | ||
| * Checks if an operation matches a pattern for a given filter type (`tag`, `operationId`, `path`, `method`). | ||
| */ | ||
| function matchesOperationPattern(node: OperationNode, type: string, pattern: string | RegExp): boolean { | ||
| switch (type) { | ||
| case 'tag': | ||
| return node.tags.some((tag) => testPattern(tag, pattern)) | ||
| case 'operationId': | ||
| return testPattern(node.operationId, pattern) | ||
| case 'path': | ||
| return testPattern(node.path, pattern) | ||
| case 'method': | ||
| return testPattern(node.method.toLowerCase(), pattern) | ||
| case 'contentType': | ||
| return node.requestBody?.content?.some((c) => testPattern(c.contentType, pattern)) ?? false | ||
| default: | ||
| return false | ||
| } | ||
| } | ||
| /** | ||
| * Checks if a schema matches a pattern for a given filter type (`schemaName`). | ||
| * | ||
| * Returns `null` when the filter type doesn't apply to schemas. | ||
| */ | ||
| function matchesSchemaPattern(node: SchemaNode, type: string, pattern: string | RegExp): boolean | null { | ||
| switch (type) { | ||
| case 'schemaName': | ||
| return node.name ? testPattern(node.name, pattern) : false | ||
| default: | ||
| return null | ||
| } | ||
| } | ||
| /** | ||
| * Default name resolver used by `defineResolver`. | ||
| * | ||
| * - `camelCase` for `function` and `file` types. | ||
| * - `PascalCase` for `type`. | ||
| * - `camelCase` for everything else. | ||
| */ | ||
| function defaultResolver(name: string, type?: 'file' | 'function' | 'type' | 'const'): string { | ||
| let resolvedName = camelCase(name) | ||
| if (type === 'file' || type === 'function') { | ||
| resolvedName = camelCase(name, { | ||
| isFile: type === 'file', | ||
| }) | ||
| } | ||
| if (type === 'type') { | ||
| resolvedName = pascalCase(name) | ||
| } | ||
| return resolvedName | ||
| } | ||
| /** | ||
| * Default option resolver — applies include/exclude filters and merges matching override options. | ||
| * | ||
| * Returns `null` when the node is filtered out by an `exclude` rule or not matched by any `include` rule. | ||
| * | ||
| * @example Include/exclude filtering | ||
| * ```ts | ||
| * const options = defaultResolveOptions(operationNode, { | ||
| * options: { output: 'types' }, | ||
| * exclude: [{ type: 'tag', pattern: 'internal' }], | ||
| * }) | ||
| * // → null when node has tag 'internal' | ||
| * ``` | ||
| * | ||
| * @example Override merging | ||
| * ```ts | ||
| * const options = defaultResolveOptions(operationNode, { | ||
| * options: { enumType: 'asConst' }, | ||
| * override: [{ type: 'operationId', pattern: 'listPets', options: { enumType: 'enum' } }], | ||
| * }) | ||
| * // → { enumType: 'enum' } when operationId matches | ||
| * ``` | ||
| */ | ||
| export function defaultResolveOptions<TOptions>( | ||
| node: Node, | ||
| { options, exclude = [], include, override = [] }: ResolveOptionsContext<TOptions>, | ||
| ): TOptions | null { | ||
| if (isOperationNode(node)) { | ||
| const isExcluded = exclude.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern)) | ||
| if (isExcluded) { | ||
| return null | ||
| } | ||
| if (include && !include.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) { | ||
| return null | ||
| } | ||
| const overrideOptions = override.find(({ type, pattern }) => matchesOperationPattern(node, type, pattern))?.options | ||
| return { ...options, ...overrideOptions } | ||
| } | ||
| if (isSchemaNode(node)) { | ||
| if (exclude.some(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)) { | ||
| return null | ||
| } | ||
| if (include) { | ||
| const results = include.map(({ type, pattern }) => matchesSchemaPattern(node, type, pattern)) | ||
| const applicable = results.filter((r) => r !== null) | ||
| if (applicable.length > 0 && !applicable.includes(true)) { | ||
| return null | ||
| } | ||
| } | ||
| const overrideOptions = override.find(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)?.options | ||
| return { ...options, ...overrideOptions } | ||
| } | ||
| return options | ||
| } | ||
| /** | ||
| * Default path resolver used by `defineResolver`. | ||
| * | ||
| * - Returns the output directory in `single` mode. | ||
| * - Resolves into a tag- or path-based subdirectory when `group` and a `tag`/`path` value are provided. | ||
| * - Falls back to a flat `output/baseName` path otherwise. | ||
| * | ||
| * A custom `group.name` function overrides the default subdirectory naming. | ||
| * For `tag` groups the default is `${camelCase(tag)}Controller`. | ||
| * For `path` groups the default is the first path segment after `/`. | ||
| * | ||
| * @example Flat output | ||
| * ```ts | ||
| * defaultResolvePath({ baseName: 'petTypes.ts' }, { root: '/src', output: { path: 'types' } }) | ||
| * // → '/src/types/petTypes.ts' | ||
| * ``` | ||
| * | ||
| * @example Tag-based grouping | ||
| * ```ts | ||
| * defaultResolvePath( | ||
| * { baseName: 'petTypes.ts', tag: 'pets' }, | ||
| * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } }, | ||
| * ) | ||
| * // → '/src/types/petsController/petTypes.ts' | ||
| * ``` | ||
| * | ||
| * @example Path-based grouping | ||
| * ```ts | ||
| * defaultResolvePath( | ||
| * { baseName: 'petTypes.ts', path: '/pets/list' }, | ||
| * { root: '/src', output: { path: 'types' }, group: { type: 'path' } }, | ||
| * ) | ||
| * // → '/src/types/pets/petTypes.ts' | ||
| * ``` | ||
| * | ||
| * @example Single-file mode | ||
| * ```ts | ||
| * defaultResolvePath( | ||
| * { baseName: 'petTypes.ts', pathMode: 'single' }, | ||
| * { root: '/src', output: { path: 'types' } }, | ||
| * ) | ||
| * // → '/src/types' | ||
| * ``` | ||
| */ | ||
| export function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }: ResolverPathParams, { root, output, group }: ResolverContext): string { | ||
| const mode = pathMode ?? PluginDriver.getMode(path.resolve(root, output.path)) | ||
| if (mode === 'single') { | ||
| return path.resolve(root, output.path) | ||
| } | ||
| let result: string | ||
| if (group && (groupPath || tag)) { | ||
| const groupValue = group.type === 'path' ? groupPath! : tag! | ||
| const defaultName = | ||
| group.type === 'tag' | ||
| ? ({ group: g }: { group: string }) => `${camelCase(g)}Controller` | ||
| : ({ group: g }: { group: string }) => { | ||
| // Strip traversal components (empty, '.', '..') before taking the first meaningful segment. | ||
| // When every segment is a traversal component (e.g. '../../') we fall back to '' so the | ||
| // file is placed directly in the output root — the boundary check below ensures safety. | ||
| const segment = g.split('/').filter((s) => s !== '' && s !== '.' && s !== '..')[0] | ||
| return segment ? camelCase(segment) : '' | ||
| } | ||
| const resolveName = group.name ?? defaultName | ||
| result = path.resolve(root, output.path, resolveName({ group: groupValue }), baseName) | ||
| } else { | ||
| result = path.resolve(root, output.path, baseName) | ||
| } | ||
| // Ensure the resolved path stays within the configured output directory. | ||
| // This prevents path traversal from malicious OpenAPI specs or custom group.name functions. | ||
| // `result === outputDir` is intentionally permitted: it matches single-file mode paths and | ||
| // edge cases where baseName resolves to the output directory itself. | ||
| const outputDir = path.resolve(root, output.path) | ||
| const outputDirWithSep = outputDir.endsWith(path.sep) ? outputDir : `${outputDir}${path.sep}` | ||
| if (result !== outputDir && !result.startsWith(outputDirWithSep)) { | ||
| throw new Error( | ||
| `[Kubb] Resolved path "${result}" is outside the output directory "${outputDir}". ` + | ||
| 'This may indicate a path traversal attempt in the OpenAPI specification or a misconfigured group.name function.', | ||
| ) | ||
| } | ||
| return result | ||
| } | ||
| /** | ||
| * Default file resolver used by `defineResolver`. | ||
| * | ||
| * Resolves a `FileNode` by combining name resolution (`resolver.default`) with | ||
| * path resolution (`resolver.resolvePath`). The resolved file always has empty | ||
| * `sources`, `imports`, and `exports` arrays — consumers populate those separately. | ||
| * | ||
| * In `single` mode the name is omitted and the file sits directly in the output directory. | ||
| * | ||
| * @example Resolve a schema file | ||
| * ```ts | ||
| * const file = defaultResolveFile( | ||
| * { name: 'pet', extname: '.ts' }, | ||
| * { root: '/src', output: { path: 'types' } }, | ||
| * resolver, | ||
| * ) | ||
| * // → { baseName: 'pet.ts', path: '/src/types/pet.ts', sources: [], ... } | ||
| * ``` | ||
| * | ||
| * @example Resolve an operation file with tag grouping | ||
| * ```ts | ||
| * const file = defaultResolveFile( | ||
| * { name: 'listPets', extname: '.ts', tag: 'pets' }, | ||
| * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } }, | ||
| * resolver, | ||
| * ) | ||
| * // → { baseName: 'listPets.ts', path: '/src/types/petsController/listPets.ts', ... } | ||
| * ``` | ||
| */ | ||
| export function defaultResolveFile({ name, extname, tag, path: groupPath }: ResolverFileParams, context: ResolverContext, ctx: Resolver): FileNode { | ||
| const pathMode = PluginDriver.getMode(path.resolve(context.root, context.output.path)) | ||
| const resolvedName = pathMode === 'single' ? '' : ctx.default(name, 'file') | ||
| const baseName = `${resolvedName}${extname}` as FileNode['baseName'] | ||
| const filePath = ctx.resolvePath({ baseName, pathMode, tag, path: groupPath }, context) | ||
| return createFile({ | ||
| path: filePath, | ||
| baseName: path.basename(filePath) as `${string}.${string}`, | ||
| meta: { | ||
| pluginName: ctx.pluginName, | ||
| }, | ||
| sources: [], | ||
| imports: [], | ||
| exports: [], | ||
| }) | ||
| } | ||
| /** | ||
| * Generates the default "Generated by Kubb" banner from config and optional node metadata. | ||
| */ | ||
| export function buildDefaultBanner({ | ||
| title, | ||
| description, | ||
| version, | ||
| config, | ||
| }: { | ||
| title?: string | ||
| description?: string | ||
| version?: string | ||
| config: Config | ||
| }): string { | ||
| try { | ||
| let source = '' | ||
| if (Array.isArray(config.input)) { | ||
| const first = config.input[0] | ||
| if (first && 'path' in first) { | ||
| source = path.basename(first.path) | ||
| } | ||
| } else if ('path' in config.input) { | ||
| source = path.basename(config.input.path) | ||
| } else if ('data' in config.input) { | ||
| source = 'text content' | ||
| } | ||
| let banner = '/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n' | ||
| if (config.output.defaultBanner === 'simple') { | ||
| banner += '*/\n' | ||
| return banner | ||
| } | ||
| if (source) { | ||
| banner += `* Source: ${source}\n` | ||
| } | ||
| if (title) { | ||
| banner += `* Title: ${title}\n` | ||
| } | ||
| if (description) { | ||
| const formattedDescription = description.replace(/\n/gm, '\n* ') | ||
| banner += `* Description: ${formattedDescription}\n` | ||
| } | ||
| if (version) { | ||
| banner += `* OpenAPI spec version: ${version}\n` | ||
| } | ||
| banner += '*/\n' | ||
| return banner | ||
| } catch (_error) { | ||
| return '/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n*/' | ||
| } | ||
| } | ||
| /** | ||
| * Default banner resolver — returns the banner string for a generated file. | ||
| * | ||
| * A user-supplied `output.banner` overrides the default Kubb "Generated by Kubb" notice. | ||
| * When no `output.banner` is set, the Kubb notice is used (including `title` and `version` | ||
| * from the OAS spec when a `node` is provided). | ||
| * | ||
| * - When `output.banner` is a function and `node` is provided, returns `output.banner(node)`. | ||
| * - When `output.banner` is a function and `node` is absent, falls back to the Kubb notice. | ||
| * - When `output.banner` is a string, returns it directly. | ||
| * - When `config.output.defaultBanner` is `false`, returns `undefined`. | ||
| * - Otherwise returns the Kubb "Generated by Kubb" notice. | ||
| * | ||
| * @example String banner overrides default | ||
| * ```ts | ||
| * defaultResolveBanner(undefined, { output: { banner: '// my banner' }, config }) | ||
| * // → '// my banner' | ||
| * ``` | ||
| * | ||
| * @example Function banner with node | ||
| * ```ts | ||
| * defaultResolveBanner(inputNode, { output: { banner: (node) => `// v${node.version}` }, config }) | ||
| * // → '// v3.0.0' | ||
| * ``` | ||
| * | ||
| * @example No user banner — Kubb notice with OAS metadata | ||
| * ```ts | ||
| * defaultResolveBanner(inputNode, { config }) | ||
| * // → '/** Generated by Kubb ... Title: Pet Store ... *\/' | ||
| * ``` | ||
| * | ||
| * @example Disabled default banner | ||
| * ```ts | ||
| * defaultResolveBanner(undefined, { config: { output: { defaultBanner: false }, ...config } }) | ||
| * // → undefined | ||
| * ``` | ||
| */ | ||
| export function defaultResolveBanner(node: InputNode | undefined, { output, config }: ResolveBannerContext): string | undefined { | ||
| if (typeof output?.banner === 'function') { | ||
| return output.banner(node) | ||
| } | ||
| if (typeof output?.banner === 'string') { | ||
| return output.banner | ||
| } | ||
| if (config.output.defaultBanner === false) { | ||
| return undefined | ||
| } | ||
| return buildDefaultBanner({ | ||
| title: node?.meta?.title, | ||
| version: node?.meta?.version, | ||
| config, | ||
| }) | ||
| } | ||
| /** | ||
| * Default footer resolver — returns the footer string for a generated file. | ||
| * | ||
| * - When `output.footer` is a function and `node` is provided, calls it with the node. | ||
| * - When `output.footer` is a function and `node` is absent, returns `undefined`. | ||
| * - When `output.footer` is a string, returns it directly. | ||
| * - Otherwise returns `undefined`. | ||
| * | ||
| * @example String footer | ||
| * ```ts | ||
| * defaultResolveFooter(undefined, { output: { footer: '// end of file' }, config }) | ||
| * // → '// end of file' | ||
| * ``` | ||
| * | ||
| * @example Function footer with node | ||
| * ```ts | ||
| * defaultResolveFooter(inputNode, { output: { footer: (node) => `// ${node.title}` }, config }) | ||
| * // → '// Pet Store' | ||
| * ``` | ||
| */ | ||
| export function defaultResolveFooter(node: InputNode | undefined, { output }: ResolveBannerContext): string | undefined { | ||
| if (typeof output?.footer === 'function') { | ||
| return node ? output.footer(node) : undefined | ||
| } | ||
| if (typeof output?.footer === 'string') { | ||
| return output.footer | ||
| } | ||
| return undefined | ||
| } | ||
| /** | ||
| * Defines a resolver for a plugin, injecting built-in defaults for name casing, | ||
| * include/exclude/override filtering, path resolution, and file construction. | ||
| * | ||
| * All four defaults can be overridden by providing them in the builder function: | ||
| * - `default` — name casing strategy (camelCase / PascalCase) | ||
| * - `resolveOptions` — include/exclude/override filtering | ||
| * - `resolvePath` — output path computation | ||
| * - `resolveFile` — full `FileNode` construction | ||
| * | ||
| * The builder receives `ctx` — a reference to the assembled resolver — so methods can | ||
| * call sibling resolver methods using `ctx` instead of `this`. | ||
| * | ||
| * @example Basic resolver with naming helpers | ||
| * ```ts | ||
| * export const resolver = defineResolver<PluginTs>((ctx) => ({ | ||
| * name: 'default', | ||
| * resolveName(node) { | ||
| * return ctx.default(node.name, 'function') | ||
| * }, | ||
| * resolveTypedName(node) { | ||
| * return ctx.default(node.name, 'type') | ||
| * }, | ||
| * })) | ||
| * ``` | ||
| * | ||
| * @example Override resolvePath for a custom output structure | ||
| * ```ts | ||
| * export const resolver = defineResolver<PluginTs>((_ctx) => ({ | ||
| * name: 'custom', | ||
| * resolvePath({ baseName }, { root, output }) { | ||
| * return path.resolve(root, output.path, 'generated', baseName) | ||
| * }, | ||
| * })) | ||
| * ``` | ||
| * | ||
| * @example Use ctx.default inside a helper | ||
| * ```ts | ||
| * export const resolver = defineResolver<PluginTs>((ctx) => ({ | ||
| * name: 'default', | ||
| * resolveParamName(node, param) { | ||
| * return ctx.default(`${node.operationId} ${param.in} ${param.name}`, 'type') | ||
| * }, | ||
| * })) | ||
| * ``` | ||
| */ | ||
| export function defineResolver<T extends PluginFactoryOptions>(build: ResolverBuilder<T>): T['resolver'] { | ||
| // Create the resolver shell first. When `build(resolver)` executes below, `resolver` is | ||
| // still empty, but methods returned by the builder capture it by reference. By the time | ||
| // those methods are actually called, `Object.assign` will have already populated all | ||
| // properties (including any overrides from the builder itself). | ||
| const resolver = {} as T['resolver'] | ||
| Object.assign(resolver, { | ||
| default: defaultResolver, | ||
| resolveOptions: defaultResolveOptions, | ||
| resolvePath: defaultResolvePath, | ||
| // Wire the default resolveFile implementation with a wrapper that passes resolver as ctx. | ||
| // Unlike other defaults which can be assigned directly, defaultResolveFile requires the | ||
| // resolver as its third parameter. | ||
| resolveFile: (params: ResolverFileParams, context: ResolverContext) => defaultResolveFile(params, context, resolver as Resolver), | ||
| resolveBanner: defaultResolveBanner, | ||
| resolveFooter: defaultResolveFooter, | ||
| // Builder overrides are applied last. Any method in the builder can call | ||
| // ctx.xxx() and will see the fully merged resolver (including its own overrides). | ||
| ...build(resolver), | ||
| }) | ||
| return resolver | ||
| } |
| import type { InputNode } from '@kubb/ast' | ||
| import { deflateSync, inflateSync } from 'fflate' | ||
| import { x } from 'tinyexec' | ||
| import type { DevtoolsOptions } from './types.ts' | ||
| /** | ||
| * Encodes an `InputNode` as a compressed, URL-safe string. | ||
| * | ||
| * The JSON representation is deflate-compressed with {@link deflateSync} before | ||
| * base64url encoding, which typically reduces payload size by 70–80 % and | ||
| * keeps URLs well within browser and server path-length limits. | ||
| * | ||
| * Use {@link decodeAst} to reverse. | ||
| */ | ||
| export function encodeAst(input: InputNode): string { | ||
| const compressed = deflateSync(new TextEncoder().encode(JSON.stringify(input))) | ||
| return Buffer.from(compressed).toString('base64url') | ||
| } | ||
| /** | ||
| * Decodes an `InputNode` from a string produced by {@link encodeAst}. | ||
| * | ||
| * Works in both Node.js and the browser — no streaming APIs required. | ||
| */ | ||
| export function decodeAst(encoded: string): InputNode { | ||
| const bytes = Buffer.from(encoded, 'base64url') | ||
| return JSON.parse(new TextDecoder().decode(inflateSync(bytes))) as InputNode | ||
| } | ||
| /** | ||
| * Constructs the Kubb Studio URL for the given `InputNode`. | ||
| * When `options.ast` is `true`, navigates to the AST inspector (`/ast`). | ||
| * The `input` is encoded and attached as the `?root=` query parameter so Studio | ||
| * can decode and render it without a round-trip to any server. | ||
| */ | ||
| export function getStudioUrl(input: InputNode, studioUrl: string, options: DevtoolsOptions = {}): string { | ||
| const baseUrl = studioUrl.replace(/\/$/, '') | ||
| const path = options.ast ? '/ast' : '' | ||
| return `${baseUrl}${path}?root=${encodeAst(input)}` | ||
| } | ||
| /** | ||
| * Opens the Kubb Studio URL for the given `InputNode` in the default browser — | ||
| * | ||
| * Falls back to printing the URL if the browser cannot be launched. | ||
| */ | ||
| export async function openInStudio(input: InputNode, studioUrl: string, options: DevtoolsOptions = {}): Promise<void> { | ||
| const url = getStudioUrl(input, studioUrl, options) | ||
| const cmd = process.platform === 'win32' ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open' | ||
| const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url] | ||
| try { | ||
| await x(cmd, args) | ||
| } catch { | ||
| console.log(`\n ${url}\n`) | ||
| } | ||
| } |
| import type { FileNode } from '@kubb/ast' | ||
| import { createFile } from '@kubb/ast' | ||
| function mergeFile<TMeta extends object = object>(a: FileNode<TMeta>, b: FileNode<TMeta>): FileNode<TMeta> { | ||
| return { | ||
| ...a, | ||
| // Incoming file (b) takes precedence for banner/footer so that barrel files, | ||
| // which never carry a banner, can clear banners set by plugin-generated files | ||
| // at the same path. | ||
| banner: b.banner, | ||
| footer: b.footer, | ||
| sources: [...(a.sources || []), ...(b.sources || [])], | ||
| imports: [...(a.imports || []), ...(b.imports || [])], | ||
| exports: [...(a.exports || []), ...(b.exports || [])], | ||
| } | ||
| } | ||
| /** | ||
| * Collapses a list of files so that duplicates sharing the same `path` are merged | ||
| * in arrival order. Keeps the original order of first occurrence. | ||
| */ | ||
| function mergeFilesByPath(files: ReadonlyArray<FileNode>): Map<string, FileNode> { | ||
| const merged = new Map<string, FileNode>() | ||
| for (const file of files) { | ||
| const existing = merged.get(file.path) | ||
| merged.set(file.path, existing ? mergeFile(existing, file) : file) | ||
| } | ||
| return merged | ||
| } | ||
| /** | ||
| * In-memory file store for generated files. | ||
| * | ||
| * Files with the same `path` are merged — sources, imports, and exports are concatenated. | ||
| * The `files` getter returns all stored files sorted by path length (shortest first). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { FileManager } from '@kubb/core' | ||
| * | ||
| * const manager = new FileManager() | ||
| * manager.upsert(myFile) | ||
| * console.log(manager.files) // all stored files | ||
| * ``` | ||
| */ | ||
| export class FileManager { | ||
| readonly #cache = new Map<string, FileNode>() | ||
| #filesCache: Array<FileNode> | null = null | ||
| /** | ||
| * Adds one or more files. Incoming files with the same path are merged | ||
| * (sources/imports/exports concatenated), but existing cache entries are | ||
| * replaced — use {@link upsert} when you want to merge into the cache too. | ||
| */ | ||
| add(...files: Array<FileNode>): Array<FileNode> { | ||
| return this.#store(files, false) | ||
| } | ||
| /** | ||
| * Adds or merges one or more files. | ||
| * If a file with the same path already exists in the cache, its | ||
| * sources/imports/exports are merged into the incoming file. | ||
| */ | ||
| upsert(...files: Array<FileNode>): Array<FileNode> { | ||
| return this.#store(files, true) | ||
| } | ||
| #store(files: ReadonlyArray<FileNode>, mergeExisting: boolean): Array<FileNode> { | ||
| const resolvedFiles: Array<FileNode> = [] | ||
| for (const file of mergeFilesByPath(files).values()) { | ||
| const existing = mergeExisting ? this.#cache.get(file.path) : undefined | ||
| const resolvedFile = createFile(existing ? mergeFile(existing, file) : file) | ||
| this.#cache.set(resolvedFile.path, resolvedFile) | ||
| resolvedFiles.push(resolvedFile) | ||
| } | ||
| this.#filesCache = null | ||
| return resolvedFiles | ||
| } | ||
| getByPath(path: string): FileNode | null { | ||
| return this.#cache.get(path) ?? null | ||
| } | ||
| deleteByPath(path: string): void { | ||
| this.#cache.delete(path) | ||
| this.#filesCache = null | ||
| } | ||
| clear(): void { | ||
| this.#cache.clear() | ||
| this.#filesCache = null | ||
| } | ||
| /** | ||
| * All stored files, sorted by path length (shorter paths first). | ||
| */ | ||
| get files(): Array<FileNode> { | ||
| if (this.#filesCache) { | ||
| return this.#filesCache | ||
| } | ||
| this.#filesCache = [...this.#cache.values()].sort((a, b) => { | ||
| const lenDiff = a.path.length - b.path.length | ||
| if (lenDiff !== 0) return lenDiff | ||
| // Within the same length bucket, index.ts barrel files go last so other | ||
| // files are always processed before their barrel file. | ||
| const aIsIndex = a.path.endsWith('/index.ts') || a.path === 'index.ts' | ||
| const bIsIndex = b.path.endsWith('/index.ts') || b.path === 'index.ts' | ||
| if (aIsIndex && !bIsIndex) return 1 | ||
| if (!aIsIndex && bIsIndex) return -1 | ||
| return 0 | ||
| }) | ||
| return this.#filesCache | ||
| } | ||
| } |
| import type { CodeNode, FileNode } from '@kubb/ast' | ||
| import { extractStringsFromNodes } from '@kubb/ast' | ||
| import pLimit from 'p-limit' | ||
| import { PARALLEL_CONCURRENCY_LIMIT } from './constants.ts' | ||
| import type { Parser } from './defineParser.ts' | ||
| type ParseOptions = { | ||
| parsers?: Map<FileNode['extname'], Parser> | ||
| extension?: Record<FileNode['extname'], FileNode['extname'] | ''> | ||
| } | ||
| type RunOptions = ParseOptions & { | ||
| /** | ||
| * @default 'sequential' | ||
| */ | ||
| mode?: 'sequential' | 'parallel' | ||
| onStart?: (files: Array<FileNode>) => Promise<void> | void | ||
| onEnd?: (files: Array<FileNode>) => Promise<void> | void | ||
| onUpdate?: (params: { file: FileNode; source?: string; processed: number; total: number; percentage: number }) => Promise<void> | void | ||
| } | ||
| function joinSources(file: FileNode): string { | ||
| return file.sources | ||
| .map((item) => extractStringsFromNodes(item.nodes as Array<CodeNode>)) | ||
| .filter(Boolean) | ||
| .join('\n\n') | ||
| } | ||
| /** | ||
| * Converts a single file to a string using the registered parsers. | ||
| * Falls back to joining source values when no matching parser is found. | ||
| * | ||
| * @internal | ||
| */ | ||
| export class FileProcessor { | ||
| readonly #limit = pLimit(PARALLEL_CONCURRENCY_LIMIT) | ||
| async parse(file: FileNode, { parsers, extension }: ParseOptions = {}): Promise<string> { | ||
| const parseExtName = extension?.[file.extname] || undefined | ||
| if (!parsers || !file.extname) { | ||
| return joinSources(file) | ||
| } | ||
| const parser = parsers.get(file.extname) | ||
| if (!parser) { | ||
| return joinSources(file) | ||
| } | ||
| return parser.parse(file, { extname: parseExtName }) | ||
| } | ||
| async run(files: Array<FileNode>, { parsers, mode = 'sequential', extension, onStart, onEnd, onUpdate }: RunOptions = {}): Promise<Array<FileNode>> { | ||
| await onStart?.(files) | ||
| const total = files.length | ||
| let processed = 0 | ||
| const processOne = async (file: FileNode) => { | ||
| const source = await this.parse(file, { extension, parsers }) | ||
| const currentProcessed = ++processed | ||
| const percentage = (currentProcessed / total) * 100 | ||
| await onUpdate?.({ | ||
| file, | ||
| source, | ||
| processed: currentProcessed, | ||
| percentage, | ||
| total, | ||
| }) | ||
| } | ||
| if (mode === 'sequential') { | ||
| for (const file of files) { | ||
| await processOne(file) | ||
| } | ||
| } else { | ||
| await Promise.all(files.map((file) => this.#limit(() => processOne(file)))) | ||
| } | ||
| await onEnd?.(files) | ||
| return files | ||
| } | ||
| } |
-20
| export { AsyncEventEmitter, URLPath } from '@internals/utils' | ||
| export * as ast from '@kubb/ast' | ||
| export { logLevel } from './constants.ts' | ||
| export { createAdapter } from './createAdapter.ts' | ||
| export { createKubb } from './createKubb.ts' | ||
| export { createRenderer } from './createRenderer.ts' | ||
| export { createStorage } from './createStorage.ts' | ||
| export { defineGenerator } from './defineGenerator.ts' | ||
| export { defineLogger } from './defineLogger.ts' | ||
| export { defineMiddleware } from './defineMiddleware.ts' | ||
| export { defineParser } from './defineParser.ts' | ||
| export { definePlugin } from './definePlugin.ts' | ||
| export { defineResolver } from './defineResolver.ts' | ||
| export { FileManager } from './FileManager.ts' | ||
| export { FileProcessor } from './FileProcessor.ts' | ||
| export { PluginDriver } from './PluginDriver.ts' | ||
| export { fsStorage } from './storages/fsStorage.ts' | ||
| export { memoryStorage } from './storages/memoryStorage.ts' | ||
| export * from './types.ts' | ||
| export { isInputPath } from './utils/isInputPath.ts' |
-300
| import type { AsyncEventEmitter } from '@internals/utils' | ||
| import type { OperationNode, SchemaNode } from '@kubb/ast' | ||
| import type { BuildOutput } from './createKubb.ts' | ||
| import type { PluginDriver } from './PluginDriver.ts' | ||
| import type { | ||
| Config, | ||
| GeneratorContext, | ||
| KubbBuildEndContext, | ||
| KubbBuildStartContext, | ||
| KubbConfigEndContext, | ||
| KubbDebugContext, | ||
| KubbErrorContext, | ||
| KubbFileProcessingUpdateContext, | ||
| KubbFilesProcessingEndContext, | ||
| KubbFilesProcessingStartContext, | ||
| KubbGenerationEndContext, | ||
| KubbGenerationStartContext, | ||
| KubbGenerationSummaryContext, | ||
| KubbHookEndContext, | ||
| KubbHookStartContext, | ||
| KubbInfoContext, | ||
| KubbLifecycleStartContext, | ||
| KubbPluginEndContext, | ||
| KubbPluginSetupContext, | ||
| KubbPluginStartContext, | ||
| KubbPluginsEndContext, | ||
| KubbSuccessContext, | ||
| KubbVersionNewContext, | ||
| KubbWarnContext, | ||
| } from './types' | ||
| /** | ||
| * Kubb code generation instance returned by {@link createKubb}. | ||
| * | ||
| * Use this when orchestrating multiple builds, inspecting plugin timings, or integrating Kubb into a larger toolchain. | ||
| * For a single one-off build, chain directly: `await createKubb(config).build()`. | ||
| */ | ||
| export type Kubb = { | ||
| /** | ||
| * Shared event emitter for lifecycle and status events. Attach listeners before calling `setup()` or `build()`. | ||
| */ | ||
| readonly hooks: AsyncEventEmitter<KubbHooks> | ||
| /** | ||
| * Generated source code keyed by absolute file path. Available after `build()` or `safeBuild()` completes. | ||
| */ | ||
| readonly sources: Map<string, string> | ||
| /** | ||
| * Plugin driver managing all plugins. Available after `setup()` completes. | ||
| */ | ||
| readonly driver: PluginDriver | undefined | ||
| /** | ||
| * Resolved configuration with defaults applied. Available after `setup()` completes. | ||
| */ | ||
| readonly config: Config | undefined | ||
| /** | ||
| * Resolves config and initializes the driver. `build()` calls this automatically. | ||
| */ | ||
| setup(): Promise<void> | ||
| /** | ||
| * Runs the full pipeline and throws on any plugin error. Automatically calls `setup()` if needed. | ||
| */ | ||
| build(): Promise<BuildOutput> | ||
| /** | ||
| * Runs the full pipeline and captures errors in `BuildOutput` instead of throwing. Automatically calls `setup()` if needed. | ||
| */ | ||
| safeBuild(): Promise<BuildOutput> | ||
| } | ||
| /** | ||
| * Lifecycle events emitted during Kubb code generation. | ||
| * Use these for logging, progress tracking, and custom integrations. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * import type { AsyncEventEmitter } from '@internals/utils' | ||
| * import type { KubbHooks } from '@kubb/core' | ||
| * | ||
| * const hooks: AsyncEventEmitter<KubbHooks> = new AsyncEventEmitter() | ||
| * | ||
| * hooks.on('kubb:lifecycle:start', () => { | ||
| * console.log('Starting Kubb generation') | ||
| * }) | ||
| * | ||
| * hooks.on('kubb:plugin:end', ({ plugin, duration }) => { | ||
| * console.log(`Plugin ${plugin.name} completed in ${duration}ms`) | ||
| * }) | ||
| * ``` | ||
| */ | ||
| export interface KubbHooks { | ||
| /** | ||
| * Fires at the start of the Kubb lifecycle, before code generation begins. | ||
| */ | ||
| 'kubb:lifecycle:start': [ctx: KubbLifecycleStartContext] | ||
| /** | ||
| * Fires at the end of the Kubb lifecycle, after all code generation completes. | ||
| */ | ||
| 'kubb:lifecycle:end': [] | ||
| /** | ||
| * Fires when configuration loading starts. | ||
| */ | ||
| 'kubb:config:start': [] | ||
| /** | ||
| * Fires when configuration loading completes. | ||
| */ | ||
| 'kubb:config:end': [ctx: KubbConfigEndContext] | ||
| /** | ||
| * Fires when code generation starts. | ||
| */ | ||
| 'kubb:generation:start': [ctx: KubbGenerationStartContext] | ||
| /** | ||
| * Fires when code generation completes. | ||
| */ | ||
| 'kubb:generation:end': [ctx: KubbGenerationEndContext] | ||
| /** | ||
| * Fires with a generation summary including summary lines, title, and success status. | ||
| */ | ||
| 'kubb:generation:summary': [ctx: KubbGenerationSummaryContext] | ||
| /** | ||
| * Fires when code formatting starts (e.g., Biome or Prettier). | ||
| */ | ||
| 'kubb:format:start': [] | ||
| /** | ||
| * Fires when code formatting completes. | ||
| */ | ||
| 'kubb:format:end': [] | ||
| /** | ||
| * Fires when linting starts. | ||
| */ | ||
| 'kubb:lint:start': [] | ||
| /** | ||
| * Fires when linting completes. | ||
| */ | ||
| 'kubb:lint:end': [] | ||
| /** | ||
| * Fires when plugin hooks execution starts. | ||
| */ | ||
| 'kubb:hooks:start': [] | ||
| /** | ||
| * Fires when plugin hooks execution completes. | ||
| */ | ||
| 'kubb:hooks:end': [] | ||
| /** | ||
| * Fires when a single hook executes (e.g., format or lint). The callback is invoked when the command finishes. | ||
| */ | ||
| 'kubb:hook:start': [ctx: KubbHookStartContext] | ||
| /** | ||
| * Fires when a single hook execution completes. | ||
| */ | ||
| 'kubb:hook:end': [ctx: KubbHookEndContext] | ||
| /** | ||
| * Fires when a new Kubb version is available. | ||
| */ | ||
| 'kubb:version:new': [ctx: KubbVersionNewContext] | ||
| /** | ||
| * Informational message event. | ||
| */ | ||
| 'kubb:info': [ctx: KubbInfoContext] | ||
| /** | ||
| * Error event, fired when an error occurs during generation. | ||
| */ | ||
| 'kubb:error': [ctx: KubbErrorContext] | ||
| /** | ||
| * Success message event. | ||
| */ | ||
| 'kubb:success': [ctx: KubbSuccessContext] | ||
| /** | ||
| * Warning message event. | ||
| */ | ||
| 'kubb:warn': [ctx: KubbWarnContext] | ||
| /** | ||
| * Debug event for detailed logging with timestamp and optional filename. | ||
| */ | ||
| 'kubb:debug': [ctx: KubbDebugContext] | ||
| /** | ||
| * Fires when file processing starts with the list of files to process. | ||
| */ | ||
| 'kubb:files:processing:start': [ctx: KubbFilesProcessingStartContext] | ||
| /** | ||
| * Fires for each file with progress updates: processed count, total, percentage, and file details. | ||
| */ | ||
| 'kubb:file:processing:update': [ctx: KubbFileProcessingUpdateContext] | ||
| /** | ||
| * Fires when file processing completes with the list of processed files. | ||
| */ | ||
| 'kubb:files:processing:end': [ctx: KubbFilesProcessingEndContext] | ||
| /** | ||
| * Fires when a plugin starts execution. | ||
| */ | ||
| 'kubb:plugin:start': [ctx: KubbPluginStartContext] | ||
| /** | ||
| * Fires when a plugin completes execution. Duration measured in milliseconds. | ||
| */ | ||
| 'kubb:plugin:end': [ctx: KubbPluginEndContext] | ||
| /** | ||
| * Fires once before plugins execute — allowing plugins to register generators, configure resolvers/transformers/renderers, or inject files. | ||
| */ | ||
| 'kubb:plugin:setup': [ctx: KubbPluginSetupContext] | ||
| /** | ||
| * Fires before the plugin execution loop begins. The adapter has already parsed the source and `inputNode` is available. | ||
| */ | ||
| 'kubb:build:start': [ctx: KubbBuildStartContext] | ||
| /** | ||
| * Fires after all plugins run and per-plugin barrels generate, but before files write to disk. | ||
| * Use this to inject final files that must persist in the same write pass as plugin output. | ||
| */ | ||
| 'kubb:plugins:end': [ctx: KubbPluginsEndContext] | ||
| /** | ||
| * Fires after all files write to disk. | ||
| */ | ||
| 'kubb:build:end': [ctx: KubbBuildEndContext] | ||
| /** | ||
| * Fires for each schema node during AST traversal. Generator listeners respond to this. | ||
| */ | ||
| 'kubb:generate:schema': [node: SchemaNode, ctx: GeneratorContext] | ||
| /** | ||
| * Fires for each operation node during AST traversal. Generator listeners respond to this. | ||
| */ | ||
| 'kubb:generate:operation': [node: OperationNode, ctx: GeneratorContext] | ||
| /** | ||
| * Fires once after all operations traverse with the full collected array. Batch generator listeners respond to this. | ||
| */ | ||
| 'kubb:generate:operations': [nodes: Array<OperationNode>, ctx: GeneratorContext] | ||
| } | ||
| declare global { | ||
| namespace Kubb { | ||
| /** | ||
| * Registry that maps plugin names to their `PluginFactoryOptions`. | ||
| * Augment this interface in each plugin's `types.ts` to enable automatic | ||
| * typing for `getPlugin` and `requirePlugin`. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // packages/plugin-ts/src/types.ts | ||
| * declare global { | ||
| * namespace Kubb { | ||
| * interface PluginRegistry { | ||
| * 'plugin-ts': PluginTs | ||
| * } | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| interface PluginRegistry {} | ||
| /** | ||
| * Extension point for root `Config['output']` options. | ||
| * Augment the `output` key in middleware or plugin packages to add extra fields | ||
| * to the global output configuration without touching core types. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // packages/middleware-barrel/src/types.ts | ||
| * declare global { | ||
| * namespace Kubb { | ||
| * interface ConfigOptionsRegistry { | ||
| * output: { | ||
| * barrelType?: import('./types.ts').BarrelType | false | ||
| * } | ||
| * } | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| interface ConfigOptionsRegistry {} | ||
| /** | ||
| * Extension point for per-plugin `Output` options. | ||
| * Augment the `output` key in middleware or plugin packages to add extra fields | ||
| * to the per-plugin output configuration without touching core types. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // packages/middleware-barrel/src/types.ts | ||
| * declare global { | ||
| * namespace Kubb { | ||
| * interface PluginOptionsRegistry { | ||
| * output: { | ||
| * barrelType?: import('./types.ts').BarrelType | false | ||
| * } | ||
| * } | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| interface PluginOptionsRegistry {} | ||
| } | ||
| } |
-178
| import { resolve } from 'node:path' | ||
| import type { FileNode, OperationNode, SchemaNode, Visitor } from '@kubb/ast' | ||
| import { transform } from '@kubb/ast' | ||
| import { FileManager } from './FileManager.ts' | ||
| import { PluginDriver } from './PluginDriver.ts' | ||
| import { applyHookResult } from './renderNode.ts' | ||
| import type { Adapter, AdapterFactoryOptions, Config, Generator, GeneratorContext, NormalizedPlugin, PluginFactoryOptions } from './types.ts' | ||
| /** | ||
| * Creates a minimal `PluginDriver` mock for unit tests. | ||
| */ | ||
| export function createMockedPluginDriver(options: { name?: string; plugin?: NormalizedPlugin; config?: Config } = {}): PluginDriver { | ||
| return { | ||
| config: options?.config ?? { | ||
| root: '.', | ||
| output: { | ||
| path: './path', | ||
| }, | ||
| }, | ||
| getPlugin(_pluginName: string): NormalizedPlugin | undefined { | ||
| return options?.plugin | ||
| }, | ||
| getResolver: (_pluginName: string) => options?.plugin?.resolver, | ||
| fileManager: new FileManager(), | ||
| } as unknown as PluginDriver | ||
| } | ||
| /** | ||
| * Creates a minimal `Adapter` mock for unit tests. | ||
| * `parse` returns an empty `InputNode` by default; override via `options.parse`. | ||
| * `getImports` returns `[]` by default. | ||
| */ | ||
| export function createMockedAdapter<TOptions extends AdapterFactoryOptions = AdapterFactoryOptions>( | ||
| options: { | ||
| name?: TOptions['name'] | ||
| resolvedOptions?: TOptions['resolvedOptions'] | ||
| inputNode?: Adapter<TOptions>['inputNode'] | ||
| parse?: Adapter<TOptions>['parse'] | ||
| getImports?: Adapter<TOptions>['getImports'] | ||
| } = {}, | ||
| ): Adapter<TOptions> { | ||
| const inputNode = options.inputNode ?? null | ||
| return { | ||
| name: (options.name ?? 'oas') as TOptions['name'], | ||
| options: (options.resolvedOptions ?? {}) as TOptions['resolvedOptions'], | ||
| inputNode, | ||
| parse: options.parse ?? (async () => ({ kind: 'Input' as const, schemas: [], operations: [] })), | ||
| getImports: options.getImports ?? ((_node: SchemaNode, _resolve: (schemaName: string) => { name: string; path: string }) => []), | ||
| } as Adapter<TOptions> | ||
| } | ||
| /** | ||
| * Creates a minimal plugin mock for unit tests. | ||
| * | ||
| * @example | ||
| * `const plugin = createMockedPlugin<PluginTs>({ name: '@kubb/plugin-ts', options })` | ||
| */ | ||
| export function createMockedPlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions>(params: { | ||
| name: TOptions['name'] | ||
| options: TOptions['resolvedOptions'] | ||
| resolver?: TOptions['resolver'] | ||
| transformer?: Visitor | ||
| dependencies?: Array<string> | ||
| }): NormalizedPlugin<TOptions> { | ||
| return { | ||
| name: params.name, | ||
| options: params.options, | ||
| resolver: params.resolver, | ||
| transformer: params.transformer, | ||
| dependencies: params.dependencies, | ||
| hooks: {}, | ||
| } as unknown as NormalizedPlugin<TOptions> | ||
| } | ||
| type RenderGeneratorOptions<TOptions extends PluginFactoryOptions> = { | ||
| config: Config | ||
| adapter: Adapter | ||
| driver: PluginDriver | ||
| plugin: NormalizedPlugin<TOptions> | ||
| options: TOptions['resolvedOptions'] | ||
| resolver: TOptions['resolver'] | ||
| } | ||
| function createMockedPluginContext<TOptions extends PluginFactoryOptions>(opts: RenderGeneratorOptions<TOptions>): Omit<GeneratorContext<TOptions>, 'options'> { | ||
| const root = resolve(opts.config.root, opts.config.output.path) | ||
| return { | ||
| config: opts.config, | ||
| root, | ||
| getMode: (output: { path: string }) => PluginDriver.getMode(resolve(root, output.path)), | ||
| adapter: opts.adapter, | ||
| resolver: opts.resolver, | ||
| plugin: opts.plugin, | ||
| driver: opts.driver, | ||
| getResolver: (name: string) => opts.driver.getResolver(name), | ||
| inputNode: { kind: 'Input', schemas: [], operations: [] }, | ||
| addFile: async (...files: Array<FileNode>) => opts.driver.fileManager.add(...files), | ||
| upsertFile: async (...files: Array<FileNode>) => opts.driver.fileManager.upsert(...files), | ||
| hooks: opts.driver.hooks ?? ({} as never), | ||
| warn: (msg: string) => console.warn(msg), | ||
| error: (msg: string) => console.error(msg), | ||
| info: (msg: string) => console.info(msg), | ||
| openInStudio: async () => {}, | ||
| } as unknown as Omit<GeneratorContext<TOptions>, 'options'> | ||
| } | ||
| /** | ||
| * Renders a generator's `schema` method in a test context. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * await renderGeneratorSchema(typeGenerator, node, { config, adapter, driver, plugin, options, resolver }) | ||
| * await matchFiles(driver.fileManager.files) | ||
| * ``` | ||
| */ | ||
| export async function renderGeneratorSchema<TOptions extends PluginFactoryOptions>( | ||
| generator: Generator<TOptions>, | ||
| node: SchemaNode, | ||
| opts: RenderGeneratorOptions<TOptions>, | ||
| ): Promise<void> { | ||
| if (!generator.schema) return | ||
| const context = createMockedPluginContext(opts) | ||
| const transformedNode = opts.plugin.transformer ? transform(node, opts.plugin.transformer) : node | ||
| const result = await generator.schema(transformedNode, { | ||
| ...context, | ||
| options: opts.options, | ||
| }) | ||
| await applyHookResult(result, opts.driver, generator.renderer ?? undefined) | ||
| } | ||
| /** | ||
| * Renders a generator's `operation` method in a test context. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * await renderGeneratorOperation(typeGenerator, node, { config, adapter, driver, plugin, options, resolver }) | ||
| * await matchFiles(driver.fileManager.files) | ||
| * ``` | ||
| */ | ||
| export async function renderGeneratorOperation<TOptions extends PluginFactoryOptions>( | ||
| generator: Generator<TOptions>, | ||
| node: OperationNode, | ||
| opts: RenderGeneratorOptions<TOptions>, | ||
| ): Promise<void> { | ||
| if (!generator.operation) return | ||
| const context = createMockedPluginContext(opts) | ||
| const transformedNode = opts.plugin.transformer ? transform(node, opts.plugin.transformer) : node | ||
| const result = await generator.operation(transformedNode, { | ||
| ...context, | ||
| options: opts.options, | ||
| }) | ||
| await applyHookResult(result, opts.driver, generator.renderer ?? undefined) | ||
| } | ||
| /** | ||
| * Renders a generator's `operations` method in a test context. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * await renderGeneratorOperations(classClientGenerator, nodes, { config, adapter, driver, plugin, options, resolver }) | ||
| * await matchFiles(driver.fileManager.files) | ||
| * ``` | ||
| */ | ||
| export async function renderGeneratorOperations<TOptions extends PluginFactoryOptions>( | ||
| generator: Generator<TOptions>, | ||
| nodes: Array<OperationNode>, | ||
| opts: RenderGeneratorOptions<TOptions>, | ||
| ): Promise<void> { | ||
| if (!generator.operations) return | ||
| const context = createMockedPluginContext(opts) | ||
| const transformedNodes = opts.plugin.transformer ? nodes.map((n) => transform(n, opts.plugin.transformer!)) : nodes | ||
| const result = await generator.operations(transformedNodes, { | ||
| ...context, | ||
| options: opts.options, | ||
| }) | ||
| await applyHookResult(result, opts.driver, generator.renderer ?? undefined) | ||
| } |
| import { extname, resolve } from 'node:path' | ||
| import type { AsyncEventEmitter } from '@internals/utils' | ||
| import type { FileNode, InputNode, OperationNode, SchemaNode } from '@kubb/ast' | ||
| import { createFile } from '@kubb/ast' | ||
| import { DEFAULT_STUDIO_URL } from './constants.ts' | ||
| import type { Generator } from './defineGenerator.ts' | ||
| import type { Plugin } from './definePlugin.ts' | ||
| import { defineResolver } from './defineResolver.ts' | ||
| import { openInStudio as openInStudioFn } from './devtools.ts' | ||
| import { FileManager } from './FileManager.ts' | ||
| import { applyHookResult } from './renderNode.ts' | ||
| import type { | ||
| Adapter, | ||
| Config, | ||
| DevtoolsOptions, | ||
| GeneratorContext, | ||
| KubbHooks, | ||
| KubbPluginSetupContext, | ||
| NormalizedPlugin, | ||
| PluginFactoryOptions, | ||
| Resolver, | ||
| } from './types.ts' | ||
| // inspired by: https://github.com/rollup/rollup/blob/master/src/utils/PluginDriver.ts# | ||
| type Options = { | ||
| hooks: AsyncEventEmitter<KubbHooks> | ||
| } | ||
| function enforceOrder(enforce: 'pre' | 'post' | undefined): number { | ||
| return enforce === 'pre' ? -1 : enforce === 'post' ? 1 : 0 | ||
| } | ||
| export class PluginDriver { | ||
| readonly config: Config | ||
| readonly options: Options | ||
| /** | ||
| * Returns `'single'` when `fileOrFolder` has a file extension, `'split'` otherwise. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * PluginDriver.getMode('src/gen/types.ts') // 'single' | ||
| * PluginDriver.getMode('src/gen/types') // 'split' | ||
| * ``` | ||
| */ | ||
| static getMode(fileOrFolder: string | undefined | null): 'single' | 'split' { | ||
| if (!fileOrFolder) { | ||
| return 'split' | ||
| } | ||
| return extname(fileOrFolder) ? 'single' : 'split' | ||
| } | ||
| /** | ||
| * The universal `@kubb/ast` `InputNode` produced by the adapter, set by | ||
| * the build pipeline after the adapter's `parse()` resolves. | ||
| */ | ||
| inputNode: InputNode | undefined = undefined | ||
| adapter: Adapter | undefined = undefined | ||
| #studioIsOpen = false | ||
| /** | ||
| * Central file store for all generated files. | ||
| * Plugins should use `this.addFile()` / `this.upsertFile()` (via their context) to | ||
| * add files; this property gives direct read/write access when needed. | ||
| */ | ||
| readonly fileManager = new FileManager() | ||
| readonly plugins = new Map<string, NormalizedPlugin>() | ||
| /** | ||
| * Tracks which plugins have generators registered via `addGenerator()` (event-based path). | ||
| * Used by the build loop to decide whether to emit generator events for a given plugin. | ||
| */ | ||
| readonly #pluginsWithEventGenerators = new Set<string>() | ||
| readonly #resolvers = new Map<string, Resolver>() | ||
| readonly #defaultResolvers = new Map<string, Resolver>() | ||
| readonly #hookListeners = new Map<keyof KubbHooks, Set<(...args: never[]) => void | Promise<void>>>() | ||
| constructor(config: Config, options: Options) { | ||
| this.config = config | ||
| this.options = options | ||
| config.plugins | ||
| .map((rawPlugin) => this.#normalizePlugin(rawPlugin as Plugin)) | ||
| .filter((plugin) => { | ||
| if (typeof plugin.apply === 'function') { | ||
| return plugin.apply(config) | ||
| } | ||
| return true | ||
| }) | ||
| .sort((a, b) => { | ||
| if (b.dependencies?.includes(a.name)) return -1 | ||
| if (a.dependencies?.includes(b.name)) return 1 | ||
| // enforce: 'pre' plugins run first, 'post' plugins run last | ||
| return enforceOrder(a.enforce) - enforceOrder(b.enforce) | ||
| }) | ||
| .forEach((plugin) => { | ||
| this.plugins.set(plugin.name, plugin) | ||
| }) | ||
| } | ||
| get hooks() { | ||
| return this.options.hooks | ||
| } | ||
| /** | ||
| * Creates an `NormalizedPlugin` from a hook-style plugin and registers | ||
| * its lifecycle handlers on the `AsyncEventEmitter`. | ||
| */ | ||
| #normalizePlugin(hookPlugin: Plugin): NormalizedPlugin { | ||
| const normalizedPlugin = { | ||
| name: hookPlugin.name, | ||
| dependencies: hookPlugin.dependencies, | ||
| enforce: hookPlugin.enforce, | ||
| options: { output: { path: '.' }, exclude: [], override: [] }, | ||
| } as unknown as NormalizedPlugin | ||
| this.registerPluginHooks(hookPlugin, normalizedPlugin) | ||
| return normalizedPlugin | ||
| } | ||
| /** | ||
| * Registers a hook-style plugin's lifecycle handlers on the shared `AsyncEventEmitter`. | ||
| * | ||
| * For `kubb:plugin:setup`, the registered listener wraps the globally emitted context with a | ||
| * plugin-specific one so that `addGenerator`, `setResolver`, `setTransformer`, and | ||
| * `setRenderer` all target the correct `normalizedPlugin` entry in the plugins map. | ||
| * | ||
| * All other hooks are iterated and registered directly as pass-through listeners. | ||
| * Any event key present in the global `KubbHooks` interface can be subscribed to. | ||
| * | ||
| * External tooling can subscribe to any of these events via `hooks.on(...)` to observe | ||
| * the plugin lifecycle without modifying plugin behavior. | ||
| * | ||
| * @internal | ||
| */ | ||
| registerPluginHooks(hookPlugin: Plugin, normalizedPlugin: NormalizedPlugin): void { | ||
| const { hooks } = hookPlugin | ||
| // kubb:plugin:setup gets special treatment: the globally emitted context is wrapped with | ||
| // plugin-specific implementations so that addGenerator / setResolver / etc. target | ||
| // this plugin's normalizedPlugin entry rather than being no-ops. | ||
| if (hooks['kubb:plugin:setup']) { | ||
| const setupHandler = (globalCtx: KubbPluginSetupContext) => { | ||
| const pluginCtx: KubbPluginSetupContext = { | ||
| ...globalCtx, | ||
| options: hookPlugin.options ?? {}, | ||
| addGenerator: (gen) => { | ||
| this.registerGenerator(normalizedPlugin.name, gen) | ||
| }, | ||
| setResolver: (resolver) => { | ||
| this.setPluginResolver(normalizedPlugin.name, resolver) | ||
| }, | ||
| setTransformer: (visitor) => { | ||
| normalizedPlugin.transformer = visitor | ||
| }, | ||
| setRenderer: (renderer) => { | ||
| normalizedPlugin.renderer = renderer | ||
| }, | ||
| setOptions: (opts) => { | ||
| normalizedPlugin.options = { ...normalizedPlugin.options, ...opts } | ||
| }, | ||
| injectFile: (userFileNode) => { | ||
| this.fileManager.add(createFile(userFileNode)) | ||
| }, | ||
| } | ||
| return hooks['kubb:plugin:setup']!(pluginCtx) | ||
| } | ||
| this.hooks.on('kubb:plugin:setup', setupHandler) | ||
| this.#trackHookListener('kubb:plugin:setup', setupHandler as (...args: never[]) => void | Promise<void>) | ||
| } | ||
| // All other hooks are registered as direct pass-through listeners on the shared emitter. | ||
| for (const [event, handler] of Object.entries(hooks) as Array<[keyof KubbHooks, ((...args: never[]) => void | Promise<void>) | undefined]>) { | ||
| if (event === 'kubb:plugin:setup' || !handler) continue | ||
| this.hooks.on(event, handler as never) | ||
| this.#trackHookListener(event, handler as (...args: never[]) => void | Promise<void>) | ||
| } | ||
| } | ||
| /** | ||
| * Emits the `kubb:plugin:setup` event so that all registered hook-style plugin listeners | ||
| * can configure generators, resolvers, transformers and renderers before `buildStart` runs. | ||
| * | ||
| * Call this once from `safeBuild` before the plugin execution loop begins. | ||
| */ | ||
| async emitSetupHooks(): Promise<void> { | ||
| const noop = () => {} | ||
| await this.hooks.emit('kubb:plugin:setup', { | ||
| config: this.config, | ||
| options: {}, | ||
| addGenerator: noop, | ||
| setResolver: noop, | ||
| setTransformer: noop, | ||
| setRenderer: noop, | ||
| setOptions: noop, | ||
| injectFile: noop, | ||
| updateConfig: noop, | ||
| }) | ||
| } | ||
| /** | ||
| * Registers a generator for the given plugin on the shared event emitter. | ||
| * | ||
| * The generator's `schema`, `operation`, and `operations` methods are registered as | ||
| * listeners on `kubb:generate:schema`, `kubb:generate:operation`, and `kubb:generate:operations` | ||
| * respectively. Each listener is scoped to the owning plugin via a `ctx.plugin.name` check | ||
| * so that generators from different plugins do not cross-fire. | ||
| * | ||
| * The renderer resolution chain is: `generator.renderer → plugin.renderer → config.renderer`. | ||
| * Set `generator.renderer = null` to explicitly opt out of rendering even when the plugin | ||
| * declares a renderer. | ||
| * | ||
| * Call this method inside `addGenerator()` (in `kubb:plugin:setup`) to wire up a generator. | ||
| */ | ||
| registerGenerator(pluginName: string, gen: Generator): void { | ||
| const resolveRenderer = () => { | ||
| const plugin = this.plugins.get(pluginName) | ||
| return gen.renderer === null ? undefined : (gen.renderer ?? plugin?.renderer ?? this.config.renderer) | ||
| } | ||
| if (gen.schema) { | ||
| const schemaHandler = async (node: SchemaNode, ctx: GeneratorContext) => { | ||
| if (ctx.plugin.name !== pluginName) return | ||
| const result = await gen.schema!(node, ctx) | ||
| await applyHookResult(result, this, resolveRenderer()) | ||
| } | ||
| this.hooks.on('kubb:generate:schema', schemaHandler) | ||
| this.#trackHookListener('kubb:generate:schema', schemaHandler as (...args: never[]) => void | Promise<void>) | ||
| } | ||
| if (gen.operation) { | ||
| const operationHandler = async (node: OperationNode, ctx: GeneratorContext) => { | ||
| if (ctx.plugin.name !== pluginName) return | ||
| const result = await gen.operation!(node, ctx) | ||
| await applyHookResult(result, this, resolveRenderer()) | ||
| } | ||
| this.hooks.on('kubb:generate:operation', operationHandler) | ||
| this.#trackHookListener('kubb:generate:operation', operationHandler as (...args: never[]) => void | Promise<void>) | ||
| } | ||
| if (gen.operations) { | ||
| const operationsHandler = async (nodes: Array<OperationNode>, ctx: GeneratorContext) => { | ||
| if (ctx.plugin.name !== pluginName) return | ||
| const result = await gen.operations!(nodes, ctx) | ||
| await applyHookResult(result, this, resolveRenderer()) | ||
| } | ||
| this.hooks.on('kubb:generate:operations', operationsHandler) | ||
| this.#trackHookListener('kubb:generate:operations', operationsHandler as (...args: never[]) => void | Promise<void>) | ||
| } | ||
| this.#pluginsWithEventGenerators.add(pluginName) | ||
| } | ||
| /** | ||
| * Returns `true` when at least one generator was registered for the given plugin | ||
| * via `addGenerator()` in `kubb:plugin:setup` (event-based path). | ||
| * | ||
| * Used by the build loop to decide whether to walk the AST and emit generator events | ||
| * for a plugin that has no static `plugin.generators`. | ||
| */ | ||
| hasRegisteredGenerators(pluginName: string): boolean { | ||
| return this.#pluginsWithEventGenerators.has(pluginName) | ||
| } | ||
| /** | ||
| * Unregisters all plugin lifecycle listeners from the shared event emitter. | ||
| * Called at the end of a build to prevent listener leaks across repeated builds. | ||
| * | ||
| * @internal | ||
| */ | ||
| dispose(): void { | ||
| for (const [event, handlers] of this.#hookListeners) { | ||
| for (const handler of handlers) { | ||
| this.hooks.off(event, handler as never) | ||
| } | ||
| } | ||
| this.#hookListeners.clear() | ||
| this.#pluginsWithEventGenerators.clear() | ||
| } | ||
| #trackHookListener(event: keyof KubbHooks, handler: (...args: never[]) => void | Promise<void>): void { | ||
| let handlers = this.#hookListeners.get(event) | ||
| if (!handlers) { | ||
| handlers = new Set() | ||
| this.#hookListeners.set(event, handlers) | ||
| } | ||
| handlers.add(handler) | ||
| } | ||
| #createDefaultResolver(pluginName: string): Resolver { | ||
| const existingResolver = this.#defaultResolvers.get(pluginName) | ||
| if (existingResolver) { | ||
| return existingResolver | ||
| } | ||
| const resolver = defineResolver<PluginFactoryOptions>((_ctx) => ({ | ||
| name: 'default', | ||
| pluginName, | ||
| })) | ||
| this.#defaultResolvers.set(pluginName, resolver) | ||
| return resolver | ||
| } | ||
| /** | ||
| * Merges `partial` with the plugin's default resolver and stores the result. | ||
| * Also mirrors it onto `plugin.resolver` so callers using `getPlugin(name).resolver` | ||
| * get the up-to-date resolver without going through `getResolver()`. | ||
| */ | ||
| setPluginResolver(pluginName: string, partial: Partial<Resolver>): void { | ||
| const defaultResolver = this.#createDefaultResolver(pluginName) | ||
| const merged = { ...defaultResolver, ...partial } | ||
| this.#resolvers.set(pluginName, merged) | ||
| const plugin = this.plugins.get(pluginName) | ||
| if (plugin) { | ||
| plugin.resolver = merged | ||
| } | ||
| } | ||
| /** | ||
| * Returns the resolver for the given plugin. | ||
| * | ||
| * Resolution order: dynamic resolver set via `setPluginResolver` → static resolver on the | ||
| * plugin → lazily created default resolver (identity name, no path transforms). | ||
| */ | ||
| getResolver<TName extends keyof Kubb.PluginRegistry>(pluginName: TName): Kubb.PluginRegistry[TName]['resolver'] | ||
| getResolver<TResolver extends Resolver = Resolver>(pluginName: string): TResolver | ||
| getResolver(pluginName: string): Resolver { | ||
| return this.#resolvers.get(pluginName) ?? this.plugins.get(pluginName)?.resolver ?? this.#createDefaultResolver(pluginName) | ||
| } | ||
| getContext<TOptions extends PluginFactoryOptions>(plugin: NormalizedPlugin<TOptions>): GeneratorContext<TOptions> & Record<string, unknown> { | ||
| const driver = this | ||
| const baseContext = { | ||
| config: driver.config, | ||
| get root(): string { | ||
| return resolve(driver.config.root, driver.config.output.path) | ||
| }, | ||
| getMode(output: { path: string }): 'single' | 'split' { | ||
| return PluginDriver.getMode(resolve(driver.config.root, driver.config.output.path, output.path)) | ||
| }, | ||
| hooks: driver.hooks, | ||
| plugin, | ||
| getPlugin: driver.getPlugin.bind(driver), | ||
| requirePlugin: driver.requirePlugin.bind(driver), | ||
| getResolver: driver.getResolver.bind(driver), | ||
| driver, | ||
| addFile: async (...files: Array<FileNode>) => { | ||
| driver.fileManager.add(...files) | ||
| }, | ||
| upsertFile: async (...files: Array<FileNode>) => { | ||
| driver.fileManager.upsert(...files) | ||
| }, | ||
| get inputNode(): InputNode | undefined { | ||
| return driver.inputNode | ||
| }, | ||
| get adapter(): Adapter | undefined { | ||
| return driver.adapter | ||
| }, | ||
| get resolver() { | ||
| return driver.getResolver(plugin.name) | ||
| }, | ||
| get transformer() { | ||
| return plugin.transformer | ||
| }, | ||
| warn(message: string) { | ||
| driver.hooks.emit('kubb:warn', { message }) | ||
| }, | ||
| error(error: string | Error) { | ||
| driver.hooks.emit('kubb:error', { error: typeof error === 'string' ? new Error(error) : error }) | ||
| }, | ||
| info(message: string) { | ||
| driver.hooks.emit('kubb:info', { message }) | ||
| }, | ||
| openInStudio(options?: DevtoolsOptions) { | ||
| if (!driver.config.devtools || driver.#studioIsOpen) { | ||
| return | ||
| } | ||
| if (typeof driver.config.devtools !== 'object') { | ||
| throw new Error('Devtools must be an object') | ||
| } | ||
| if (!driver.inputNode || !driver.adapter) { | ||
| throw new Error('adapter is not defined, make sure you have set the parser in kubb.config.ts') | ||
| } | ||
| driver.#studioIsOpen = true | ||
| const studioUrl = driver.config.devtools?.studioUrl ?? DEFAULT_STUDIO_URL | ||
| return openInStudioFn(driver.inputNode, studioUrl, options) | ||
| }, | ||
| } as unknown as GeneratorContext<TOptions> | ||
| return baseContext | ||
| } | ||
| getPlugin<TName extends keyof Kubb.PluginRegistry>(pluginName: TName): Plugin<Kubb.PluginRegistry[TName]> | undefined | ||
| getPlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions>(pluginName: string): Plugin<TOptions> | undefined | ||
| getPlugin(pluginName: string): Plugin | undefined { | ||
| return this.plugins.get(pluginName) | ||
| } | ||
| /** | ||
| * Like `getPlugin` but throws a descriptive error when the plugin is not found. | ||
| */ | ||
| requirePlugin<TName extends keyof Kubb.PluginRegistry>(pluginName: TName): Plugin<Kubb.PluginRegistry[TName]> | ||
| requirePlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions>(pluginName: string): Plugin<TOptions> | ||
| requirePlugin(pluginName: string): Plugin { | ||
| const plugin = this.plugins.get(pluginName) | ||
| if (!plugin) { | ||
| throw new Error(`[kubb] Plugin "${pluginName}" is required but not found. Make sure it is included in your Kubb config.`) | ||
| } | ||
| return plugin | ||
| } | ||
| } |
| import type { FileNode } from '@kubb/ast' | ||
| import type { RendererFactory } from './createRenderer.ts' | ||
| import type { PluginDriver } from './PluginDriver.ts' | ||
| /** | ||
| * Handles the return value of a plugin AST hook or generator method. | ||
| * | ||
| * - Renderer output → rendered via the provided `rendererFactory` (e.g. JSX), files stored in `driver.fileManager` | ||
| * - `Array<FileNode>` → added directly into `driver.fileManager` | ||
| * - `void` / `null` / `undefined` → no-op (plugin handled it via `this.upsertFile`) | ||
| * | ||
| * Pass a `rendererFactory` (e.g. `jsxRenderer` from `@kubb/renderer-jsx`) when the result | ||
| * may be a renderer element. Generators that only return `Array<FileNode>` do not need one. | ||
| */ | ||
| export async function applyHookResult<TElement = unknown>( | ||
| result: TElement | Array<FileNode> | void, | ||
| driver: PluginDriver, | ||
| rendererFactory?: RendererFactory<TElement>, | ||
| ): Promise<void> { | ||
| if (!result) return | ||
| if (Array.isArray(result)) { | ||
| driver.fileManager.upsert(...(result as Array<FileNode>)) | ||
| return | ||
| } | ||
| if (!rendererFactory) { | ||
| return | ||
| } | ||
| const renderer = rendererFactory() | ||
| await renderer.render(result) | ||
| driver.fileManager.upsert(...renderer.files) | ||
| renderer.unmount() | ||
| } |
| import type { Dirent } from 'node:fs' | ||
| import { access, readdir, readFile, rm } from 'node:fs/promises' | ||
| import { join, resolve } from 'node:path' | ||
| import { clean, write } from '@internals/utils' | ||
| import { createStorage } from '../createStorage.ts' | ||
| /** | ||
| * Detects the filesystem error used to indicate that a path does not exist. | ||
| */ | ||
| function isMissingPathError(error: unknown): error is NodeJS.ErrnoException { | ||
| return typeof error === 'object' && error !== null && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT' | ||
| } | ||
| /** | ||
| * Built-in filesystem storage driver. | ||
| * | ||
| * This is the default storage when no `storage` option is configured in the root config. | ||
| * Keys are resolved against `process.cwd()`, so root-relative paths such as | ||
| * `src/gen/api/getPets.ts` are written to the correct location without extra configuration. | ||
| * | ||
| * Internally uses the `write` utility from `@internals/utils`, which: | ||
| * - trims leading/trailing whitespace before writing | ||
| * - skips the write when file content is already identical (deduplication) | ||
| * - creates missing parent directories automatically | ||
| * - supports Bun's native file API when running under Bun | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { fsStorage } from '@kubb/core' | ||
| * import { defineConfig } from 'kubb' | ||
| * | ||
| * export default defineConfig({ | ||
| * input: { path: './petStore.yaml' }, | ||
| * output: { path: './src/gen' }, | ||
| * storage: fsStorage(), | ||
| * }) | ||
| * ``` | ||
| */ | ||
| export const fsStorage = createStorage(() => ({ | ||
| name: 'fs', | ||
| async hasItem(key: string) { | ||
| try { | ||
| await access(resolve(key)) | ||
| return true | ||
| } catch (error) { | ||
| if (isMissingPathError(error)) { | ||
| return false | ||
| } | ||
| throw new Error(`Failed to access storage item "${key}"`, { | ||
| cause: error as Error, | ||
| }) | ||
| } | ||
| }, | ||
| async getItem(key: string) { | ||
| try { | ||
| return await readFile(resolve(key), 'utf8') | ||
| } catch (error) { | ||
| if (isMissingPathError(error)) { | ||
| return null | ||
| } | ||
| throw new Error(`Failed to read storage item "${key}"`, { | ||
| cause: error as Error, | ||
| }) | ||
| } | ||
| }, | ||
| async setItem(key: string, value: string) { | ||
| await write(resolve(key), value, { sanity: false }) | ||
| }, | ||
| async removeItem(key: string) { | ||
| await rm(resolve(key), { force: true }) | ||
| }, | ||
| async getKeys(base?: string) { | ||
| const keys: Array<string> = [] | ||
| const resolvedBase = resolve(base ?? process.cwd()) | ||
| async function walk(dir: string, prefix: string): Promise<void> { | ||
| let entries: Array<Dirent> | ||
| try { | ||
| entries = (await readdir(dir, { | ||
| withFileTypes: true, | ||
| })) as Array<Dirent> | ||
| } catch (error) { | ||
| if (isMissingPathError(error)) { | ||
| return | ||
| } | ||
| throw new Error(`Failed to list storage keys under "${resolvedBase}"`, { | ||
| cause: error as Error, | ||
| }) | ||
| } | ||
| for (const entry of entries) { | ||
| const rel = prefix ? `${prefix}/${entry.name}` : entry.name | ||
| if (entry.isDirectory()) { | ||
| await walk(join(dir, entry.name), rel) | ||
| } else { | ||
| keys.push(rel) | ||
| } | ||
| } | ||
| } | ||
| await walk(resolvedBase, '') | ||
| return keys | ||
| }, | ||
| async clear(base?: string) { | ||
| if (!base) { | ||
| return | ||
| } | ||
| await clean(resolve(base)) | ||
| }, | ||
| })) |
| import { createStorage } from '../createStorage.ts' | ||
| /** | ||
| * In-memory storage driver. Useful for testing and dry-run scenarios where | ||
| * generated output should be captured without touching the filesystem. | ||
| * | ||
| * All data lives in a `Map` scoped to the storage instance and is discarded | ||
| * when the instance is garbage-collected. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { memoryStorage } from '@kubb/core' | ||
| * import { defineConfig } from 'kubb' | ||
| * | ||
| * export default defineConfig({ | ||
| * input: { path: './petStore.yaml' }, | ||
| * output: { path: './src/gen' }, | ||
| * storage: memoryStorage(), | ||
| * }) | ||
| * ``` | ||
| */ | ||
| export const memoryStorage = createStorage(() => { | ||
| const store = new Map<string, string>() | ||
| return { | ||
| name: 'memory', | ||
| async hasItem(key: string) { | ||
| return store.has(key) | ||
| }, | ||
| async getItem(key: string) { | ||
| return store.get(key) ?? null | ||
| }, | ||
| async setItem(key: string, value: string) { | ||
| store.set(key, value) | ||
| }, | ||
| async removeItem(key: string) { | ||
| store.delete(key) | ||
| }, | ||
| async getKeys(base?: string) { | ||
| const keys = [...store.keys()] | ||
| return base ? keys.filter((k) => k.startsWith(base)) : keys | ||
| }, | ||
| async clear(base?: string) { | ||
| if (!base) { | ||
| store.clear() | ||
| return | ||
| } | ||
| for (const key of store.keys()) { | ||
| if (key.startsWith(base)) { | ||
| store.delete(key) | ||
| } | ||
| } | ||
| }, | ||
| } | ||
| }) |
-1296
| import type { AsyncEventEmitter, PossiblePromise } from '@internals/utils' | ||
| import type { FileNode, HttpMethod, ImportNode, InputNode, Node, SchemaNode, UserFileNode, Visitor } from '@kubb/ast' | ||
| import type { DEFAULT_STUDIO_URL, logLevel } from './constants.ts' | ||
| import type { RendererFactory } from './createRenderer.ts' | ||
| import type { Storage } from './createStorage.ts' | ||
| import type { Generator } from './defineGenerator.ts' | ||
| import type { Middleware } from './defineMiddleware.ts' | ||
| import type { Parser } from './defineParser.ts' | ||
| import type { Plugin } from './definePlugin.ts' | ||
| import type { KubbHooks } from './Kubb.ts' | ||
| import type { PluginDriver } from './PluginDriver.ts' | ||
| export type { Renderer, RendererFactory } from './createRenderer.ts' | ||
| /** | ||
| * Safely extracts a type from a registry, returning `{}` if the key doesn't exist. | ||
| * Enables optional interface augmentation for `Kubb.ConfigOptionsRegistry` and `Kubb.PluginOptionsRegistry` | ||
| * without requiring changes to core. | ||
| * | ||
| * @internal | ||
| */ | ||
| type ExtractRegistryKey<T, K extends PropertyKey> = K extends keyof T ? T[K] : {} | ||
| /** | ||
| * Reference to an input file to generate code from. | ||
| * | ||
| * Specify an absolute path or a path relative to the config file location. | ||
| * The adapter will parse this file (e.g., OpenAPI YAML or JSON) into the universal AST. | ||
| */ | ||
| export type InputPath = { | ||
| /** | ||
| * Path to your Swagger/OpenAPI file, absolute or relative to the config file location. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * { path: './petstore.yaml' } | ||
| * { path: '/absolute/path/to/openapi.json' } | ||
| * ``` | ||
| */ | ||
| path: string | ||
| } | ||
| /** | ||
| * Inline input data to generate code from. | ||
| * | ||
| * Useful when you want to pass the specification directly instead of from a file. | ||
| * Can be a string (YAML/JSON) or a parsed object. | ||
| */ | ||
| export type InputData = { | ||
| /** | ||
| * Swagger/OpenAPI data as a string (YAML/JSON) or a parsed object. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * { data: fs.readFileSync('./openapi.yaml', 'utf8') } | ||
| * { data: { openapi: '3.1.0', info: { ... } } } | ||
| * ``` | ||
| */ | ||
| data: string | unknown | ||
| } | ||
| type Input = InputPath | InputData | ||
| /** | ||
| * Source data passed to an adapter's `parse` function. | ||
| * Mirrors the config input shape with paths resolved to absolute. | ||
| */ | ||
| export type AdapterSource = { type: 'path'; path: string } | { type: 'data'; data: string | unknown } | { type: 'paths'; paths: Array<string> } | ||
| /** | ||
| * Generic type parameters for an adapter definition. | ||
| * | ||
| * - `TName` — unique identifier (e.g. `'oas'`, `'asyncapi'`) | ||
| * - `TOptions` — user-facing options passed to the adapter factory | ||
| * - `TResolvedOptions` — options after defaults applied | ||
| * - `TDocument` — type of the parsed source document | ||
| */ | ||
| export type AdapterFactoryOptions< | ||
| TName extends string = string, | ||
| TOptions extends object = object, | ||
| TResolvedOptions extends object = TOptions, | ||
| TDocument = unknown, | ||
| > = { | ||
| name: TName | ||
| options: TOptions | ||
| resolvedOptions: TResolvedOptions | ||
| document: TDocument | ||
| } | ||
| /** | ||
| * Adapter that converts input files or data into an `InputNode`. | ||
| * | ||
| * Adapters parse different schema formats (OpenAPI, AsyncAPI, Drizzle, etc.) into Kubb's | ||
| * universal intermediate representation that all plugins consume. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { adapterOas } from '@kubb/adapter-oas' | ||
| * | ||
| * export default defineConfig({ | ||
| * adapter: adapterOas(), | ||
| * input: { path: './openapi.yaml' }, | ||
| * plugins: [pluginTs(), pluginZod()], | ||
| * }) | ||
| * ``` | ||
| */ | ||
| export type Adapter<TOptions extends AdapterFactoryOptions = AdapterFactoryOptions> = { | ||
| /** | ||
| * Human-readable adapter identifier (e.g. `'oas'`, `'asyncapi'`). | ||
| */ | ||
| name: TOptions['name'] | ||
| /** | ||
| * Resolved adapter options after defaults have been applied. | ||
| */ | ||
| options: TOptions['resolvedOptions'] | ||
| /** | ||
| * Parsed source document after the first `parse()` call. `null` before parsing. | ||
| */ | ||
| document: TOptions['document'] | null | ||
| inputNode: InputNode | null | ||
| /** | ||
| * Parse the source into a universal `InputNode`. | ||
| */ | ||
| parse: (source: AdapterSource) => PossiblePromise<InputNode> | ||
| /** | ||
| * Extract `ImportNode` entries for a schema tree. | ||
| * Returns an empty array before the first `parse()` call. | ||
| * | ||
| * The `resolve` callback receives the collision-corrected schema name and must | ||
| * return `{ name, path }` for the import, or `undefined` to skip it. | ||
| */ | ||
| getImports: (node: SchemaNode, resolve: (schemaName: string) => { name: string; path: string }) => Array<ImportNode> | ||
| } | ||
| export type DevtoolsOptions = { | ||
| /** | ||
| * Open the AST inspector in Kubb Studio (`/ast`). Defaults to the main Studio page. | ||
| * @default false | ||
| */ | ||
| ast?: boolean | ||
| } | ||
| /** | ||
| * Build configuration for Kubb code generation. | ||
| * | ||
| * The Config is the main entry point for customizing how Kubb generates code. It specifies: | ||
| * - What to generate from (adapter + input) | ||
| * - Where to output generated code (output) | ||
| * - How to generate (plugins + middleware) | ||
| * - Runtime details (parsers, storage, renderer) | ||
| * | ||
| * See `UserConfig` for a relaxed version with sensible defaults. | ||
| * | ||
| * @private | ||
| */ | ||
| export type Config<TInput = Input> = { | ||
| /** | ||
| * Display name for this configuration in CLI output and logs. | ||
| * Useful when running multiple builds with `defineConfig` arrays. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * name: 'api-client' | ||
| * ``` | ||
| */ | ||
| name?: string | ||
| /** | ||
| * Project root directory, absolute or relative to the config file. | ||
| * @default process.cwd() | ||
| */ | ||
| root: string | ||
| /** | ||
| * Parsers that convert generated files to strings. | ||
| * Each parser handles specific extensions (e.g. `.ts`, `.tsx`). | ||
| * A fallback parser is appended for unhandled extensions. | ||
| * When omitted, defaults to `parserTs` from `@kubb/parser-ts`. | ||
| * | ||
| * @default [parserTs] from `@kubb/parser-ts` | ||
| * @example | ||
| * ```ts | ||
| * import { parserTs, tsxParser } from '@kubb/parser-ts' | ||
| * export default defineConfig({ | ||
| * parsers: [parserTs, tsxParser], | ||
| * }) | ||
| * ``` | ||
| */ | ||
| parsers: Array<Parser> | ||
| /** | ||
| * Adapter that parses input files into the universal `InputNode` representation. | ||
| * Use `@kubb/adapter-oas` for OpenAPI/Swagger or `@kubb/adapter-asyncapi` for other formats. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { adapterOas } from '@kubb/adapter-oas' | ||
| * export default defineConfig({ | ||
| * adapter: adapterOas(), | ||
| * input: { path: './petstore.yaml' }, | ||
| * }) | ||
| * ``` | ||
| */ | ||
| adapter: Adapter | ||
| /** | ||
| * Source file or data to generate code from. | ||
| * Use `input.path` for a file path or `input.data` for inline data. | ||
| */ | ||
| input: TInput | ||
| output: { | ||
| /** | ||
| * Output directory for generated files, absolute or relative to `root`. | ||
| * | ||
| * All generated files will be written under this directory. Subdirectories can be created | ||
| * by plugins based on grouping strategy (by tag, path, etc.). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * output: { | ||
| * path: './src/gen', // generates ./src/gen/api.ts, ./src/gen/types.ts, etc. | ||
| * } | ||
| * ``` | ||
| */ | ||
| path: string | ||
| /** | ||
| * Remove all files from the output directory before starting the build. | ||
| * | ||
| * Useful to ensure old generated files aren't mixed with new ones. | ||
| * Set to `true` for fresh builds, `false` to preserve manual edits in output dir. | ||
| * | ||
| * @default false | ||
| * @example | ||
| * ```ts | ||
| * clean: true // wipes ./src/gen/* before generating | ||
| * ``` | ||
| */ | ||
| clean?: boolean | ||
| /** | ||
| * Persists generated files to the file system. | ||
| * | ||
| * @default true | ||
| * @deprecated Use `storage` option to control where files are written instead. | ||
| */ | ||
| write?: boolean | ||
| /** | ||
| * Auto-format generated files after code generation completes. | ||
| * | ||
| * Applies a code formatter to all generated files. Use `'auto'` to detect which formatter | ||
| * is available on your system. Pass `false` to skip formatting (useful for CI or specific workflows). | ||
| * | ||
| * @default false | ||
| * @example | ||
| * ```ts | ||
| * format: 'auto' // auto-detect prettier, biome, or oxfmt | ||
| * format: 'prettier' // force prettier | ||
| * format: false // skip formatting | ||
| * ``` | ||
| */ | ||
| format?: 'auto' | 'prettier' | 'biome' | 'oxfmt' | false | ||
| /** | ||
| * Auto-lint generated files after code generation completes. | ||
| * | ||
| * Analyzes all generated files for style/correctness issues. Use `'auto'` to detect which linter | ||
| * is available on your system. Pass `false` to skip linting. | ||
| * | ||
| * @default false | ||
| * @example | ||
| * ```ts | ||
| * lint: 'auto' // auto-detect oxlint, biome, or eslint | ||
| * lint: 'eslint' // force eslint | ||
| * lint: false // skip linting | ||
| * ``` | ||
| */ | ||
| lint?: 'auto' | 'eslint' | 'biome' | 'oxlint' | false | ||
| /** | ||
| * Map file extensions to different output extensions. | ||
| * | ||
| * Useful when you want generated `.ts` imports to reference `.js` files or vice versa (e.g., for ESM dual packages). | ||
| * Keys are the original extension, values are the output extension. Use empty string `''` to omit extension. | ||
| * | ||
| * @default { '.ts': '.ts' } | ||
| * @example | ||
| * ```ts | ||
| * extension: { '.ts': '.js' } // generates import './api.js' instead of './api.ts' | ||
| * extension: { '.ts': '', '.tsx': '.jsx' } | ||
| * ``` | ||
| */ | ||
| extension?: Record<FileNode['extname'], FileNode['extname'] | ''> | ||
| /** | ||
| * Banner text prepended to every generated file. | ||
| * | ||
| * Useful for auto-generation notices or license headers. Choose a preset or write custom text. | ||
| * Use `'simple'` for a basic Kubb banner, `'full'` for detailed metadata, or `false` to omit. | ||
| * | ||
| * @default 'simple' | ||
| * @example | ||
| * ```ts | ||
| * defaultBanner: 'simple' // "This file was autogenerated by Kubb" | ||
| * defaultBanner: 'full' // adds source, title, description, API version | ||
| * defaultBanner: false // no banner | ||
| * ``` | ||
| */ | ||
| defaultBanner?: 'simple' | 'full' | false | ||
| /** | ||
| * When `true`, overwrites existing files. When `false`, skips generated files that already exist. | ||
| * | ||
| * Individual plugins can override this setting. This is useful for preventing accidental data loss | ||
| * when re-generating while you have local edits in the output folder. | ||
| * | ||
| * @default false | ||
| * @example | ||
| * ```ts | ||
| * override: true // regenerate everything, even existing files | ||
| * override: false // skip files that already exist | ||
| * ``` | ||
| */ | ||
| override?: boolean | ||
| } & ExtractRegistryKey<Kubb.ConfigOptionsRegistry, 'output'> | ||
| /** | ||
| * Storage backend that controls where and how generated files are persisted. | ||
| * | ||
| * Defaults to `fsStorage()` which writes to the file system. Pass `memoryStorage()` to keep files in RAM, | ||
| * or implement a custom `Storage` interface to write to cloud storage, databases, or other backends. | ||
| * | ||
| * @default fsStorage() | ||
| * @example | ||
| * ```ts | ||
| * import { memoryStorage } from '@kubb/core' | ||
| * | ||
| * // Keep generated files in memory (useful for testing, CI pipelines) | ||
| * storage: memoryStorage() | ||
| * | ||
| * // Use custom S3 storage | ||
| * storage: myS3Storage() | ||
| * ``` | ||
| * | ||
| * @see {@link Storage} interface for implementing custom backends. | ||
| */ | ||
| storage?: Storage | ||
| /** | ||
| * Plugins that execute during the build to generate code and transform the AST. | ||
| * | ||
| * Each plugin processes the AST produced by the adapter and can emit files for different | ||
| * programming languages or formats (TypeScript, Zod schemas, Faker data, etc.). | ||
| * Dependencies are enforced — an error is thrown if a plugin requires another plugin that isn't registered. | ||
| * | ||
| * Plugins can declare their own options via `PluginFactoryOptions`. See plugin documentation for details. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { pluginTs } from '@kubb/plugin-ts' | ||
| * import { pluginZod } from '@kubb/plugin-zod' | ||
| * | ||
| * plugins: [ | ||
| * pluginTs({ output: { path: './src/gen' } }), | ||
| * pluginZod({ output: { path: './src/gen' } }), | ||
| * ] | ||
| * ``` | ||
| */ | ||
| plugins: Array<Plugin> | ||
| /** | ||
| * Middleware instances that observe build events and post-process generated code. | ||
| * | ||
| * Middleware fires AFTER all plugins for each event. Perfect for tasks like: | ||
| * - Auditing what was generated | ||
| * - Adding barrel/index files | ||
| * - Validating output | ||
| * - Running custom transformations | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { middlewareBarrel } from '@kubb/middleware-barrel' | ||
| * | ||
| * middleware: [middlewareBarrel()] | ||
| * ``` | ||
| * | ||
| * @see {@link defineMiddleware} to create custom middleware. | ||
| */ | ||
| middleware?: Array<Middleware> | ||
| /** | ||
| * Default renderer factory used by all plugins and generators. | ||
| * Resolution chain: `generator.renderer` → `plugin.renderer` → `config.renderer` → `undefined` (raw FileNode[] mode). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { jsxRenderer } from '@kubb/renderer-jsx' | ||
| * export default defineConfig({ | ||
| * renderer: jsxRenderer, | ||
| * plugins: [pluginTs(), pluginZod()], | ||
| * }) | ||
| * ``` | ||
| */ | ||
| /** | ||
| * Renderer that converts generated AST nodes to code strings. | ||
| * | ||
| * By default, Kubb uses the JSX renderer (`rendererJsx`). Pass a custom renderer to support | ||
| * different output formats (template engines, code generation DSLs, etc.). | ||
| * | ||
| * @default rendererJsx() // from @kubb/renderer-jsx | ||
| * @example | ||
| * ```ts | ||
| * import { rendererJsx } from '@kubb/renderer-jsx' | ||
| * renderer: rendererJsx() | ||
| * ``` | ||
| * | ||
| * @see {@link Renderer} to implement a custom renderer. | ||
| */ | ||
| renderer?: RendererFactory | ||
| /** | ||
| * Kubb Studio cloud integration settings. | ||
| * | ||
| * Kubb Studio (https://studio.kubb.dev) is a web-based IDE for managing API specs and generated code. | ||
| * Set to `true` to enable with default settings, or pass an object to customize the Studio URL. | ||
| * | ||
| * @default false // disabled by default | ||
| * @example | ||
| * ```ts | ||
| * devtools: true // use default Kubb Studio | ||
| * devtools: { studioUrl: 'https://my-studio.dev' } // custom Studio instance | ||
| * ``` | ||
| */ | ||
| devtools?: | ||
| | true | ||
| | { | ||
| /** | ||
| * Override the Kubb Studio base URL. | ||
| * @default 'https://studio.kubb.dev' | ||
| */ | ||
| studioUrl?: typeof DEFAULT_STUDIO_URL | (string & {}) | ||
| } | ||
| /** | ||
| * Lifecycle hooks that execute during or after the build process. | ||
| * | ||
| * Hooks allow you to run external tools (prettier, eslint, custom scripts) based on build events. | ||
| * Currently supports the `done` hook which fires after all plugins and middleware complete. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * hooks: { | ||
| * done: 'prettier --write "./src/gen"', // auto-format generated files | ||
| * // or multiple commands: | ||
| * done: ['prettier --write "./src/gen"', 'eslint --fix "./src/gen"'] | ||
| * } | ||
| * ``` | ||
| */ | ||
| hooks?: { | ||
| /** | ||
| * Command(s) to run after all plugins and middleware complete generation. | ||
| * | ||
| * Useful for post-processing: formatting, linting, copying files, or custom validation. | ||
| * Pass a single command string or array of command strings to run sequentially. | ||
| * Commands are executed relative to the `root` directory. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * done: 'prettier --write "./src/gen"' | ||
| * done: ['prettier --write "./src/gen"', 'eslint --fix "./src/gen"'] | ||
| * ``` | ||
| */ | ||
| done?: string | Array<string> | ||
| } | ||
| } | ||
| // plugin | ||
| /** | ||
| * Type/string pattern filter for include/exclude/override matching. | ||
| */ | ||
| type PatternFilter = { | ||
| type: string | ||
| pattern: string | RegExp | ||
| } | ||
| /** | ||
| * Pattern filter with partial option overrides applied when the pattern matches. | ||
| */ | ||
| type PatternOverride<TOptions> = PatternFilter & { | ||
| options: Omit<Partial<TOptions>, 'override'> | ||
| } | ||
| /** | ||
| * Context for resolving filtered options for a given operation or schema node. | ||
| * | ||
| * @internal | ||
| */ | ||
| export type ResolveOptionsContext<TOptions> = { | ||
| options: TOptions | ||
| exclude?: Array<PatternFilter> | ||
| include?: Array<PatternFilter> | ||
| override?: Array<PatternOverride<TOptions>> | ||
| } | ||
| /** | ||
| * Base constraint for all plugin resolver objects. | ||
| * | ||
| * `default`, `resolveOptions`, `resolvePath`, `resolveFile`, `resolveBanner`, and `resolveFooter` | ||
| * are injected automatically by `defineResolver` — extend this type to add custom resolution methods. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * type MyResolver = Resolver & { | ||
| * resolveName(node: SchemaNode): string | ||
| * resolveTypedName(node: SchemaNode): string | ||
| * } | ||
| * ``` | ||
| */ | ||
| export type Resolver = { | ||
| name: string | ||
| pluginName: Plugin['name'] | ||
| default(name: string, type?: 'file' | 'function' | 'type' | 'const'): string | ||
| resolveOptions<TOptions>(node: Node, context: ResolveOptionsContext<TOptions>): TOptions | null | ||
| resolvePath(params: ResolverPathParams, context: ResolverContext): string | ||
| resolveFile(params: ResolverFileParams, context: ResolverContext): FileNode | ||
| resolveBanner(node: InputNode | null, context: ResolveBannerContext): string | undefined | ||
| resolveFooter(node: InputNode | null, context: ResolveBannerContext): string | undefined | ||
| } | ||
| export type PluginFactoryOptions< | ||
| /** | ||
| * Unique plugin name. | ||
| */ | ||
| TName extends string = string, | ||
| /** | ||
| * User-facing plugin options. | ||
| */ | ||
| TOptions extends object = object, | ||
| /** | ||
| * Plugin options after defaults are applied. | ||
| */ | ||
| TResolvedOptions extends object = TOptions, | ||
| /** | ||
| * Resolver that encapsulates naming and path-resolution helpers. | ||
| * Define with `defineResolver` and export alongside the plugin. | ||
| */ | ||
| TResolver extends Resolver = Resolver, | ||
| > = { | ||
| name: TName | ||
| options: TOptions | ||
| resolvedOptions: TResolvedOptions | ||
| resolver: TResolver | ||
| } | ||
| /** | ||
| * Normalized plugin after setup, with runtime fields populated. | ||
| * For internal use only — plugins use the public `Plugin` type externally. | ||
| * | ||
| * @internal | ||
| */ | ||
| export type NormalizedPlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions> = Plugin<TOptions> & { | ||
| options: TOptions['resolvedOptions'] & { | ||
| output: Output | ||
| include?: Array<Include> | ||
| exclude: Array<Exclude> | ||
| override: Array<Override<TOptions['resolvedOptions']>> | ||
| } | ||
| resolver: TOptions['resolver'] | ||
| transformer?: Visitor | ||
| renderer?: RendererFactory | ||
| generators?: Array<Generator> | ||
| apply?: (config: Config) => boolean | ||
| version?: string | ||
| } | ||
| /** | ||
| * Partial `Config` for user-facing entry points with sensible defaults. | ||
| * | ||
| * `UserConfig` is what you pass to `defineConfig()`. It has optional `root`, `plugins`, `parsers`, and `adapter` | ||
| * fields (which fall back to sensible defaults). All other Config options are available, including `output`, `input`, | ||
| * `storage`, `middleware`, `renderer`, `devtools`, and `hooks`. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * export default defineConfig({ | ||
| * input: { path: './petstore.yaml' }, | ||
| * output: { path: './src/gen' }, | ||
| * plugins: [pluginTs(), pluginZod()], | ||
| * }) | ||
| * ``` | ||
| */ | ||
| export type UserConfig<TInput = Input> = Omit<Config<TInput>, 'root' | 'plugins' | 'parsers' | 'adapter'> & { | ||
| /** | ||
| * Project root directory, absolute or relative to the config file location. | ||
| * | ||
| * Used as the base path for `root`-relative paths (e.g., `output.path`, file paths in hooks). | ||
| * | ||
| * @default process.cwd() | ||
| * @example | ||
| * ```ts | ||
| * root: '/home/user/my-project' | ||
| * root: './my-project' // relative to config file | ||
| * ``` | ||
| */ | ||
| root?: string | ||
| /** | ||
| * Custom parsers that convert generated AST nodes to strings (TypeScript, JSON, markdown, etc.). | ||
| * | ||
| * Each parser handles a specific file type. By default, Kubb uses `parserTs` from `@kubb/parser-ts` for TypeScript files. | ||
| * Pass custom parsers to support additional languages or custom formats. | ||
| * | ||
| * @default [parserTs] // from @kubb/parser-ts | ||
| * @example | ||
| * ```ts | ||
| * import { parserTs } from '@kubb/parser-ts' | ||
| * import { parserJsonSchema } from '@kubb/parser-json-schema' | ||
| * | ||
| * parsers: [parserTs(), parserJsonSchema()] | ||
| * ``` | ||
| * | ||
| * @see {@link Parser} to implement a custom parser. | ||
| */ | ||
| parsers?: Array<Parser> | ||
| /** | ||
| * Adapter that parses your API specification (OpenAPI, GraphQL, AsyncAPI, etc.) into Kubb's universal AST. | ||
| * | ||
| * The adapter bridge between your input format and Kubb's internal representation. By default, uses the OAS adapter. | ||
| * Pass an alternative adapter (or multiple configs with different adapters) to support different spec formats. | ||
| * | ||
| * @default new OasAdapter() // from @kubb/adapter-oas | ||
| * @example | ||
| * ```ts | ||
| * import { Oas } from '@kubb/adapter-oas' | ||
| * | ||
| * adapter: new Oas({ apiVersion: '3.0.0' }) | ||
| * ``` | ||
| * | ||
| * @see {@link Adapter} to implement a custom adapter for GraphQL or other formats. | ||
| */ | ||
| adapter?: Adapter | ||
| /** | ||
| * Plugins that execute during the build to generate code and transform the AST. | ||
| * | ||
| * Each plugin processes the AST produced by the adapter and can emit files for different | ||
| * programming languages or formats (TypeScript, Zod schemas, Faker data, etc.). | ||
| * | ||
| * @default [] // no plugins (useful for setup/testing) | ||
| * @example | ||
| * ```ts | ||
| * plugins: [ | ||
| * pluginTs({ output: { path: './src/gen' } }), | ||
| * pluginZod({ output: { path: './src/gen' } }), | ||
| * ] | ||
| * ``` | ||
| * | ||
| * @see {@link definePlugin} to create a custom plugin. | ||
| */ | ||
| plugins?: Array<Plugin> | ||
| } | ||
| export type ResolveNameParams = { | ||
| name: string | ||
| pluginName?: string | ||
| /** | ||
| * Entity type being named. | ||
| * - `'file'` — file name (camelCase) | ||
| * - `'function'` — exported function name (camelCase) | ||
| * - `'type'` — TypeScript type name (PascalCase) | ||
| * - `'const'` — variable name (camelCase) | ||
| */ | ||
| type?: 'file' | 'function' | 'type' | 'const' | ||
| } | ||
| /** | ||
| * Context object passed to generator `schema`, `operation`, and `operations` methods. | ||
| * | ||
| * The adapter is always defined (guaranteed by `runPluginAstHooks`) so no runtime checks | ||
| * are needed. `ctx.options` carries resolved per-node options after exclude/include/override | ||
| * filtering for individual schema/operation calls, or plugin-level options for operations. | ||
| */ | ||
| export type GeneratorContext<TOptions extends PluginFactoryOptions = PluginFactoryOptions> = { | ||
| config: Config | ||
| /** | ||
| * Absolute path to the current plugin's output directory. | ||
| */ | ||
| root: string | ||
| /** | ||
| * Determine output mode based on the output config. | ||
| * Returns `'single'` when `output.path` is a file, `'split'` for a directory. | ||
| */ | ||
| getMode: (output: { path: string }) => 'single' | 'split' | ||
| driver: PluginDriver | ||
| /** | ||
| * Get a plugin by name, typed via `Kubb.PluginRegistry` when registered. | ||
| */ | ||
| getPlugin<TName extends keyof Kubb.PluginRegistry>(name: TName): Plugin<Kubb.PluginRegistry[TName]> | undefined | ||
| getPlugin(name: string): Plugin | undefined | ||
| /** | ||
| * Get a plugin by name, throws an error if not found. | ||
| */ | ||
| requirePlugin<TName extends keyof Kubb.PluginRegistry>(name: TName): Plugin<Kubb.PluginRegistry[TName]> | ||
| requirePlugin(name: string): Plugin | ||
| /** | ||
| * Get a resolver by plugin name, typed via `Kubb.PluginRegistry` when registered. | ||
| */ | ||
| getResolver<TName extends keyof Kubb.PluginRegistry>(name: TName): Kubb.PluginRegistry[TName]['resolver'] | ||
| getResolver(name: string): Resolver | ||
| /** | ||
| * Add files only if they don't exist. | ||
| */ | ||
| addFile: (...file: Array<FileNode>) => Promise<void> | ||
| /** | ||
| * Merge sources into the same output file. | ||
| */ | ||
| upsertFile: (...file: Array<FileNode>) => Promise<void> | ||
| hooks: AsyncEventEmitter<KubbHooks> | ||
| /** | ||
| * The current plugin instance. | ||
| */ | ||
| plugin: Plugin<TOptions> | ||
| /** | ||
| * The current plugin's resolver. | ||
| */ | ||
| resolver: TOptions['resolver'] | ||
| /** | ||
| * The current plugin's transformer. | ||
| */ | ||
| transformer: Visitor | undefined | ||
| /** | ||
| * Emit a warning. | ||
| */ | ||
| warn: (message: string) => void | ||
| /** | ||
| * Emit an error. | ||
| */ | ||
| error: (error: string | Error) => void | ||
| /** | ||
| * Emit an info message. | ||
| */ | ||
| info: (message: string) => void | ||
| /** | ||
| * Open the current input node in Kubb Studio. | ||
| */ | ||
| openInStudio: (options?: DevtoolsOptions) => Promise<void> | ||
| /** | ||
| * The configured adapter instance. | ||
| */ | ||
| adapter: Adapter | ||
| /** | ||
| * The universal `InputNode` produced by the adapter. | ||
| */ | ||
| inputNode: InputNode | ||
| /** | ||
| * Resolved options after exclude/include/override filtering. | ||
| */ | ||
| options: TOptions['resolvedOptions'] | ||
| } | ||
| /** | ||
| * Output configuration for generated files. | ||
| */ | ||
| export type Output<_TOptions = unknown> = { | ||
| /** | ||
| * Output folder or file path for generated code. | ||
| */ | ||
| path: string | ||
| /** | ||
| * Text or function prepended to every generated file. | ||
| * When a function, receives the current `InputNode` and returns a string. | ||
| */ | ||
| banner?: string | ((node?: InputNode) => string) | ||
| /** | ||
| * Text or function appended to every generated file. | ||
| * When a function, receives the current `InputNode` and returns a string. | ||
| */ | ||
| footer?: string | ((node?: InputNode) => string) | ||
| /** | ||
| * Whether to override existing external files if they already exist. | ||
| * @default false | ||
| */ | ||
| override?: boolean | ||
| } & ExtractRegistryKey<Kubb.PluginOptionsRegistry, 'output'> | ||
| export type Group = { | ||
| /** | ||
| * How to group files into subdirectories. | ||
| * - `'tag'` — group by OpenAPI tags | ||
| * - `'path'` — group by OpenAPI paths | ||
| */ | ||
| type: 'tag' | 'path' | ||
| /** | ||
| * Function that returns the subdirectory name for a group value. | ||
| * Defaults to `${camelCase(group)}Controller` for tags, first path segment for paths. | ||
| */ | ||
| name?: (context: { group: string }) => string | ||
| } | ||
| export type LoggerOptions = { | ||
| /** | ||
| * Log level for output verbosity. | ||
| * @default 3 | ||
| */ | ||
| logLevel: (typeof logLevel)[keyof typeof logLevel] | ||
| } | ||
| /** | ||
| * Shared context passed to plugins, parsers, and other internals. | ||
| */ | ||
| export type LoggerContext = AsyncEventEmitter<KubbHooks> | ||
| export type Logger<TOptions extends LoggerOptions = LoggerOptions> = { | ||
| name: string | ||
| install: (context: LoggerContext, options?: TOptions) => void | Promise<void> | ||
| } | ||
| export type UserLogger<TOptions extends LoggerOptions = LoggerOptions> = Logger<TOptions> | ||
| export type { Storage } from './createStorage.ts' | ||
| export type { Generator } from './defineGenerator.ts' | ||
| export type { Middleware } from './defineMiddleware.ts' | ||
| export type { Plugin } from './definePlugin.ts' | ||
| export type { Kubb, KubbHooks } from './Kubb.ts' | ||
| /** | ||
| * Context for hook-style plugin `kubb:plugin:setup` handler. | ||
| * Provides methods to register generators, configure resolvers, transformers, and renderers. | ||
| */ | ||
| export type KubbPluginSetupContext<TFactory extends PluginFactoryOptions = PluginFactoryOptions> = { | ||
| /** | ||
| * Register a generator dynamically. Generators fire during the AST walk (schema/operation/operations) | ||
| * just like generators declared statically on `createPlugin`. | ||
| */ | ||
| addGenerator<TElement = unknown>(generator: Generator<TFactory, TElement>): void | ||
| /** | ||
| * Set or override the resolver for this plugin. | ||
| * The resolver controls file naming and path resolution. | ||
| */ | ||
| setResolver(resolver: Partial<TFactory['resolver']>): void | ||
| /** | ||
| * Set the AST transformer to pre-process nodes before they reach generators. | ||
| */ | ||
| setTransformer(visitor: Visitor): void | ||
| /** | ||
| * Set the renderer factory to process JSX elements from generators. | ||
| */ | ||
| setRenderer(renderer: RendererFactory): void | ||
| /** | ||
| * Set resolved options merged into the normalized plugin's `options`. | ||
| * Call this in `kubb:plugin:setup` to provide options generators need. | ||
| */ | ||
| setOptions(options: TFactory['resolvedOptions']): void | ||
| /** | ||
| * Inject a raw file into the build output, bypassing the generation pipeline. | ||
| */ | ||
| injectFile(userFileNode: UserFileNode): void | ||
| /** | ||
| * Merge a partial config update into the current build configuration. | ||
| */ | ||
| updateConfig(config: Partial<Config>): void | ||
| /** | ||
| * The resolved build configuration at setup time. | ||
| */ | ||
| config: Config | ||
| /** | ||
| * The plugin's user-provided options. | ||
| */ | ||
| options: TFactory['options'] | ||
| } | ||
| /** | ||
| * Context for hook-style plugin `kubb:build:start` handler. | ||
| * Fires immediately before the plugin execution loop begins. | ||
| */ | ||
| export type KubbBuildStartContext = { | ||
| config: Config | ||
| adapter: Adapter | ||
| inputNode: InputNode | ||
| /** | ||
| * Get a plugin by name, typed via `Kubb.PluginRegistry` when registered. | ||
| */ | ||
| getPlugin<TName extends keyof Kubb.PluginRegistry>(name: TName): Plugin<Kubb.PluginRegistry[TName]> | undefined | ||
| getPlugin(name: string): Plugin | undefined | ||
| /** | ||
| * Get all files currently in the file manager. | ||
| * Call this lazily (e.g. in `kubb:plugin:end`) to see files added by prior plugins. | ||
| */ | ||
| readonly files: ReadonlyArray<FileNode> | ||
| /** | ||
| * Upsert one or more files into the file manager. | ||
| * Files with the same path are merged; new files are appended. | ||
| * Safe to call at any point during the plugin lifecycle, including inside `kubb:plugin:end`. | ||
| */ | ||
| upsertFile: (...files: Array<FileNode>) => void | ||
| } | ||
| /** | ||
| * Context for `kubb:plugins:end` handlers. | ||
| * Fires after plugins run and per-plugin barrels are written, before final write to disk. | ||
| * Middleware that needs final files (e.g. root barrel) use this event. | ||
| */ | ||
| export type KubbPluginsEndContext = { | ||
| config: Config | ||
| /** | ||
| * All files currently in the file manager (lazy snapshot). | ||
| */ | ||
| readonly files: ReadonlyArray<FileNode> | ||
| /** | ||
| * Upsert files into the file manager. | ||
| * Files added here are included in the write pass. | ||
| */ | ||
| upsertFile: (...files: Array<FileNode>) => void | ||
| } | ||
| /** | ||
| * Context for hook-style plugin `kubb:build:end` handler. | ||
| * Fires after all files have been written to disk. | ||
| */ | ||
| export type KubbBuildEndContext = { | ||
| files: Array<FileNode> | ||
| config: Config | ||
| outputDir: string | ||
| } | ||
| export type KubbLifecycleStartContext = { | ||
| version: string | ||
| } | ||
| export type KubbConfigEndContext = { | ||
| configs: Array<Config> | ||
| } | ||
| export type KubbGenerationStartContext = { | ||
| config: Config | ||
| } | ||
| export type KubbGenerationEndContext = { | ||
| config: Config | ||
| files: Array<FileNode> | ||
| sources: Map<string, string> | ||
| } | ||
| export type KubbGenerationSummaryContext = { | ||
| config: Config | ||
| failedPlugins: Set<{ plugin: Plugin; error: Error }> | ||
| status: 'success' | 'failed' | ||
| hrStart: [number, number] | ||
| filesCreated: number | ||
| pluginTimings?: Map<Plugin['name'], number> | ||
| } | ||
| export type KubbVersionNewContext = { | ||
| currentVersion: string | ||
| latestVersion: string | ||
| } | ||
| export type KubbInfoContext = { | ||
| message: string | ||
| info?: string | ||
| } | ||
| export type KubbErrorContext = { | ||
| error: Error | ||
| meta?: Record<string, unknown> | ||
| } | ||
| export type KubbSuccessContext = { | ||
| message: string | ||
| info?: string | ||
| } | ||
| export type KubbWarnContext = { | ||
| message: string | ||
| info?: string | ||
| } | ||
| export type KubbDebugContext = { | ||
| date: Date | ||
| logs: Array<string> | ||
| fileName?: string | ||
| } | ||
| export type KubbFilesProcessingStartContext = { | ||
| files: Array<FileNode> | ||
| } | ||
| export type KubbFileProcessingUpdateContext = { | ||
| /** | ||
| * Number of files processed. | ||
| */ | ||
| processed: number | ||
| /** | ||
| * Total files to process. | ||
| */ | ||
| total: number | ||
| /** | ||
| * Processing percentage (0–100). | ||
| */ | ||
| percentage: number | ||
| /** | ||
| * Optional source identifier. | ||
| */ | ||
| source?: string | ||
| /** | ||
| * The file being processed. | ||
| */ | ||
| file: FileNode | ||
| /** | ||
| * The current build configuration. | ||
| */ | ||
| config: Config | ||
| } | ||
| export type KubbFilesProcessingEndContext = { | ||
| files: Array<FileNode> | ||
| } | ||
| export type KubbPluginStartContext = { | ||
| plugin: NormalizedPlugin | ||
| } | ||
| export type KubbPluginEndContext = { | ||
| plugin: NormalizedPlugin | ||
| duration: number | ||
| success: boolean | ||
| error?: Error | ||
| config: Config | ||
| /** | ||
| * Returns all files currently in the file manager (lazy snapshot). | ||
| * Includes files added by plugins that have already run. | ||
| */ | ||
| readonly files: ReadonlyArray<FileNode> | ||
| /** | ||
| * Upsert one or more files into the file manager. | ||
| */ | ||
| upsertFile: (...files: Array<FileNode>) => void | ||
| } | ||
| export type KubbHookStartContext = { | ||
| id?: string | ||
| command: string | ||
| args?: readonly string[] | ||
| } | ||
| export type KubbHookEndContext = { | ||
| id?: string | ||
| command: string | ||
| args?: readonly string[] | ||
| success: boolean | ||
| error: Error | null | ||
| } | ||
| type ByTag = { | ||
| /** | ||
| * Filter by OpenAPI `tags` field. Matches one or more tags assigned to operations. | ||
| */ | ||
| type: 'tag' | ||
| /** | ||
| * Tag name to match (case-sensitive). Can be a literal string or regex pattern. | ||
| */ | ||
| pattern: string | RegExp | ||
| } | ||
| type ByOperationId = { | ||
| /** | ||
| * Filter by OpenAPI `operationId` field. Each operation (GET, POST, etc.) has a unique identifier. | ||
| */ | ||
| type: 'operationId' | ||
| /** | ||
| * Operation ID to match (case-sensitive). Can be a literal string or regex pattern. | ||
| */ | ||
| pattern: string | RegExp | ||
| } | ||
| type ByPath = { | ||
| /** | ||
| * Filter by OpenAPI `path` (URL endpoint). Useful to group or filter by service segments like `/pets`, `/users`, etc. | ||
| */ | ||
| type: 'path' | ||
| /** | ||
| * URL path to match (case-sensitive). Can be a literal string or regex pattern. Matches against the full path. | ||
| */ | ||
| pattern: string | RegExp | ||
| } | ||
| type ByMethod = { | ||
| /** | ||
| * Filter by HTTP method: `'get'`, `'post'`, `'put'`, `'delete'`, `'patch'`, `'head'`, `'options'`. | ||
| */ | ||
| type: 'method' | ||
| /** | ||
| * HTTP method to match (case-insensitive when using string, or regex for dynamic matching). | ||
| */ | ||
| pattern: HttpMethod | RegExp | ||
| } | ||
| // TODO implement as alternative for ByMethod | ||
| // type ByMethods = { | ||
| // type: 'methods' | ||
| // pattern: Array<HttpMethod> | ||
| // } | ||
| type BySchemaName = { | ||
| /** | ||
| * Filter by schema component name (TypeScript or JSON schema). Matches schemas in `#/components/schemas`. | ||
| */ | ||
| type: 'schemaName' | ||
| /** | ||
| * Schema name to match (case-sensitive). Can be a literal string or regex pattern. | ||
| */ | ||
| pattern: string | RegExp | ||
| } | ||
| type ByContentType = { | ||
| /** | ||
| * Filter by response or request content type: `'application/json'`, `'application/xml'`, etc. | ||
| */ | ||
| type: 'contentType' | ||
| /** | ||
| * Content type to match (case-sensitive). Can be a literal string or regex pattern. | ||
| */ | ||
| pattern: string | RegExp | ||
| } | ||
| /** | ||
| * A pattern filter that prevents matching nodes from being generated. | ||
| * | ||
| * Use to skip code generation for specific operations or schemas. For example, exclude deprecated endpoints | ||
| * or internal-only schemas. Can filter by tag, operationId, path, HTTP method, content type, or schema name. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * exclude: [ | ||
| * { type: 'tag', pattern: 'internal' }, // skip "internal" tag | ||
| * { type: 'path', pattern: /^\/admin/ }, // skip all /admin endpoints | ||
| * { type: 'operationId', pattern: 'deprecated_*' } // skip operationIds matching pattern | ||
| * ] | ||
| * ``` | ||
| */ | ||
| export type Exclude = ByTag | ByOperationId | ByPath | ByMethod | ByContentType | BySchemaName | ||
| /** | ||
| * A pattern filter that restricts generation to only matching nodes. | ||
| * | ||
| * Use to generate code for a subset of operations or schemas. For example, only generate for a specific service | ||
| * tag or only for "production" endpoints. Can filter by tag, operationId, path, HTTP method, content type, or schema name. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * include: [ | ||
| * { type: 'tag', pattern: 'public' }, // generate only "public" tag | ||
| * { type: 'path', pattern: /^\/api\/v1/ }, // generate only v1 endpoints | ||
| * ] | ||
| * ``` | ||
| */ | ||
| export type Include = ByTag | ByOperationId | ByPath | ByMethod | ByContentType | BySchemaName | ||
| /** | ||
| * A pattern filter paired with partial option overrides applied when the pattern matches. | ||
| * | ||
| * Use to customize generation for specific operations or schemas. For example, apply different output paths | ||
| * for different tags, or use custom resolver functions per operation. Can filter by tag, operationId, path, | ||
| * HTTP method, schema name, or content type. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * override: [ | ||
| * { | ||
| * type: 'tag', | ||
| * pattern: 'admin', | ||
| * options: { output: { path: './src/gen/admin' } } // admin APIs go to separate folder | ||
| * }, | ||
| * { | ||
| * type: 'operationId', | ||
| * pattern: 'listPets', | ||
| * options: { exclude: true } // skip this specific operation | ||
| * } | ||
| * ] | ||
| * ``` | ||
| */ | ||
| export type Override<TOptions> = (ByTag | ByOperationId | ByPath | ByMethod | BySchemaName | ByContentType) & { | ||
| //TODO should be options: Omit<Partial<TOptions>, 'override'> | ||
| options: Partial<TOptions> | ||
| } | ||
| /** | ||
| * File-specific parameters for `Resolver.resolvePath`. | ||
| * | ||
| * Pass alongside a `ResolverContext` to identify which file to resolve. | ||
| * Provide `tag` for tag-based grouping or `path` for path-based grouping. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * resolver.resolvePath( | ||
| * { baseName: 'petTypes.ts', tag: 'pets' }, | ||
| * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } }, | ||
| * ) | ||
| * // → '/src/types/petsController/petTypes.ts' | ||
| * ``` | ||
| */ | ||
| export type ResolverPathParams = { | ||
| baseName: FileNode['baseName'] | ||
| pathMode?: 'single' | 'split' | ||
| /** | ||
| * Tag value used when `group.type === 'tag'`. | ||
| */ | ||
| tag?: string | ||
| /** | ||
| * Path value used when `group.type === 'path'`. | ||
| */ | ||
| path?: string | ||
| } | ||
| /** | ||
| * Shared context passed as the second argument to `Resolver.resolvePath` and `Resolver.resolveFile`. | ||
| * | ||
| * Describes where on disk output is rooted, which output config is active, and the optional | ||
| * grouping strategy that controls subdirectory layout. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const context: ResolverContext = { | ||
| * root: config.root, | ||
| * output, | ||
| * group, | ||
| * } | ||
| * ``` | ||
| */ | ||
| export type ResolverContext = { | ||
| root: string | ||
| output: Output | ||
| group?: Group | ||
| /** | ||
| * Plugin name used to populate `meta.pluginName` on the resolved file. | ||
| */ | ||
| pluginName?: string | ||
| } | ||
| /** | ||
| * File-specific parameters for `Resolver.resolveFile`. | ||
| * | ||
| * Pass alongside a `ResolverContext` to fully describe the file to resolve. | ||
| * `tag` and `path` are used only when a matching `group` is present in the context. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * resolver.resolveFile( | ||
| * { name: 'listPets', extname: '.ts', tag: 'pets' }, | ||
| * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } }, | ||
| * ) | ||
| * // → { baseName: 'listPets.ts', path: '/src/types/petsController/listPets.ts', ... } | ||
| * ``` | ||
| */ | ||
| export type ResolverFileParams = { | ||
| name: string | ||
| extname: FileNode['extname'] | ||
| /** | ||
| * Tag value used when `group.type === 'tag'`. | ||
| */ | ||
| tag?: string | ||
| /** | ||
| * Path value used when `group.type === 'path'`. | ||
| */ | ||
| path?: string | ||
| } | ||
| /** | ||
| * Context passed to `Resolver.resolveBanner` and `Resolver.resolveFooter`. | ||
| * | ||
| * `output` is optional — not every plugin configures a banner/footer. | ||
| * `config` carries the global Kubb config, used to derive the default Kubb banner. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * resolver.resolveBanner(inputNode, { output: { banner: '// generated' }, config }) | ||
| * // → '// generated' | ||
| * ``` | ||
| */ | ||
| export type ResolveBannerContext = { | ||
| output?: Pick<Output, 'banner' | 'footer'> | ||
| config: Config | ||
| } | ||
| /** | ||
| * CLI options derived from command-line flags. | ||
| */ | ||
| export type CLIOptions = { | ||
| /** | ||
| * Path to `kubb.config.js`. | ||
| */ | ||
| config?: string | ||
| /** | ||
| * Enable watch mode for input files. | ||
| */ | ||
| watch?: boolean | ||
| /** | ||
| * Logging verbosity for CLI usage. | ||
| * @default 'silent' | ||
| */ | ||
| logLevel?: 'silent' | 'info' | 'debug' | ||
| } | ||
| /** | ||
| * All accepted forms of a Kubb configuration. | ||
| * | ||
| * Config is always `@kubb/core` {@link Config}. | ||
| * - `PossibleConfig` accepts `Config`/`Config[]`/promise or a no-arg config factory. | ||
| * - `PossibleConfig<TCliOptions>` accepts the same config forms or a config factory receiving `TCliOptions`. | ||
| */ | ||
| export type PossibleConfig<TCliOptions = undefined> = | ||
| | PossiblePromise<Config | Config[]> | ||
| | ((...args: [TCliOptions] extends [undefined] ? [] : [TCliOptions]) => PossiblePromise<Config | Config[]>) | ||
| export type { BuildOutput } from './createKubb.ts' | ||
| export type { Parser } from './defineParser.ts' |
| import { version as nodeVersion } from 'node:process' | ||
| import { version as KubbVersion } from '../../package.json' | ||
| /** | ||
| * Returns a snapshot of the current runtime environment. | ||
| * | ||
| * Useful for attaching context to debug logs and error reports so that | ||
| * issues can be reproduced without manual information gathering. | ||
| */ | ||
| export function getDiagnosticInfo() { | ||
| return { | ||
| nodeVersion, | ||
| KubbVersion, | ||
| platform: process.platform, | ||
| arch: process.arch, | ||
| cwd: process.cwd(), | ||
| } as const | ||
| } |
| import type { Config, InputPath, UserConfig } from '../types' | ||
| /** | ||
| * Type guard to check if a given config has an `input.path`. | ||
| */ | ||
| export function isInputPath(config: UserConfig | undefined): config is UserConfig<InputPath> | ||
| export function isInputPath(config: Config | undefined): config is Config<InputPath> | ||
| export function isInputPath(config: Config | UserConfig | undefined): config is Config<InputPath> | UserConfig<InputPath> { | ||
| return typeof config?.input === 'object' && config.input !== null && 'path' in config.input | ||
| } |
| import { findPackageJSON, readSync } from '@internals/utils' | ||
| type PackageJSON = { | ||
| dependencies?: Record<string, string> | ||
| devDependencies?: Record<string, string> | ||
| } | ||
| type DependencyName = string | ||
| type DependencyVersion = string | ||
| function getPackageJSONSync(cwd?: string): PackageJSON | null { | ||
| const pkgPath = findPackageJSON(cwd) | ||
| if (!pkgPath) { | ||
| return null | ||
| } | ||
| return JSON.parse(readSync(pkgPath)) as PackageJSON | ||
| } | ||
| function match(packageJSON: PackageJSON, dependency: DependencyName | RegExp): string | null { | ||
| const dependencies = { | ||
| ...(packageJSON.dependencies || {}), | ||
| ...(packageJSON.devDependencies || {}), | ||
| } | ||
| if (typeof dependency === 'string' && dependencies[dependency]) { | ||
| return dependencies[dependency] | ||
| } | ||
| const matched = Object.keys(dependencies).find((dep) => dep.match(dependency)) | ||
| return matched ? (dependencies[matched] ?? null) : null | ||
| } | ||
| function getVersionSync(dependency: DependencyName | RegExp, cwd?: string): DependencyVersion | null { | ||
| const packageJSON = getPackageJSONSync(cwd) | ||
| return packageJSON ? match(packageJSON, dependency) : null | ||
| } | ||
| /** | ||
| * Returns `true` when the nearest `package.json` declares a dependency that | ||
| * satisfies the given semver range. | ||
| * | ||
| * - Searches both `dependencies` and `devDependencies`. | ||
| * - Accepts a string package name or a `RegExp` to match scoped/pattern packages. | ||
| * - Uses `semver.satisfies` for range comparison; returns `false` when the | ||
| * version string cannot be coerced into a valid semver. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * satisfiesDependency('react', '>=18') // true when react@18.x is installed | ||
| * satisfiesDependency(/^@tanstack\//, '>=5') // true when any @tanstack/* >=5 is found | ||
| * ``` | ||
| */ | ||
| function coerceSemver(version: string): [number, number, number] | null { | ||
| const m = version.match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/) | ||
| return m ? [Number(m[1]), Number(m[2] ?? 0), Number(m[3] ?? 0)] : null | ||
| } | ||
| function satisfiesSemver(v: [number, number, number], range: string): boolean { | ||
| return range | ||
| .trim() | ||
| .split(/\s+/) | ||
| .every((cond) => { | ||
| const m = cond.match(/^(>=|<=|>|<|=|\^|~)?(\d+)(?:\.(\d+))?(?:\.(\d+))?$/) | ||
| if (!m) return false | ||
| const op = m[1] ?? '=' | ||
| const r: [number, number, number] = [Number(m[2]), Number(m[3] ?? 0), Number(m[4] ?? 0)] | ||
| const cmp = v[0] !== r[0] ? v[0] - r[0] : v[1] !== r[1] ? v[1] - r[1] : v[2] - r[2] | ||
| if (op === '>=') return cmp >= 0 | ||
| if (op === '<=') return cmp <= 0 | ||
| if (op === '>') return cmp > 0 | ||
| if (op === '<') return cmp < 0 | ||
| if (op === '^') return v[0] === r[0] && cmp >= 0 | ||
| if (op === '~') return v[0] === r[0] && v[1] === r[1] && cmp >= 0 | ||
| return cmp === 0 | ||
| }) | ||
| } | ||
| export function satisfiesDependency(dependency: DependencyName | RegExp, version: DependencyVersion, cwd?: string): boolean { | ||
| const packageVersion = getVersionSync(dependency, cwd) | ||
| if (!packageVersion) { | ||
| return false | ||
| } | ||
| if (packageVersion === version) { | ||
| return true | ||
| } | ||
| const semVer = coerceSemver(packageVersion) | ||
| if (!semVer) { | ||
| return false | ||
| } | ||
| return satisfiesSemver(semVer, version) | ||
| } |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Copyleft License
LicenseCopyleft license information was found.
Mixed license
LicensePackage contains multiple licenses.
Non-permissive License
LicenseA license not known to be considered permissive was found.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
897368
22.46%2
-50%2
-33.33%0
-100%100
42.86%19
-56.82%10885
-7.86%70
-65.52%1
Infinity%+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated