@kubb/core
Advanced tools
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
Sorry, the diff of this file is too big to display
| import { createHash } from 'node:crypto' | ||
| import { join, resolve } from 'node:path' | ||
| import { clean, read, write } from '@internals/utils' | ||
| import { createCache } from '../createCache.ts' | ||
| import { Manifest } from '../Manifest.ts' | ||
| /** | ||
| * Options for {@link fsCache}. | ||
| */ | ||
| export type FsCacheOptions = { | ||
| /** | ||
| * Directory that holds the cache. Resolved against `process.cwd()` when relative. | ||
| * | ||
| * @default 'node_modules/.cache/kubb' | ||
| */ | ||
| dir?: string | ||
| /** | ||
| * Maximum number of build snapshots to keep. The least-recently-used entries are | ||
| * evicted once the cache grows past it. | ||
| * | ||
| * @default 50 | ||
| */ | ||
| maxEntries?: number | ||
| /** | ||
| * Days a snapshot may go untouched before it is evicted. | ||
| * | ||
| * @default 7 | ||
| */ | ||
| ttlDays?: number | ||
| } | ||
| type IndexFile = Array<{ path: string; blob: string }> | ||
| function blobName(relativePath: string): string { | ||
| return `${createHash('sha256').update(relativePath).digest('hex')}.blob` | ||
| } | ||
| /** | ||
| * Local filesystem cache. Stores each build snapshot as content blobs plus an index, | ||
| * tracked by a manifest under `node_modules/.cache/kubb/` (the Nx and Vitest | ||
| * convention). Least-recently-used and expired entries are pruned on every persist. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { fsCache } from '@kubb/core' | ||
| * | ||
| * export default defineConfig({ | ||
| * cache: fsCache(), | ||
| * }) | ||
| * ``` | ||
| */ | ||
| export const fsCache = createCache((options: FsCacheOptions = {}) => { | ||
| const dir = resolve(options.dir ?? join('node_modules', '.cache', 'kubb')) | ||
| const maxEntries = options.maxEntries ?? 50 | ||
| const ttlDays = options.ttlDays ?? 7 | ||
| const blobsDir = join(dir, 'blobs') | ||
| const manifestPath = join(dir, 'manifest.json') | ||
| return { | ||
| name: 'fs', | ||
| async restore({ key }) { | ||
| const manifest = await Manifest.read(dir) | ||
| const entry = manifest.entries[key] | ||
| if (!entry) { | ||
| return null | ||
| } | ||
| try { | ||
| const index = JSON.parse(await read(join(blobsDir, key, 'index.json'))) as IndexFile | ||
| const files: Record<string, string> = {} | ||
| for (const { path, blob } of index) { | ||
| files[path] = await read(join(blobsDir, key, blob)) | ||
| } | ||
| entry.lastAccess = Date.now() | ||
| await write(manifestPath, JSON.stringify(manifest)).catch(() => {}) | ||
| return { files } | ||
| } catch { | ||
| return null | ||
| } | ||
| }, | ||
| async persist({ key, snapshot }) { | ||
| const entryDir = join(blobsDir, key) | ||
| const index: IndexFile = [] | ||
| for (const [path, source] of Object.entries(snapshot.files)) { | ||
| const blob = blobName(path) | ||
| await write(join(entryDir, blob), source) | ||
| index.push({ path, blob }) | ||
| } | ||
| await write(join(entryDir, 'index.json'), JSON.stringify(index)) | ||
| const manifest = await Manifest.read(dir) | ||
| const now = Date.now() | ||
| manifest.entries[key] = { files: index.map((item) => item.path), createdAt: now, lastAccess: now } | ||
| const pruned = Manifest.prune(manifest, { maxEntries, ttlDays, now }) | ||
| await Promise.all(pruned.removed.map((removedKey) => clean(join(blobsDir, removedKey)))) | ||
| await write(manifestPath, JSON.stringify(pruned.manifest)) | ||
| }, | ||
| } | ||
| }) |
| /** | ||
| * A snapshot of a completed build: the final rendered source of every generated | ||
| * file, keyed by its path relative to the output root. Restoring a snapshot writes | ||
| * those sources straight to storage, skipping generation entirely. | ||
| * | ||
| * Paths are relative, not absolute, so the snapshot never depends on where the | ||
| * project lives on disk. | ||
| */ | ||
| export type CachedSnapshot = { | ||
| /** | ||
| * Final source per output file, keyed by the path relative to | ||
| * `resolve(config.root, config.output.path)`. | ||
| */ | ||
| files: Record<string, string> | ||
| } | ||
| /** | ||
| * Backend that stores build snapshots for incremental ("hot") rebuilds. When the | ||
| * input fingerprint matches a stored key, Kubb restores the snapshot instead of | ||
| * regenerating. Kubb ships with `fsCache` (local disk). Implement this interface to | ||
| * back the cache with any other store. | ||
| * | ||
| * @see {@link createCache} to build a custom backend. | ||
| */ | ||
| export type Cache = { | ||
| /** | ||
| * Identifier used in logs and diagnostics (`'fs'`, `'memory'`). | ||
| */ | ||
| readonly name: string | ||
| /** | ||
| * Returns the snapshot stored under `key`, or `null` on a miss. A backend never | ||
| * throws on a miss or a transient failure. It returns `null` so the build falls | ||
| * through to regeneration. | ||
| */ | ||
| restore(params: { key: string }): Promise<CachedSnapshot | null> | ||
| /** | ||
| * Stores `snapshot` under `key`. Only called after a successful build with no | ||
| * error diagnostics. | ||
| */ | ||
| persist(params: { key: string; snapshot: CachedSnapshot }): Promise<void> | ||
| /** | ||
| * Optional teardown called after the build. Use to flush buffers or close | ||
| * connections. | ||
| */ | ||
| dispose?(): Promise<void> | ||
| } | ||
| /** | ||
| * Defines a custom cache backend. The builder receives user options and returns a | ||
| * {@link Cache}. Reach for this when the filesystem backend doesn't fit, for | ||
| * example to store snapshots in Redis or a database. | ||
| * | ||
| * @example In-memory cache (the built-in implementation) | ||
| * ```ts | ||
| * import { createCache } from '@kubb/core' | ||
| * | ||
| * export const memoryCache = createCache(() => { | ||
| * const store = new Map<string, CachedSnapshot>() | ||
| * | ||
| * return { | ||
| * name: 'memory', | ||
| * async restore({ key }) { | ||
| * return store.get(key) ?? null | ||
| * }, | ||
| * async persist({ key, snapshot }) { | ||
| * store.set(key, snapshot) | ||
| * }, | ||
| * } | ||
| * }) | ||
| * ``` | ||
| */ | ||
| export function createCache<TOptions = Record<string, never>>(build: (options: TOptions) => Cache): (options?: TOptions) => Cache { | ||
| return (options) => build(options ?? ({} as TOptions)) | ||
| } |
| import { createHash } from 'node:crypto' | ||
| import { readFile } from 'node:fs/promises' | ||
| import { relative } from 'node:path' | ||
| import { URLPath } from '@internals/utils' | ||
| import type { AdapterSource } from './createAdapter.ts' | ||
| import type { Config } from './createKubb.ts' | ||
| /** | ||
| * Computes the cache key for an incremental build. All methods are static, so call them as | ||
| * `Fingerprint.compute(...)` and `Fingerprint.stringify(...)`. The key holds no absolute | ||
| * paths or modification times, so it never depends on where the project lives on disk. | ||
| */ | ||
| export class Fingerprint { | ||
| /** | ||
| * Bumped when the snapshot format or fingerprint inputs change in an incompatible way, so stale | ||
| * cache entries from older Kubb builds are never reused. | ||
| */ | ||
| static version = 1 | ||
| /** | ||
| * Deterministically serializes a value to JSON: object keys are sorted recursively and | ||
| * `undefined` values and functions are dropped. Two structurally equal configs produce the same | ||
| * string regardless of key order, which keeps the fingerprint stable across machines. | ||
| */ | ||
| static stringify(value: unknown): string { | ||
| return JSON.stringify(Fingerprint.#normalize(value)) | ||
| } | ||
| /** | ||
| * Computes a cache key from everything that affects the generated output: the spec content, the | ||
| * output-shaping config, each plugin's name and options, the middleware names, the running | ||
| * `@kubb/core` version, and the cache format version. Returns `null` when the input can't be | ||
| * fingerprinted (remote URL or no adapter source), which disables caching for that build. | ||
| */ | ||
| static async compute({ config, adapterSource, version }: { config: Config; adapterSource: AdapterSource | null; version: string }): Promise<string | null> { | ||
| if (!adapterSource) { | ||
| return null | ||
| } | ||
| const spec = await Fingerprint.#readSpec(adapterSource, config.root) | ||
| if (spec === null) { | ||
| return null | ||
| } | ||
| const input = { | ||
| cacheVersion: Fingerprint.version, | ||
| version, | ||
| spec, | ||
| name: config.name, | ||
| output: config.output, | ||
| adapter: config.adapter?.name, | ||
| parsers: config.parsers.map((parser) => parser.name), | ||
| plugins: config.plugins.map((plugin) => ({ name: plugin.name, options: plugin.options })), | ||
| middleware: (config.middleware ?? []).map((middleware) => middleware.name), | ||
| } | ||
| return createHash('sha256').update(Fingerprint.stringify(input)).digest('hex') | ||
| } | ||
| static #normalize(value: unknown): unknown { | ||
| if (value === null || typeof value !== 'object') { | ||
| return typeof value === 'function' ? undefined : value | ||
| } | ||
| if (Array.isArray(value)) { | ||
| return value.map((item) => Fingerprint.#normalize(item)) | ||
| } | ||
| const source = value as Record<string, unknown> | ||
| const result: Record<string, unknown> = {} | ||
| for (const key of Object.keys(source).sort()) { | ||
| const normalized = Fingerprint.#normalize(source[key]) | ||
| if (normalized !== undefined) { | ||
| result[key] = normalized | ||
| } | ||
| } | ||
| return result | ||
| } | ||
| /** | ||
| * Reads the spec content that feeds the fingerprint. Returns `null` for a remote URL source | ||
| * (hashing remote content would mean fetching it on every run) or when a file can't be read, so a | ||
| * missing or virtual spec disables caching instead of failing the build. | ||
| */ | ||
| static async #readSpec(source: AdapterSource, root: string): Promise<unknown> { | ||
| if (source.type === 'data') { | ||
| return { kind: 'data', data: typeof source.data === 'string' ? source.data : Fingerprint.stringify(source.data) } | ||
| } | ||
| const paths = source.type === 'paths' ? source.paths : [source.path] | ||
| if (paths.some((path) => new URLPath(path).isURL)) { | ||
| return null | ||
| } | ||
| try { | ||
| const contents = await Promise.all(paths.map(async (path) => ({ path: relative(root, path), content: await readFile(path, 'utf8') }))) | ||
| return { kind: 'path', contents } | ||
| } catch { | ||
| return null | ||
| } | ||
| } | ||
| } |
| import { join } from 'node:path' | ||
| import { read } from '@internals/utils' | ||
| /** | ||
| * Bookkeeping for one cached build: the relative paths it covers and timestamps used by the pruner. | ||
| */ | ||
| export type ManifestEntry = { | ||
| files: Array<string> | ||
| createdAt: number | ||
| lastAccess: number | ||
| } | ||
| /** | ||
| * The on-disk manifest: a version marker plus an entry per cached build, keyed by fingerprint. | ||
| */ | ||
| export type ManifestData = { | ||
| version: number | ||
| entries: Record<string, ManifestEntry> | ||
| } | ||
| /** | ||
| * Reads and prunes the local cache manifest. All methods are static, so call them as | ||
| * `Manifest.read(dir)` and `Manifest.prune(data, ...)`. A damaged manifest reads as empty so the | ||
| * cache degrades to misses instead of throwing. Writing goes through `write` from `@internals/utils`. | ||
| */ | ||
| export class Manifest { | ||
| /** | ||
| * On-disk layout version for the manifest itself. Bumped when the manifest shape changes; a | ||
| * mismatch makes the whole local cache read as empty. | ||
| */ | ||
| static version = 1 | ||
| /** | ||
| * Reads the manifest at `dir/manifest.json`. A missing, corrupt, or version-mismatched file reads | ||
| * as an empty manifest. | ||
| */ | ||
| static async read(dir: string): Promise<ManifestData> { | ||
| try { | ||
| const parsed = JSON.parse(await read(join(dir, 'manifest.json'))) as ManifestData | ||
| if (parsed.version !== Manifest.version || typeof parsed.entries !== 'object') { | ||
| return Manifest.#empty() | ||
| } | ||
| return parsed | ||
| } catch { | ||
| return Manifest.#empty() | ||
| } | ||
| } | ||
| /** | ||
| * Selects the keys to evict so the cache stays within `ttlDays` and `maxEntries`. Returns the | ||
| * surviving manifest plus the evicted keys (the caller deletes their blobs). Pure, does no IO. | ||
| */ | ||
| static prune( | ||
| manifest: ManifestData, | ||
| { maxEntries, ttlDays, now }: { maxEntries: number; ttlDays: number; now: number }, | ||
| ): { | ||
| manifest: ManifestData | ||
| removed: Array<string> | ||
| } { | ||
| const ttlMs = ttlDays * 24 * 60 * 60 * 1000 | ||
| const removed: Array<string> = [] | ||
| const kept: Array<[string, ManifestEntry]> = [] | ||
| for (const [key, entry] of Object.entries(manifest.entries)) { | ||
| if (now - entry.lastAccess > ttlMs) { | ||
| removed.push(key) | ||
| } else { | ||
| kept.push([key, entry]) | ||
| } | ||
| } | ||
| if (kept.length > maxEntries) { | ||
| kept.sort((a, b) => b[1].lastAccess - a[1].lastAccess) | ||
| for (const [key] of kept.splice(maxEntries)) { | ||
| removed.push(key) | ||
| } | ||
| } | ||
| return { manifest: { version: Manifest.version, entries: Object.fromEntries(kept) }, removed } | ||
| } | ||
| static #empty(): ManifestData { | ||
| return { version: Manifest.version, entries: {} } | ||
| } | ||
| } |
+267
-16
| Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); | ||
| const require_memoryStorage = require("./memoryStorage-DZqlEW7H.cjs"); | ||
| const require_memoryStorage = require("./memoryStorage-Dldu8sRT.cjs"); | ||
| let node_util = require("node:util"); | ||
@@ -64,5 +64,24 @@ let node_crypto = require("node:crypto"); | ||
| if (!text) return ""; | ||
| return (0, node_util.styleText)(randomColors[(0, node_crypto.createHash)("sha256").update(text).digest().readUInt32BE(0) % randomColors.length] ?? "white", text); | ||
| return (0, node_util.styleText)(randomColors[(0, node_crypto.hash)("sha256", text, "buffer").readUInt32BE(0) % randomColors.length] ?? "white", text); | ||
| } | ||
| //#endregion | ||
| //#region ../../internals/utils/src/path.ts | ||
| /** | ||
| * 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("\\", "/"); | ||
| } | ||
| //#endregion | ||
| //#region ../../internals/utils/src/env.ts | ||
@@ -85,4 +104,68 @@ /** | ||
| //#endregion | ||
| //#region ../../internals/utils/src/runtime.ts | ||
| /** | ||
| * Returns `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 (isBun()) { | ||
| * await Bun.write(path, data) | ||
| * } | ||
| * ``` | ||
| */ | ||
| function isBun() { | ||
| return typeof Bun !== "undefined"; | ||
| } | ||
| /** | ||
| * Returns `true` when the current process is running under Deno. | ||
| */ | ||
| function isDeno() { | ||
| return typeof globalThis.Deno !== "undefined"; | ||
| } | ||
| /** | ||
| * Returns the name of the runtime executing the current process. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * getRuntimeName() // 'bun' when run with `bun kubb`, 'node' otherwise | ||
| * ``` | ||
| */ | ||
| function getRuntimeName() { | ||
| if (isBun()) return "bun"; | ||
| if (isDeno()) return "deno"; | ||
| return "node"; | ||
| } | ||
| /** | ||
| * Returns the version of the active runtime, or an empty string when it cannot be read. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * getRuntimeVersion() // '1.3.11' under Bun, '22.22.2' under Node | ||
| * ``` | ||
| */ | ||
| function getRuntimeVersion() { | ||
| if (isBun()) return process.versions.bun ?? ""; | ||
| if (isDeno()) return globalThis.Deno?.version?.deno ?? ""; | ||
| return process.versions?.node ?? ""; | ||
| } | ||
| //#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 (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. | ||
@@ -104,3 +187,3 @@ * Skips the write when the trimmed content is empty or identical to what is already on disk. | ||
| const resolved = (0, node_path.resolve)(path); | ||
| if (typeof Bun !== "undefined") { | ||
| if (isBun()) { | ||
| const file = Bun.file(resolved); | ||
@@ -218,2 +301,31 @@ if ((await file.exists() ? await file.text() : null) === trimmed) return null; | ||
| //#endregion | ||
| //#region src/createCache.ts | ||
| /** | ||
| * Defines a custom cache backend. The builder receives user options and returns a | ||
| * {@link Cache}. Reach for this when the filesystem backend doesn't fit, for | ||
| * example to store snapshots in Redis or a database. | ||
| * | ||
| * @example In-memory cache (the built-in implementation) | ||
| * ```ts | ||
| * import { createCache } from '@kubb/core' | ||
| * | ||
| * export const memoryCache = createCache(() => { | ||
| * const store = new Map<string, CachedSnapshot>() | ||
| * | ||
| * return { | ||
| * name: 'memory', | ||
| * async restore({ key }) { | ||
| * return store.get(key) ?? null | ||
| * }, | ||
| * async persist({ key, snapshot }) { | ||
| * store.set(key, snapshot) | ||
| * }, | ||
| * } | ||
| * }) | ||
| * ``` | ||
| */ | ||
| function createCache(build) { | ||
| return (options) => build(options ?? {}); | ||
| } | ||
| //#endregion | ||
| //#region src/storages/fsStorage.ts | ||
@@ -270,17 +382,17 @@ /** | ||
| const resolvedBase = (0, node_path.resolve)(base ?? process.cwd()); | ||
| async function* walk(dir, prefix) { | ||
| let entries; | ||
| try { | ||
| entries = await (0, node_fs_promises.readdir)(dir, { withFileTypes: true }); | ||
| } catch (_error) { | ||
| return; | ||
| } | ||
| for (const entry of entries) { | ||
| const rel = prefix ? `${prefix}/${entry.name}` : entry.name; | ||
| if (entry.isDirectory()) yield* walk((0, node_path.join)(dir, entry.name), rel); | ||
| else yield rel; | ||
| } | ||
| if (isBun()) { | ||
| const bunGlob = new Bun.Glob("**/*"); | ||
| return Array.fromAsync(bunGlob.scan({ | ||
| cwd: resolvedBase, | ||
| onlyFiles: true, | ||
| dot: true | ||
| })); | ||
| } | ||
| const keys = []; | ||
| for await (const key of walk(resolvedBase, "")) keys.push(key); | ||
| try { | ||
| for await (const entry of (0, node_fs_promises.glob)("**/*", { | ||
| cwd: resolvedBase, | ||
| withFileTypes: true | ||
| })) if (entry.isFile()) keys.push(toPosixPath((0, node_path.relative)(resolvedBase, (0, node_path.join)(entry.parentPath, entry.name)))); | ||
| } catch (_error) {} | ||
| return keys; | ||
@@ -344,2 +456,3 @@ }, | ||
| storage: userConfig.storage ?? fsStorage(), | ||
| cache: userConfig.cache === false ? void 0 : userConfig.cache, | ||
| reporters: userConfig.reporters ?? [], | ||
@@ -775,2 +888,4 @@ plugins: userConfig.plugins ?? [] | ||
| nodeVersion: node_process.default.versions.node.split(".")[0], | ||
| runtime: getRuntimeName(), | ||
| runtimeVersion: getRuntimeVersion().split(".")[0], | ||
| platform: node_os.default.platform(), | ||
@@ -807,2 +922,10 @@ ci: isCIEnvironment(), | ||
| { | ||
| key: "kubb.runtime", | ||
| value: { stringValue: event.runtime } | ||
| }, | ||
| { | ||
| key: "kubb.runtime_version", | ||
| value: { stringValue: event.runtimeVersion } | ||
| }, | ||
| { | ||
| key: "kubb.platform", | ||
@@ -1038,2 +1161,128 @@ value: { stringValue: event.platform } | ||
| //#endregion | ||
| //#region src/Manifest.ts | ||
| /** | ||
| * Reads and prunes the local cache manifest. All methods are static, so call them as | ||
| * `Manifest.read(dir)` and `Manifest.prune(data, ...)`. A damaged manifest reads as empty so the | ||
| * cache degrades to misses instead of throwing. Writing goes through `write` from `@internals/utils`. | ||
| */ | ||
| var Manifest = class Manifest { | ||
| /** | ||
| * On-disk layout version for the manifest itself. Bumped when the manifest shape changes; a | ||
| * mismatch makes the whole local cache read as empty. | ||
| */ | ||
| static version = 1; | ||
| /** | ||
| * Reads the manifest at `dir/manifest.json`. A missing, corrupt, or version-mismatched file reads | ||
| * as an empty manifest. | ||
| */ | ||
| static async read(dir) { | ||
| try { | ||
| const parsed = JSON.parse(await read((0, node_path.join)(dir, "manifest.json"))); | ||
| if (parsed.version !== Manifest.version || typeof parsed.entries !== "object") return Manifest.#empty(); | ||
| return parsed; | ||
| } catch { | ||
| return Manifest.#empty(); | ||
| } | ||
| } | ||
| /** | ||
| * Selects the keys to evict so the cache stays within `ttlDays` and `maxEntries`. Returns the | ||
| * surviving manifest plus the evicted keys (the caller deletes their blobs). Pure, does no IO. | ||
| */ | ||
| static prune(manifest, { maxEntries, ttlDays, now }) { | ||
| const ttlMs = ttlDays * 24 * 60 * 60 * 1e3; | ||
| const removed = []; | ||
| const kept = []; | ||
| for (const [key, entry] of Object.entries(manifest.entries)) if (now - entry.lastAccess > ttlMs) removed.push(key); | ||
| else kept.push([key, entry]); | ||
| if (kept.length > maxEntries) { | ||
| kept.sort((a, b) => b[1].lastAccess - a[1].lastAccess); | ||
| for (const [key] of kept.splice(maxEntries)) removed.push(key); | ||
| } | ||
| return { | ||
| manifest: { | ||
| version: Manifest.version, | ||
| entries: Object.fromEntries(kept) | ||
| }, | ||
| removed | ||
| }; | ||
| } | ||
| static #empty() { | ||
| return { | ||
| version: Manifest.version, | ||
| entries: {} | ||
| }; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/caches/fsCache.ts | ||
| function blobName(relativePath) { | ||
| return `${(0, node_crypto.createHash)("sha256").update(relativePath).digest("hex")}.blob`; | ||
| } | ||
| /** | ||
| * Local filesystem cache. Stores each build snapshot as content blobs plus an index, | ||
| * tracked by a manifest under `node_modules/.cache/kubb/` (the Nx and Vitest | ||
| * convention). Least-recently-used and expired entries are pruned on every persist. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { fsCache } from '@kubb/core' | ||
| * | ||
| * export default defineConfig({ | ||
| * cache: fsCache(), | ||
| * }) | ||
| * ``` | ||
| */ | ||
| const fsCache = createCache((options = {}) => { | ||
| const dir = (0, node_path.resolve)(options.dir ?? (0, node_path.join)("node_modules", ".cache", "kubb")); | ||
| const maxEntries = options.maxEntries ?? 50; | ||
| const ttlDays = options.ttlDays ?? 7; | ||
| const blobsDir = (0, node_path.join)(dir, "blobs"); | ||
| const manifestPath = (0, node_path.join)(dir, "manifest.json"); | ||
| return { | ||
| name: "fs", | ||
| async restore({ key }) { | ||
| const manifest = await Manifest.read(dir); | ||
| const entry = manifest.entries[key]; | ||
| if (!entry) return null; | ||
| try { | ||
| const index = JSON.parse(await read((0, node_path.join)(blobsDir, key, "index.json"))); | ||
| const files = {}; | ||
| for (const { path, blob } of index) files[path] = await read((0, node_path.join)(blobsDir, key, blob)); | ||
| entry.lastAccess = Date.now(); | ||
| await write(manifestPath, JSON.stringify(manifest)).catch(() => {}); | ||
| return { files }; | ||
| } catch { | ||
| return null; | ||
| } | ||
| }, | ||
| async persist({ key, snapshot }) { | ||
| const entryDir = (0, node_path.join)(blobsDir, key); | ||
| const index = []; | ||
| for (const [path, source] of Object.entries(snapshot.files)) { | ||
| const blob = blobName(path); | ||
| await write((0, node_path.join)(entryDir, blob), source); | ||
| index.push({ | ||
| path, | ||
| blob | ||
| }); | ||
| } | ||
| await write((0, node_path.join)(entryDir, "index.json"), JSON.stringify(index)); | ||
| const manifest = await Manifest.read(dir); | ||
| const now = Date.now(); | ||
| manifest.entries[key] = { | ||
| files: index.map((item) => item.path), | ||
| createdAt: now, | ||
| lastAccess: now | ||
| }; | ||
| const pruned = Manifest.prune(manifest, { | ||
| maxEntries, | ||
| ttlDays, | ||
| now | ||
| }); | ||
| await Promise.all(pruned.removed.map((removedKey) => clean((0, node_path.join)(blobsDir, removedKey)))); | ||
| await write(manifestPath, JSON.stringify(pruned.manifest)); | ||
| } | ||
| }; | ||
| }); | ||
| //#endregion | ||
| exports.AsyncEventEmitter = require_memoryStorage.AsyncEventEmitter; | ||
@@ -1052,2 +1301,3 @@ exports.Diagnostics = require_memoryStorage.Diagnostics; | ||
| exports.createAdapter = createAdapter; | ||
| exports.createCache = createCache; | ||
| exports.createKubb = createKubb; | ||
@@ -1064,2 +1314,3 @@ exports.createRenderer = createRenderer; | ||
| exports.fileReporter = fileReporter; | ||
| exports.fsCache = fsCache; | ||
| exports.fsStorage = fsStorage; | ||
@@ -1066,0 +1317,0 @@ exports.jsonReporter = jsonReporter; |
+16
-2
| import { t as __name } from "./chunk-C0LytTxp.js"; | ||
| import { $ as Output, A as KubbHookEndContext, At as defineLogger, B as UserConfig, Bt as createAdapter, C as KubbErrorContext, Ct as UserReporter, D as KubbFilesProcessingUpdateContext, Dt as LoggerContext, E as KubbFilesProcessingStartContext, Et as Logger, F as KubbLifecycleStartContext, Ft as Storage, G as KubbDriver, H as Generator, I as KubbPluginsEndContext, It as createStorage, J as Include, K as Exclude, L as KubbSuccessContext, Lt as Adapter, M as KubbHookStartContext, Mt as Renderer, N as KubbHooks, Nt as RendererFactory, O as KubbGenerationEndContext, Ot as LoggerOptions, P as KubbInfoContext, Pt as createRenderer, Q as NormalizedPlugin, R as KubbWarnContext, Rt as AdapterFactoryOptions, S as KubbDiagnosticContext, St as ReporterName, T as KubbFilesProcessingEndContext, Tt as selectReporters, U as GeneratorContext, V as createKubb, Vt as AsyncEventEmitter, W as defineGenerator, X as KubbPluginSetupContext, Y as KubbPluginEndContext, Z as KubbPluginStartContext, _ as InputPath, _t as Parser, a as DiagnosticLocation, at as ResolveBannerContext, b as KubbBuildStartContext, bt as Reporter, c as PerformanceDiagnostic, ct as Resolver, d as SerializedDiagnostic, dt as ResolverPathParams, et as Override, f as UpdateDiagnostic, ft as defineResolver, g as InputData, gt as ParsedFile, h as Config, ht as FileProcessorHooks, i as DiagnosticKind, it as BannerMeta, j as KubbHookLineContext, jt as logLevel, k as KubbGenerationStartContext, kt as UserLogger, l as ProblemCode, lt as ResolverContext, m as CLIOptions, mt as defineMiddleware, n as DiagnosticByCode, nt as PluginFactoryOptions, o as DiagnosticSeverity, ot as ResolveBannerFile, p as BuildOutput, pt as Middleware, q as Group, r as DiagnosticDoc, rt as definePlugin, s as Diagnostics, st as ResolveOptionsContext, t as Diagnostic, tt as Plugin, u as ProblemDiagnostic, ut as ResolverFileParams, v as Kubb, vt as defineParser, w as KubbFileProcessingUpdate, wt as createReporter, x as KubbConfigEndContext, xt as ReporterContext, y as KubbBuildEndContext, yt as GenerationResult, z as PossibleConfig, zt as AdapterSource } from "./diagnostics-Ba-FcsPo.js"; | ||
| import { $ as Output, A as KubbHookEndContext, At as defineLogger, B as UserConfig, Bt as CachedSnapshot, C as KubbErrorContext, Ct as UserReporter, D as KubbFilesProcessingUpdateContext, Dt as LoggerContext, E as KubbFilesProcessingStartContext, Et as Logger, F as KubbLifecycleStartContext, Ft as RendererFactory, G as KubbDriver, Gt as createAdapter, H as Generator, Ht as Adapter, I as KubbPluginsEndContext, It as createRenderer, J as Include, K as Exclude, Kt as AsyncEventEmitter, L as KubbSuccessContext, Lt as Storage, M as KubbHookStartContext, Mt as FsCacheOptions, N as KubbHooks, Nt as fsCache, O as KubbGenerationEndContext, Ot as LoggerOptions, P as KubbInfoContext, Pt as Renderer, Q as NormalizedPlugin, R as KubbWarnContext, Rt as createStorage, S as KubbDiagnosticContext, St as ReporterName, T as KubbFilesProcessingEndContext, Tt as selectReporters, U as GeneratorContext, Ut as AdapterFactoryOptions, V as createKubb, Vt as createCache, W as defineGenerator, Wt as AdapterSource, X as KubbPluginSetupContext, Y as KubbPluginEndContext, Z as KubbPluginStartContext, _ as InputPath, _t as Parser, a as DiagnosticLocation, at as ResolveBannerContext, b as KubbBuildStartContext, bt as Reporter, c as PerformanceDiagnostic, ct as Resolver, d as SerializedDiagnostic, dt as ResolverPathParams, et as Override, f as UpdateDiagnostic, ft as defineResolver, g as InputData, gt as ParsedFile, h as Config, ht as FileProcessorHooks, i as DiagnosticKind, it as BannerMeta, j as KubbHookLineContext, jt as logLevel, k as KubbGenerationStartContext, kt as UserLogger, l as ProblemCode, lt as ResolverContext, m as CLIOptions, mt as defineMiddleware, n as DiagnosticByCode, nt as PluginFactoryOptions, o as DiagnosticSeverity, ot as ResolveBannerFile, p as BuildOutput, pt as Middleware, q as Group, r as DiagnosticDoc, rt as definePlugin, s as Diagnostics, st as ResolveOptionsContext, t as Diagnostic, tt as Plugin, u as ProblemDiagnostic, ut as ResolverFileParams, v as Kubb, vt as defineParser, w as KubbFileProcessingUpdate, wt as createReporter, x as KubbConfigEndContext, xt as ReporterContext, y as KubbBuildEndContext, yt as GenerationResult, z as PossibleConfig, zt as Cache } from "./diagnostics-DZGgDzSv.js"; | ||
| import * as ast from "@kubb/ast"; | ||
| //#region ../../internals/utils/src/runtime.d.ts | ||
| /** | ||
| * Name of the JavaScript runtime executing the current process. | ||
| */ | ||
| type RuntimeName = 'bun' | 'deno' | 'node'; | ||
| //#endregion | ||
| //#region ../../internals/utils/src/urlPath.d.ts | ||
@@ -282,2 +288,10 @@ type URLObject = { | ||
| nodeVersion: string; | ||
| /** | ||
| * Name of the JavaScript runtime that executed the run, `'bun'`, `'deno'`, or `'node'`. | ||
| */ | ||
| runtime: RuntimeName; | ||
| /** | ||
| * Major version of the active runtime, e.g. `'1'` under Bun or `'22'` under Node. | ||
| */ | ||
| runtimeVersion: string; | ||
| platform: string; | ||
@@ -373,3 +387,3 @@ ci: boolean; | ||
| //#endregion | ||
| export { type Adapter, type AdapterFactoryOptions, type AdapterSource, AsyncEventEmitter, type BannerMeta, type BuildOutput, type CLIOptions, type 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, type InputData, type InputPath, type Kubb, type KubbBuildEndContext, type KubbBuildStartContext, type KubbConfigEndContext, type KubbDiagnosticContext, KubbDriver, type KubbErrorContext, type KubbFileProcessingUpdate, type KubbFilesProcessingEndContext, type KubbFilesProcessingStartContext, type KubbFilesProcessingUpdateContext, type KubbGenerationEndContext, type KubbGenerationStartContext, type KubbHookEndContext, type KubbHookLineContext, type KubbHookStartContext, type KubbHooks, type KubbInfoContext, type KubbLifecycleStartContext, type KubbPluginEndContext, type KubbPluginSetupContext, type KubbPluginStartContext, type KubbPluginsEndContext, type KubbSuccessContext, type KubbWarnContext, type Logger, type LoggerContext, type LoggerOptions, type Middleware, type NormalizedPlugin, type Output, type Override, type ParsedFile, type Parser, type PerformanceDiagnostic, type Plugin, type PluginFactoryOptions, type 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, Telemetry, URLPath, type UpdateDiagnostic, type UserConfig, type UserLogger, type UserReporter, ast, cliReporter, createAdapter, createKubb, createRenderer, createReporter, createStorage, defineGenerator, defineLogger, defineMiddleware, defineParser, definePlugin, defineResolver, fileReporter, fsStorage, jsonReporter, logLevel, memoryStorage, selectReporters }; | ||
| export { type Adapter, type AdapterFactoryOptions, type AdapterSource, AsyncEventEmitter, type BannerMeta, type BuildOutput, type CLIOptions, type Cache, type CachedSnapshot, type Config, type Diagnostic, type DiagnosticByCode, type DiagnosticDoc, type DiagnosticKind, type DiagnosticLocation, type DiagnosticSeverity, Diagnostics, type Exclude, type FileProcessorHooks, type FsCacheOptions, type GenerationResult, type Generator, type GeneratorContext, type Group, type Include, type InputData, type InputPath, type Kubb, type KubbBuildEndContext, type KubbBuildStartContext, type KubbConfigEndContext, type KubbDiagnosticContext, KubbDriver, type KubbErrorContext, type KubbFileProcessingUpdate, type KubbFilesProcessingEndContext, type KubbFilesProcessingStartContext, type KubbFilesProcessingUpdateContext, type KubbGenerationEndContext, type KubbGenerationStartContext, type KubbHookEndContext, type KubbHookLineContext, type KubbHookStartContext, type KubbHooks, type KubbInfoContext, type KubbLifecycleStartContext, type KubbPluginEndContext, type KubbPluginSetupContext, type KubbPluginStartContext, type KubbPluginsEndContext, type KubbSuccessContext, type KubbWarnContext, type Logger, type LoggerContext, type LoggerOptions, type Middleware, type NormalizedPlugin, type Output, type Override, type ParsedFile, type Parser, type PerformanceDiagnostic, type Plugin, type PluginFactoryOptions, type 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, Telemetry, URLPath, type UpdateDiagnostic, type UserConfig, type UserLogger, type UserReporter, ast, cliReporter, createAdapter, createCache, createKubb, createRenderer, createReporter, createStorage, defineGenerator, defineLogger, defineMiddleware, defineParser, definePlugin, defineResolver, fileReporter, fsCache, fsStorage, jsonReporter, logLevel, memoryStorage, selectReporters }; | ||
| //# sourceMappingURL=index.d.ts.map |
+268
-19
| import "./chunk-C0LytTxp.js"; | ||
| import { c as createStorage, d as URLPath, f as formatMs, g as BuildError, h as AsyncEventEmitter, l as Diagnostics, n as KubbDriver, o as defineResolver, p as getElapsedMs, r as _usingCtx, s as definePlugin, t as memoryStorage, u as OTLP_ENDPOINT } from "./memoryStorage-DA1bnMte.js"; | ||
| import { c as createStorage, d as URLPath, f as formatMs, g as BuildError, h as AsyncEventEmitter, l as Diagnostics, n as KubbDriver, o as defineResolver, p as getElapsedMs, r as _usingCtx, s as definePlugin, t as memoryStorage, u as OTLP_ENDPOINT } from "./memoryStorage-BOnaknb7.js"; | ||
| import { stripVTControlCharacters, styleText } from "node:util"; | ||
| import { createHash, randomBytes } from "node:crypto"; | ||
| import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises"; | ||
| import { createHash, hash, randomBytes } from "node:crypto"; | ||
| import { access, glob, mkdir, readFile, rm, writeFile } from "node:fs/promises"; | ||
| import { dirname, join, relative, resolve } from "node:path"; | ||
@@ -61,5 +61,24 @@ import { promises } from "node:dns"; | ||
| if (!text) return ""; | ||
| return styleText(randomColors[createHash("sha256").update(text).digest().readUInt32BE(0) % randomColors.length] ?? "white", text); | ||
| return styleText(randomColors[hash("sha256", text, "buffer").readUInt32BE(0) % randomColors.length] ?? "white", text); | ||
| } | ||
| //#endregion | ||
| //#region ../../internals/utils/src/path.ts | ||
| /** | ||
| * 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("\\", "/"); | ||
| } | ||
| //#endregion | ||
| //#region ../../internals/utils/src/env.ts | ||
@@ -82,4 +101,68 @@ /** | ||
| //#endregion | ||
| //#region ../../internals/utils/src/runtime.ts | ||
| /** | ||
| * Returns `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 (isBun()) { | ||
| * await Bun.write(path, data) | ||
| * } | ||
| * ``` | ||
| */ | ||
| function isBun() { | ||
| return typeof Bun !== "undefined"; | ||
| } | ||
| /** | ||
| * Returns `true` when the current process is running under Deno. | ||
| */ | ||
| function isDeno() { | ||
| return typeof globalThis.Deno !== "undefined"; | ||
| } | ||
| /** | ||
| * Returns the name of the runtime executing the current process. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * getRuntimeName() // 'bun' when run with `bun kubb`, 'node' otherwise | ||
| * ``` | ||
| */ | ||
| function getRuntimeName() { | ||
| if (isBun()) return "bun"; | ||
| if (isDeno()) return "deno"; | ||
| return "node"; | ||
| } | ||
| /** | ||
| * Returns the version of the active runtime, or an empty string when it cannot be read. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * getRuntimeVersion() // '1.3.11' under Bun, '22.22.2' under Node | ||
| * ``` | ||
| */ | ||
| function getRuntimeVersion() { | ||
| if (isBun()) return process.versions.bun ?? ""; | ||
| if (isDeno()) return globalThis.Deno?.version?.deno ?? ""; | ||
| return process.versions?.node ?? ""; | ||
| } | ||
| //#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 (isBun()) return Bun.file(path).text(); | ||
| return readFile(path, { encoding: "utf8" }); | ||
| } | ||
| /** | ||
| * Writes `data` to `path`, trimming leading/trailing whitespace before saving. | ||
@@ -101,3 +184,3 @@ * Skips the write when the trimmed content is empty or identical to what is already on disk. | ||
| const resolved = resolve(path); | ||
| if (typeof Bun !== "undefined") { | ||
| if (isBun()) { | ||
| const file = Bun.file(resolved); | ||
@@ -215,2 +298,31 @@ if ((await file.exists() ? await file.text() : null) === trimmed) return null; | ||
| //#endregion | ||
| //#region src/createCache.ts | ||
| /** | ||
| * Defines a custom cache backend. The builder receives user options and returns a | ||
| * {@link Cache}. Reach for this when the filesystem backend doesn't fit, for | ||
| * example to store snapshots in Redis or a database. | ||
| * | ||
| * @example In-memory cache (the built-in implementation) | ||
| * ```ts | ||
| * import { createCache } from '@kubb/core' | ||
| * | ||
| * export const memoryCache = createCache(() => { | ||
| * const store = new Map<string, CachedSnapshot>() | ||
| * | ||
| * return { | ||
| * name: 'memory', | ||
| * async restore({ key }) { | ||
| * return store.get(key) ?? null | ||
| * }, | ||
| * async persist({ key, snapshot }) { | ||
| * store.set(key, snapshot) | ||
| * }, | ||
| * } | ||
| * }) | ||
| * ``` | ||
| */ | ||
| function createCache(build) { | ||
| return (options) => build(options ?? {}); | ||
| } | ||
| //#endregion | ||
| //#region src/storages/fsStorage.ts | ||
@@ -267,17 +379,17 @@ /** | ||
| const resolvedBase = resolve(base ?? process.cwd()); | ||
| async function* walk(dir, prefix) { | ||
| let entries; | ||
| try { | ||
| entries = await readdir(dir, { withFileTypes: true }); | ||
| } catch (_error) { | ||
| return; | ||
| } | ||
| for (const entry of entries) { | ||
| const rel = prefix ? `${prefix}/${entry.name}` : entry.name; | ||
| if (entry.isDirectory()) yield* walk(join(dir, entry.name), rel); | ||
| else yield rel; | ||
| } | ||
| if (isBun()) { | ||
| const bunGlob = new Bun.Glob("**/*"); | ||
| return Array.fromAsync(bunGlob.scan({ | ||
| cwd: resolvedBase, | ||
| onlyFiles: true, | ||
| dot: true | ||
| })); | ||
| } | ||
| const keys = []; | ||
| for await (const key of walk(resolvedBase, "")) keys.push(key); | ||
| try { | ||
| for await (const entry of glob("**/*", { | ||
| cwd: resolvedBase, | ||
| withFileTypes: true | ||
| })) if (entry.isFile()) keys.push(toPosixPath(relative(resolvedBase, join(entry.parentPath, entry.name)))); | ||
| } catch (_error) {} | ||
| return keys; | ||
@@ -341,2 +453,3 @@ }, | ||
| storage: userConfig.storage ?? fsStorage(), | ||
| cache: userConfig.cache === false ? void 0 : userConfig.cache, | ||
| reporters: userConfig.reporters ?? [], | ||
@@ -772,2 +885,4 @@ plugins: userConfig.plugins ?? [] | ||
| nodeVersion: process$1.versions.node.split(".")[0], | ||
| runtime: getRuntimeName(), | ||
| runtimeVersion: getRuntimeVersion().split(".")[0], | ||
| platform: os.platform(), | ||
@@ -804,2 +919,10 @@ ci: isCIEnvironment(), | ||
| { | ||
| key: "kubb.runtime", | ||
| value: { stringValue: event.runtime } | ||
| }, | ||
| { | ||
| key: "kubb.runtime_version", | ||
| value: { stringValue: event.runtimeVersion } | ||
| }, | ||
| { | ||
| key: "kubb.platform", | ||
@@ -1035,4 +1158,130 @@ value: { stringValue: event.platform } | ||
| //#endregion | ||
| export { AsyncEventEmitter, Diagnostics, KubbDriver, Telemetry, URLPath, ast, cliReporter, createAdapter, createKubb, createRenderer, createReporter, createStorage, defineGenerator, defineLogger, defineMiddleware, defineParser, definePlugin, defineResolver, fileReporter, fsStorage, jsonReporter, logLevel, memoryStorage, selectReporters }; | ||
| //#region src/Manifest.ts | ||
| /** | ||
| * Reads and prunes the local cache manifest. All methods are static, so call them as | ||
| * `Manifest.read(dir)` and `Manifest.prune(data, ...)`. A damaged manifest reads as empty so the | ||
| * cache degrades to misses instead of throwing. Writing goes through `write` from `@internals/utils`. | ||
| */ | ||
| var Manifest = class Manifest { | ||
| /** | ||
| * On-disk layout version for the manifest itself. Bumped when the manifest shape changes; a | ||
| * mismatch makes the whole local cache read as empty. | ||
| */ | ||
| static version = 1; | ||
| /** | ||
| * Reads the manifest at `dir/manifest.json`. A missing, corrupt, or version-mismatched file reads | ||
| * as an empty manifest. | ||
| */ | ||
| static async read(dir) { | ||
| try { | ||
| const parsed = JSON.parse(await read(join(dir, "manifest.json"))); | ||
| if (parsed.version !== Manifest.version || typeof parsed.entries !== "object") return Manifest.#empty(); | ||
| return parsed; | ||
| } catch { | ||
| return Manifest.#empty(); | ||
| } | ||
| } | ||
| /** | ||
| * Selects the keys to evict so the cache stays within `ttlDays` and `maxEntries`. Returns the | ||
| * surviving manifest plus the evicted keys (the caller deletes their blobs). Pure, does no IO. | ||
| */ | ||
| static prune(manifest, { maxEntries, ttlDays, now }) { | ||
| const ttlMs = ttlDays * 24 * 60 * 60 * 1e3; | ||
| const removed = []; | ||
| const kept = []; | ||
| for (const [key, entry] of Object.entries(manifest.entries)) if (now - entry.lastAccess > ttlMs) removed.push(key); | ||
| else kept.push([key, entry]); | ||
| if (kept.length > maxEntries) { | ||
| kept.sort((a, b) => b[1].lastAccess - a[1].lastAccess); | ||
| for (const [key] of kept.splice(maxEntries)) removed.push(key); | ||
| } | ||
| return { | ||
| manifest: { | ||
| version: Manifest.version, | ||
| entries: Object.fromEntries(kept) | ||
| }, | ||
| removed | ||
| }; | ||
| } | ||
| static #empty() { | ||
| return { | ||
| version: Manifest.version, | ||
| entries: {} | ||
| }; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region src/caches/fsCache.ts | ||
| function blobName(relativePath) { | ||
| return `${createHash("sha256").update(relativePath).digest("hex")}.blob`; | ||
| } | ||
| /** | ||
| * Local filesystem cache. Stores each build snapshot as content blobs plus an index, | ||
| * tracked by a manifest under `node_modules/.cache/kubb/` (the Nx and Vitest | ||
| * convention). Least-recently-used and expired entries are pruned on every persist. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { fsCache } from '@kubb/core' | ||
| * | ||
| * export default defineConfig({ | ||
| * cache: fsCache(), | ||
| * }) | ||
| * ``` | ||
| */ | ||
| const fsCache = createCache((options = {}) => { | ||
| const dir = resolve(options.dir ?? join("node_modules", ".cache", "kubb")); | ||
| const maxEntries = options.maxEntries ?? 50; | ||
| const ttlDays = options.ttlDays ?? 7; | ||
| const blobsDir = join(dir, "blobs"); | ||
| const manifestPath = join(dir, "manifest.json"); | ||
| return { | ||
| name: "fs", | ||
| async restore({ key }) { | ||
| const manifest = await Manifest.read(dir); | ||
| const entry = manifest.entries[key]; | ||
| if (!entry) return null; | ||
| try { | ||
| const index = JSON.parse(await read(join(blobsDir, key, "index.json"))); | ||
| const files = {}; | ||
| for (const { path, blob } of index) files[path] = await read(join(blobsDir, key, blob)); | ||
| entry.lastAccess = Date.now(); | ||
| await write(manifestPath, JSON.stringify(manifest)).catch(() => {}); | ||
| return { files }; | ||
| } catch { | ||
| return null; | ||
| } | ||
| }, | ||
| async persist({ key, snapshot }) { | ||
| const entryDir = join(blobsDir, key); | ||
| const index = []; | ||
| for (const [path, source] of Object.entries(snapshot.files)) { | ||
| const blob = blobName(path); | ||
| await write(join(entryDir, blob), source); | ||
| index.push({ | ||
| path, | ||
| blob | ||
| }); | ||
| } | ||
| await write(join(entryDir, "index.json"), JSON.stringify(index)); | ||
| const manifest = await Manifest.read(dir); | ||
| const now = Date.now(); | ||
| manifest.entries[key] = { | ||
| files: index.map((item) => item.path), | ||
| createdAt: now, | ||
| lastAccess: now | ||
| }; | ||
| const pruned = Manifest.prune(manifest, { | ||
| maxEntries, | ||
| ttlDays, | ||
| now | ||
| }); | ||
| await Promise.all(pruned.removed.map((removedKey) => clean(join(blobsDir, removedKey)))); | ||
| await write(manifestPath, JSON.stringify(pruned.manifest)); | ||
| } | ||
| }; | ||
| }); | ||
| //#endregion | ||
| export { AsyncEventEmitter, Diagnostics, KubbDriver, Telemetry, URLPath, ast, cliReporter, createAdapter, createCache, createKubb, createRenderer, createReporter, createStorage, defineGenerator, defineLogger, defineMiddleware, defineParser, definePlugin, defineResolver, fileReporter, fsCache, fsStorage, jsonReporter, logLevel, memoryStorage, selectReporters }; | ||
| //# sourceMappingURL=index.js.map |
+1
-1
| Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); | ||
| const require_memoryStorage = require("./memoryStorage-DZqlEW7H.cjs"); | ||
| const require_memoryStorage = require("./memoryStorage-Dldu8sRT.cjs"); | ||
| let node_path = require("node:path"); | ||
@@ -4,0 +4,0 @@ node_path = require_memoryStorage.__toESM(node_path, 1); |
+1
-1
| import { t as __name } from "./chunk-C0LytTxp.js"; | ||
| import { G as KubbDriver, H as Generator, Lt as Adapter, Q as NormalizedPlugin, Rt as AdapterFactoryOptions, _t as Parser, h as Config, nt as PluginFactoryOptions } from "./diagnostics-Ba-FcsPo.js"; | ||
| import { G as KubbDriver, H as Generator, Ht as Adapter, Q as NormalizedPlugin, Ut as AdapterFactoryOptions, _t as Parser, h as Config, nt as PluginFactoryOptions } from "./diagnostics-DZGgDzSv.js"; | ||
| import { FileNode, InputMeta, OperationNode, SchemaNode, Visitor } from "@kubb/ast"; | ||
@@ -4,0 +4,0 @@ |
+1
-1
| import "./chunk-C0LytTxp.js"; | ||
| import { a as FileManager, i as FileProcessor, m as camelCase, n as KubbDriver, r as _usingCtx, t as memoryStorage } from "./memoryStorage-DA1bnMte.js"; | ||
| import { a as FileManager, i as FileProcessor, m as camelCase, n as KubbDriver, r as _usingCtx, t as memoryStorage } from "./memoryStorage-BOnaknb7.js"; | ||
| import path, { resolve } from "node:path"; | ||
@@ -4,0 +4,0 @@ import { transform } from "@kubb/ast"; |
+4
-4
| { | ||
| "name": "@kubb/core", | ||
| "version": "5.0.0-beta.41", | ||
| "version": "5.0.0-beta.42", | ||
| "description": "Core engine for Kubb's plugin-based code generation system. Provides the plugin driver, file manager, defineConfig, and build orchestration used by every Kubb plugin.", | ||
@@ -61,10 +61,10 @@ "keywords": [ | ||
| "tinyexec": "~1.1.2", | ||
| "@kubb/ast": "5.0.0-beta.41" | ||
| "@kubb/ast": "5.0.0-beta.42" | ||
| }, | ||
| "devDependencies": { | ||
| "@internals/utils": "0.0.0", | ||
| "@kubb/renderer-jsx": "5.0.0-beta.41" | ||
| "@kubb/renderer-jsx": "5.0.0-beta.42" | ||
| }, | ||
| "peerDependencies": { | ||
| "@kubb/renderer-jsx": "5.0.0-beta.41" | ||
| "@kubb/renderer-jsx": "5.0.0-beta.42" | ||
| }, | ||
@@ -71,0 +71,0 @@ "size-limit": [ |
+33
-4
@@ -8,2 +8,3 @@ import { resolve } from 'node:path' | ||
| import { type Diagnostic, Diagnostics, type ProblemDiagnostic, type UpdateDiagnostic } from './diagnostics.ts' | ||
| import type { Cache } from './createCache.ts' | ||
| import { createStorage, type Storage } from './createStorage.ts' | ||
@@ -238,2 +239,22 @@ import type { GeneratorContext } from './defineGenerator.ts' | ||
| /** | ||
| * Incremental build cache. Kubb fingerprints the inputs (spec content, config, plugin options, | ||
| * versions) and, on an unchanged "hot" run, restores the previously generated output instead of | ||
| * regenerating it. Same idea as Nx's computation cache. | ||
| * | ||
| * `defineConfig` enables `fsCache()` (local disk under `node_modules/.cache/kubb`) by default. | ||
| * Pass another backend to change where snapshots live, or `false` to turn caching off. A bare | ||
| * `createKubb` leaves it off unless a cache is provided. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { fsCache } from '@kubb/core' | ||
| * | ||
| * cache: fsCache({ dir: '.kubb-cache' }) | ||
| * cache: false | ||
| * ``` | ||
| * | ||
| * @see {@link Cache} interface for implementing custom backends. | ||
| */ | ||
| cache?: Cache | ||
| /** | ||
| * Plugins that run during the build to generate code and transform the AST. Each one processes | ||
@@ -341,4 +362,9 @@ * the adapter's AST and can emit files for a different target (TypeScript, Zod, Faker). A plugin | ||
| */ | ||
| export type UserConfig<TInput = Input> = Omit<Config<TInput>, 'root' | 'plugins' | 'parsers' | 'adapter' | 'storage' | 'reporters'> & { | ||
| export type UserConfig<TInput = Input> = Omit<Config<TInput>, 'root' | 'plugins' | 'parsers' | 'adapter' | 'storage' | 'reporters' | 'cache'> & { | ||
| /** | ||
| * Incremental build cache. Defaults to `fsCache()` (local disk). Pass another {@link Cache} | ||
| * backend, or `false` to turn caching off. | ||
| */ | ||
| cache?: Cache | false | ||
| /** | ||
| * Project root directory, absolute or relative to the config file location. | ||
@@ -664,3 +690,3 @@ * @default process.cwd() | ||
| /** | ||
| * Files about to be serialised and written. | ||
| * Files about to be serialized and written. | ||
| */ | ||
@@ -684,3 +710,3 @@ files: Array<FileNode> | ||
| /** | ||
| * Serialised file content, or `undefined` when the file produced no output. | ||
| * Serialized file content, or `undefined` when the file produced no output. | ||
| */ | ||
@@ -707,3 +733,3 @@ source?: string | ||
| /** | ||
| * All files that were serialised in this batch. | ||
| * All files that were serialized in this batch. | ||
| */ | ||
@@ -896,2 +922,5 @@ files: Array<FileNode> | ||
| storage: userConfig.storage ?? fsStorage(), | ||
| // Resolve `false` to "no cache". The default `fsCache()` is applied by `defineConfig`, not here, | ||
| // so a raw `createKubb` stays deterministic (no surprise on-disk cache) unless a cache is passed. | ||
| cache: userConfig.cache === false ? undefined : userConfig.cache, | ||
| reporters: userConfig.reporters ?? [], | ||
@@ -898,0 +927,0 @@ plugins: userConfig.plugins ?? [], |
@@ -26,3 +26,3 @@ import type { FileNode } from '@kubb/ast' | ||
| /** | ||
| * Serialise the file's AST into source code. | ||
| * Serialize the file's AST into source code. | ||
| */ | ||
@@ -29,0 +29,0 @@ parse(file: FileNode<TMeta>, options?: PrintOptions): string |
+2
-0
| export { AsyncEventEmitter, URLPath } from '@internals/utils' | ||
| export * as ast from '@kubb/ast' | ||
| export { createAdapter } from './createAdapter.ts' | ||
| export { createCache } from './createCache.ts' | ||
| export { Diagnostics } from './diagnostics.ts' | ||
@@ -20,4 +21,5 @@ export { createKubb } from './createKubb.ts' | ||
| export { KubbDriver } from './KubbDriver.ts' | ||
| export { fsCache } from './caches/fsCache.ts' | ||
| export { fsStorage } from './storages/fsStorage.ts' | ||
| export { memoryStorage } from './storages/memoryStorage.ts' | ||
| export * from './types.ts' |
+68
-8
@@ -1,7 +0,10 @@ | ||
| import { resolve } from 'node:path' | ||
| import { basename, join, relative, resolve } from 'node:path' | ||
| import { arrayToAsyncIterable, type AsyncEventEmitter, forBatches, getElapsedMs, isPromise, memoize, URLPath } from '@internals/utils' | ||
| import { collectUsedSchemaNames, createFile, createStreamInput } from '@kubb/ast' | ||
| import type { FileNode, InputMeta, InputStreamNode, OperationNode, SchemaNode } from '@kubb/ast' | ||
| import { version as coreVersion } from '../package.json' | ||
| import { OPERATION_FILTER_TYPES, SCHEMA_PARALLEL } from './constants.ts' | ||
| import { Fingerprint } from './Fingerprint.ts' | ||
| import { type Diagnostic, Diagnostics, type ProblemDiagnostic } from './diagnostics.ts' | ||
| import type { Cache } from './createCache.ts' | ||
| import type { RendererFactory } from './createRenderer.ts' | ||
@@ -108,5 +111,7 @@ import type { Storage } from './createStorage.ts' | ||
| const dependenciesByName = new Map(normalized.map((plugin) => [plugin.name, new Set(plugin.dependencies ?? [])])) | ||
| normalized.sort((a, b) => { | ||
| if (b.dependencies?.includes(a.name)) return -1 | ||
| if (a.dependencies?.includes(b.name)) return 1 | ||
| if (dependenciesByName.get(b.name)?.has(a.name)) return -1 | ||
| if (dependenciesByName.get(a.name)?.has(b.name)) return 1 | ||
@@ -356,4 +361,8 @@ return enforceOrder(a.enforce) - enforceOrder(b.enforce) | ||
| const updateBuffer: Array<{ file: FileNode; source?: string; processed: number; total: number; percentage: number }> = [] | ||
| // Final rendered source per output path, captured for the cache snapshot on a miss. Barrel | ||
| // files flow through here too, after the second `drain()`. | ||
| const snapshotSources = new Map<string, string>() | ||
| processor.hooks.on('update', (item) => { | ||
| updateBuffer.push(item) | ||
| if (item.source !== undefined) snapshotSources.set(item.file.path, item.source) | ||
| }) | ||
@@ -378,2 +387,12 @@ processor.hooks.on('end', async (files) => { | ||
| try { | ||
| const cache = config.cache | ||
| const outputRoot = resolve(config.root, config.output.path) | ||
| const cacheKey = cache ? await Fingerprint.compute({ config, adapterSource: this.#adapterSource, version: coreVersion }) : null | ||
| // On a cache hit, restore the snapshot and skip everything below. Skipping the work is the | ||
| // whole point, so the only event emitted is `kubb:build:end`, which reporters key off. | ||
| if (cache && cacheKey && (await this.#restoreSnapshot({ cache, cacheKey, outputRoot, storage }))) { | ||
| return { diagnostics: Diagnostics.dedupe(diagnostics) } | ||
| } | ||
| // Parse the adapter source into the streaming `InputNode`. | ||
@@ -437,4 +456,8 @@ await this.#parseInput() | ||
| await hooks.emit('kubb:build:end', { files: this.fileManager.files, config, outputDir: resolve(config.root, config.output.path) }) | ||
| await hooks.emit('kubb:build:end', { files: this.fileManager.files, config, outputDir: outputRoot }) | ||
| if (cache && cacheKey && !Diagnostics.hasError(diagnostics)) { | ||
| await this.#persistSnapshot({ cache, cacheKey, outputRoot, sources: snapshotSources }) | ||
| } | ||
| return { diagnostics: Diagnostics.dedupe(diagnostics) } | ||
@@ -451,2 +474,41 @@ } catch (caughtError) { | ||
| /** | ||
| * Writes a restored snapshot straight to storage and emits `kubb:build:end`. Returns `true` on a | ||
| * hit (the build is done), `false` on a miss so the caller falls through to a full build. | ||
| */ | ||
| async #restoreSnapshot({ cache, cacheKey, outputRoot, storage }: { cache: Cache; cacheKey: string; outputRoot: string; storage: Storage }): Promise<boolean> { | ||
| const snapshot = await cache.restore({ key: cacheKey }) | ||
| if (!snapshot) return false | ||
| for (const [relativePath, source] of Object.entries(snapshot.files)) { | ||
| const absolutePath = join(outputRoot, relativePath) | ||
| this.fileManager.upsert(createFile({ path: absolutePath, baseName: basename(relativePath) as `${string}.${string}` })) | ||
| await storage.setItem(absolutePath, source) | ||
| } | ||
| await this.hooks.emit('kubb:build:end', { files: this.fileManager.files, config: this.config, outputDir: outputRoot }) | ||
| return true | ||
| } | ||
| /** | ||
| * Stores this run's rendered output, keyed by the input fingerprint, so the next unchanged build | ||
| * restores it instead of regenerating. `sources` is keyed by absolute path and relativized here. | ||
| */ | ||
| async #persistSnapshot({ | ||
| cache, | ||
| cacheKey, | ||
| outputRoot, | ||
| sources, | ||
| }: { | ||
| cache: Cache | ||
| cacheKey: string | ||
| outputRoot: string | ||
| sources: ReadonlyMap<string, string> | ||
| }): Promise<void> { | ||
| const files: Record<string, string> = {} | ||
| for (const [absolutePath, source] of sources) { | ||
| files[relative(outputRoot, absolutePath)] = source | ||
| } | ||
| await cache.persist({ key: cacheKey, snapshot: { files } }) | ||
| } | ||
| // Returns a fresh object with a lazy `files` getter and a bound `upsertFile`. | ||
@@ -544,6 +606,4 @@ // Caller must use `Object.assign(extra, this.#filesPayload())`, not object spread. | ||
| // shares it too (previously it iterated its own copies). | ||
| const schemasBuffer: Array<SchemaNode> = [] | ||
| for await (const schema of schemas) schemasBuffer.push(schema) | ||
| const operationsBuffer: Array<OperationNode> = [] | ||
| for await (const operation of operations) operationsBuffer.push(operation) | ||
| const schemasBuffer: Array<SchemaNode> = await Array.fromAsync(schemas) | ||
| const operationsBuffer: Array<OperationNode> = await Array.fromAsync(operations) | ||
@@ -550,0 +610,0 @@ // Pre-scan: plugins with operation-based includes (but no schemaName include) need |
@@ -1,5 +0,4 @@ | ||
| 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 { access, glob, readFile, rm } from 'node:fs/promises' | ||
| import { join, relative, resolve } from 'node:path' | ||
| import { clean, isBun, toPosixPath, write } from '@internals/utils' | ||
| import { createStorage } from '../createStorage.ts' | ||
@@ -58,25 +57,18 @@ | ||
| async function* walk(dir: string, prefix: string): AsyncGenerator<string, void, undefined> { | ||
| let entries: Array<Dirent> | ||
| try { | ||
| entries = (await readdir(dir, { | ||
| withFileTypes: true, | ||
| })) as Array<Dirent> | ||
| } catch (_error) { | ||
| return | ||
| } | ||
| for (const entry of entries) { | ||
| const rel = prefix ? `${prefix}/${entry.name}` : entry.name | ||
| if (entry.isDirectory()) { | ||
| yield* walk(join(dir, entry.name), rel) | ||
| } else { | ||
| yield rel | ||
| if (isBun()) { | ||
| const bunGlob = new Bun.Glob('**/*') | ||
| return Array.fromAsync(bunGlob.scan({ cwd: resolvedBase, onlyFiles: true, dot: true })) | ||
| } | ||
| const keys: Array<string> = [] | ||
| try { | ||
| for await (const entry of glob('**/*', { cwd: resolvedBase, withFileTypes: true })) { | ||
| if (entry.isFile()) { | ||
| keys.push(toPosixPath(relative(resolvedBase, join(entry.parentPath, entry.name)))) | ||
| } | ||
| } | ||
| } catch (_error) { | ||
| // base directory does not exist yet | ||
| } | ||
| const keys: Array<string> = [] | ||
| for await (const key of walk(resolvedBase, '')) { | ||
| keys.push(key) | ||
| } | ||
| return keys | ||
@@ -83,0 +75,0 @@ }, |
+13
-1
| import { randomBytes } from 'node:crypto' | ||
| import os from 'node:os' | ||
| import process from 'node:process' | ||
| import { executeIfOnline, isCIEnvironment } from '@internals/utils' | ||
| import { executeIfOnline, getRuntimeName, getRuntimeVersion, isCIEnvironment, type RuntimeName } from '@internals/utils' | ||
| import { OTLP_ENDPOINT } from './constants.ts' | ||
@@ -117,2 +117,10 @@ | ||
| nodeVersion: string | ||
| /** | ||
| * Name of the JavaScript runtime that executed the run, `'bun'`, `'deno'`, or `'node'`. | ||
| */ | ||
| runtime: RuntimeName | ||
| /** | ||
| * Major version of the active runtime, e.g. `'1'` under Bun or `'22'` under Node. | ||
| */ | ||
| runtimeVersion: string | ||
| platform: string | ||
@@ -162,2 +170,4 @@ ci: boolean | ||
| nodeVersion: process.versions.node.split('.')[0] as string, | ||
| runtime: getRuntimeName(), | ||
| runtimeVersion: getRuntimeVersion().split('.')[0] as string, | ||
| platform: os.platform(), | ||
@@ -186,2 +196,4 @@ ci: isCIEnvironment(), | ||
| { key: 'kubb.node_version', value: { stringValue: event.nodeVersion } }, | ||
| { key: 'kubb.runtime', value: { stringValue: event.runtime } }, | ||
| { key: 'kubb.runtime_version', value: { stringValue: event.runtimeVersion } }, | ||
| { key: 'kubb.platform', value: { stringValue: event.platform } }, | ||
@@ -188,0 +200,0 @@ { key: 'kubb.ci', value: { boolValue: event.ci } }, |
+2
-0
| export type { Adapter, AdapterFactoryOptions, AdapterSource } from './createAdapter.ts' | ||
| export type { Cache, CachedSnapshot } from './createCache.ts' | ||
| export type { FsCacheOptions } from './caches/fsCache.ts' | ||
| export type { | ||
@@ -3,0 +5,0 @@ Diagnostic, |
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
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.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
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.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
1316203
8.17%51
8.51%18863
7.73%+ Added
+ Added
- Removed
- Removed
Updated