//#region src/unplugin/disk-cache.d.ts /** * Persistent transform-result cache. * * The expensive part of the unplugin transform is discovery: executing the * schema file (and transitively its whole first-party import graph) through * jiti inside the bundler's single-threaded server process. The in-memory * caches die with the process, so every `vitest run` / build re-pays that * cost even when nothing changed — which is exactly the loop integration * tests live in. * * Entries are keyed by a hash of (plugin version, zod version, transform * options, file id, file content) and validated against a recorded snapshot * of first-party files. Validation uses an mtime+size fast path and falls * back to content hashing, so a `touch` without changes still hits. A * superset of true dependencies only over-invalidates, never serves stale * output. * * Layout (CACHE_FORMAT 2): dependency snapshots are CONTENT-ADDRESSED and * shared — `deps/.json` holds the {path → hash/mtime/size} map, and * each entry stores only the dep-set id. The v1 format inlined the full dep * map into every entry; in a large-codebase field report 835 superset- * fallback entries each embedded a ~1,900-file point-in-time snapshot (813 * DISTINCT snapshots — the executed-modules superset grows as the build * progresses, so per-entry copies cannot even dedupe), totalling 283 MB * that any commit invalidated wholesale. Sharing dep-sets also means each * unique set is parsed and validated once per process instead of once per * entry. * * Superset fallbacks (entries whose static dep crawl was incomplete) are * DEFERRED: queued in memory and flushed in buildEnd / process exit against * a single end-of-build superset snapshot, so every superset entry of a * build shares ONE dep-set file (recording the still-growing snapshot at * save time is what produced 813 distinct copies). Deferred entries are * dropped on watchChange — a dependency edited between queueing and flush * would pair post-change hashes with a pre-change result, which is the one * combination that can serve stale output. A killed process loses only its * pending superset entries (the static-complete majority persists * immediately); the next run re-pays discovery for those files alone. * * Writes are atomic (tmp file + rename) so concurrent bundler processes * (vitest workspace projects) can share one cache directory safely. */ interface CacheEntryStats { schemas: number; optimized: number; } interface CacheEntry { /** Transformed code, or null when the transform produced no change. */ result: string | null; /** Content-addressed id of the shared dep-set file (deps/.json). */ depset: string; /** Build stats to replay on cache hits (only present when schemas compiled). */ stats?: CacheEntryStats; /** Composed sourcemap for `result` (original → transformed). */ map?: CacheSourceMap | null; } /** JSON shape of the persisted sourcemap (mirrors TransformSourceMap). */ interface CacheSourceMap { version: number; sources: (string | null)[]; sourcesContent?: (string | null)[]; names: string[]; mappings: string; file?: string | null; } /** * Build fingerprint: a content hash of the package's own source trees. * * The published version string alone is not enough — file: installs, linked * monorepo packages, and canary builds rebuild the compiler without a version * bump, and serving codegen from an older compiler build would be silently * stale. * * CONTENT-addressed, for the same reason depset ids are: mtimes do not survive * an install. pnpm rewrites every mtime under its `copy` import method, and * copy is what you get whenever the store sits on a different filesystem than * the workspace — the default on CI runners that mount the store as its own * volume (`npm_config_package_import_method=copy`). An mtime fingerprint * therefore rotates on every `pnpm install --frozen-lockfile`, which changes * every cache key and makes a restored CI cache wholly unreachable: the archive * unpacks, and nothing in it is ever looked up. Hardlink and APFS-clone installs * DO preserve mtimes, so the bug hides locally and reproduces only on the * runners that need the cache most. * * A content hash is also strictly tighter than mtime for the stated purpose: a * rebuild that reproduces identical bytes no longer discards the whole cache. * * Hashing ~0.7 MB across ~250 files costs ~5ms — once per process, and only if * a key is actually built (see `keyPrefix`). */ declare function computeBuildFingerprint(root: string): string; /** Reset the per-process dep validation memos (watch-mode file changes). */ declare function resetDepValidationMemo(): void; declare class DiskCache { private readonly dir; private readonly optionsKey; /** Superset snapshot provider (loader's executed first-party modules). */ private readonly superset; private pending; private initialized; constructor(dir: string, optionsKey: string, superset?: () => string[] | null); /** * Resolve the cache directory: an explicit string wins; otherwise * node_modules/.cache/zod-compiler under cwd (falling back to a * project-local .zod-compiler-cache when node_modules doesn't exist). */ static resolveDir(cacheOption: string | true): string; key(id: string, code: string): string; private entryPath; private depsetPath; /** * One-time directory init: wipe on format mismatch (v1 inline-deps caches * reached 283 MB in the field — disposable by definition), then a * throttled GC pass. Best-effort throughout; a concurrent wipe/GC from * another process can only cause cache misses, never stale hits. */ private ensureDir; /** * Throttled sweep: entries older than MAX_ENTRY_AGE_MS (orphaned by key * churn — content/version/options keys never repeat once inputs change) * and dep-set files no surviving entry references. Runs at most once per * GC_INTERVAL_MS per directory; the marker is claimed BEFORE sweeping so * concurrent processes skip. Deleting a dep-set raced by a concurrent * entry write only costs that entry a future miss — save() re-creates * absent dep-set files. */ private maybeGc; /** Load an entry and validate its dep-set. Any failure → null. */ load(key: string): CacheEntry | null; /** * Validate every dep in a dep-set, once per process per set: superset * entries all share one set, so the ~N-file validation (and the JSON * parse) happens once instead of once per entry. */ private validateDepset; /** * Stat + hash every dep into a content-addressed record map. The id hashes * sorted (path, content-hash) pairs ONLY — mtimes are validation fast-path * hints and must not fork the file name across checkouts/touches. Returns * null when any dep cannot be read (an unvalidatable set must not persist). */ private buildDepset; /** Write a dep-set file if absent (content-addressed: same id ⟹ same bytes). */ private writeDepset; private writeEntry; /** Persist an entry whose dependency set is fully known (static crawl complete). */ save(key: string, result: string | null, depPaths: readonly string[], stats?: CacheEntryStats, map?: CacheSourceMap | null): void; /** * Queue an entry whose static dep crawl was incomplete. Persisted by * flushDeferred() against ONE end-of-build superset snapshot — recording * the snapshot at save time gave every entry a distinct point-in-time * copy (the executed-modules set grows as discovery progresses). */ saveDeferred(key: string, result: string | null, stats?: CacheEntryStats, map?: CacheSourceMap | null): void; /** * Flush queued superset entries against the current loader snapshot. * Wired to buildEnd and (as a fallback) process exit; idempotent. */ flushDeferred(): void; /** * Discard queued superset entries (watch-mode file change): their results * predate the change, but a flush would record post-change dep hashes — * the one pairing that could validate a stale result. */ dropDeferred(): void; } //#endregion export { CacheEntry, CacheEntryStats, CacheSourceMap, DiskCache, computeBuildFingerprint, resetDepValidationMemo }; //# sourceMappingURL=disk-cache.d.ts.map