zod-compiler 1.27.0 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -188,6 +188,7 @@ has neither — see [React Native / Expo](#react-native--expo).
188
188
  | `apply` | `"build" \| "serve" \| "all"` | builds + Vitest | **Vite only**: when the plugin runs |
189
189
  | `codegenMode` | `"lean" \| "inline"` | auto | `"inline"` emits helpers per file; needed for transpile-only esbuild — see [SWC](#swc) |
190
190
  | `cache` | `boolean \| string` | `true` | Persistent transform cache in `node_modules/.cache/zod-compiler` |
191
+ | `parallel` | `boolean \| number` | `false` | Run transforms on worker threads — see [Parallel Transforms](#parallel-transforms) |
191
192
 
192
193
  ```typescript
193
194
  zodCompiler({
@@ -412,6 +413,41 @@ expensive one — later runs hit the persistent cache.
412
413
  Scope discovery with `include`; set `ZOD_COMPILER_TIMING=1` for a per-phase breakdown. Files that
413
414
  never mention `zod` cost nothing.
414
415
 
416
+ ### Parallel Transforms
417
+
418
+ Discovery runs one file at a time on the bundler's own thread — executions are serialized so
419
+ concurrent transforms cannot double-execute a shared dependency. `parallel` moves whole transforms
420
+ onto worker threads instead, each with its own loader and module cache, which is what makes running
421
+ them at the same time sound.
422
+
423
+ ```typescript
424
+ zodCompiler({ parallel: true }); // one worker per core, less one, capped at 4
425
+ zodCompiler({ parallel: 2 }); // or pick the count yourself
426
+ ```
427
+
428
+ **Whether it pays depends on your import graph, not your core count.** A module shared by many
429
+ schema files is executed once in-process and once _per worker_ here. Files with independent graphs
430
+ win; files chained through each other can lose. Both rows below are 120 files of 8 schemas each, on
431
+ 12 performance cores — the only difference is whether the files import one another:
432
+
433
+ | Transform (120 files) | in-process | n=2 | n=4 | n=8 | n=12 |
434
+ | --------------------- | ---------: | -------: | -------: | -------: | -------: |
435
+ | independent graphs | 3,633 ms | 2,263 ms | 1,508 ms | 1,786 ms | 2,119 ms |
436
+ | 120-deep import chain | 945 ms | 977 ms | 1,045 ms | 1,796 ms | 3,332 ms |
437
+
438
+ So measure before adopting it — `ZOD_COMPILER_TIMING=1` prints the per-phase breakdown, and the
439
+ `discover` line is the one workers move. Throughput peaks around four workers and declines past it:
440
+ beyond that point every extra worker re-executes more graph, holds another copy of it in memory, and
441
+ adds to the generated source that the single receiving thread has to deserialize.
442
+
443
+ Emitted code, sourcemaps and cache entries are identical either way — `parallel` is not part of the
444
+ cache key, so a parallel build and a serial one share the same cache. The disk cache and dependency
445
+ crawling stay on the bundler thread, and if a worker cannot start or dies mid-build its file is
446
+ retried in-process rather than failing the build.
447
+
448
+ A **warm cache still beats parallelism**, and costs no memory — reach for `parallel` for the cold
449
+ runs the cache cannot help with.
450
+
415
451
  ## Framework Examples
416
452
 
417
453
  Nothing framework-specific is needed — exported schemas are compiled in place, so anything accepting
package/dist/swc.d.ts CHANGED
@@ -26,8 +26,12 @@ interface SwcCoreLike {
26
26
  * persistent cache, so hosts that need one must key transform results on
27
27
  * content themselves. `include`/`exclude` are honored: files they reject
28
28
  * pass through to SWC without the zod-compiler step.
29
+ *
30
+ * No `parallel` either: this is a per-file transformer, so whatever drives it
31
+ * decides how many files run at once. A pool owned by a single `transform()`
32
+ * call would compete with that instead of adding to it.
29
33
  */
30
- type ZodCompilerSwcOptions = Omit<ZodCompilerPluginOptions, "apply" | "cache" | "codegenMode"> & {
34
+ type ZodCompilerSwcOptions = Omit<ZodCompilerPluginOptions, "apply" | "cache" | "codegenMode" | "parallel"> & {
31
35
  /**
32
36
  * SWC is a transformer, not a bundler plugin host, so inline is the safe
33
37
  * default. Lean mode may emit virtual runtime imports that SWC cannot resolve
package/dist/swc.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"swc.d.ts","names":[],"sources":["../src/swc.ts"],"mappings":";;;;;;;;UAYiB;EACf;EACA;EACA;GACC;;UAGc;EACf;EACA;GACC;;UAGc;EACf,UAAU,cAAc,UAAU,aAAa,QAAQ;;;;;;;;;KAU7C,wBAAwB,KAClC;;;;;;EAQA;;UAGe;;EAEf,MAAM;;EAEN,cAAc;;UAGC,kCAAkC;;EAEjD;;UAGe;EACf,UAAU,cAAc,SAAS,4BAA4B,QAAQ;EACrE,cAAc,kBAAkB,UAAU,oBAAoB,QAAQ;;iBAoFlD,UACpB,cACA,SAAS,4BACR,QAAQ;iBAIW,cACpB,kBACA,UAAU,oBACT,QAAQ;iBAKK,kBAAkB,WAAW,oBAAoB;;;;;iBAgB3C,iBACpB,KAAK,aACL,cACA,SAAS,4BACR,QAAQ;iBAIa,YAAY,WAAW,oBAAoB"}
1
+ {"version":3,"file":"swc.d.ts","names":[],"sources":["../src/swc.ts"],"mappings":";;;;;;;;UAYiB;EACf;EACA;EACA;GACC;;UAGc;EACf;EACA;GACC;;UAGc;EACf,UAAU,cAAc,UAAU,aAAa,QAAQ;;;;;;;;;;;;;KAc7C,wBAAwB,KAClC;;;;;;EAQA;;UAGe;;EAEf,MAAM;;EAEN,cAAc;;UAGC,kCAAkC;;EAEjD;;UAGe;EACf,UAAU,cAAc,SAAS,4BAA4B,QAAQ;EACrE,cAAc,kBAAkB,UAAU,oBAAoB,QAAQ;;iBAoFlD,UACpB,cACA,SAAS,4BACR,QAAQ;iBAIW,cACpB,kBACA,UAAU,oBACT,QAAQ;iBAKK,kBAAkB,WAAW,oBAAoB;;;;;iBAgB3C,iBACpB,KAAK,aACL,cACA,SAAS,4BACR,QAAQ;iBAIa,YAAY,WAAW,oBAAoB"}
package/dist/swc.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"swc.js","names":[],"sources":["../src/swc.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { invalidateModuleCache } from \"./loader.js\";\nimport { shouldTransform, transformCodeWithMap } from \"./unplugin/transform.js\";\nimport type { TransformOptions, ZodCompilerPluginOptions } from \"./unplugin/types.js\";\n\n/**\n * Minimal @swc/core option shape. zod-compiler keeps @swc/core optional so\n * non-SWC users do not install a native dependency just by using the package.\n * `inputSourceMap` matches @swc/core: a JSON string or a boolean — swc does\n * not accept map objects.\n */\nexport interface SwcOptions {\n filename?: string | undefined;\n inputSourceMap?: boolean | string | undefined;\n sourceMaps?: boolean | \"inline\" | undefined;\n [key: string]: unknown;\n}\n\nexport interface SwcOutput {\n code: string;\n map?: string | undefined;\n [key: string]: unknown;\n}\n\nexport interface SwcCoreLike {\n transform(code: string, options?: SwcOptions): Promise<SwcOutput>;\n}\n\n/**\n * Plugin options that make sense for a transformer host. `apply` is Vite\n * lifecycle and `cache` is the bundler disk cache — the bridge keeps no\n * persistent cache, so hosts that need one must key transform results on\n * content themselves. `include`/`exclude` are honored: files they reject\n * pass through to SWC without the zod-compiler step.\n */\nexport type ZodCompilerSwcOptions = Omit<\n ZodCompilerPluginOptions,\n \"apply\" | \"cache\" | \"codegenMode\"\n> & {\n /**\n * SWC is a transformer, not a bundler plugin host, so inline is the safe\n * default. Lean mode may emit virtual runtime imports that SWC cannot resolve\n * unless another tool handles them after SWC.\n */\n codegenMode?: \"inline\" | \"lean\" | undefined;\n};\n\nexport interface SwcBridgeDefaults {\n /** Options passed through to @swc/core.transform. */\n swc?: SwcOptions | undefined;\n /** zod-compiler options. Defaults match the build plugins except codegenMode is \"inline\". */\n zodCompiler?: ZodCompilerSwcOptions | undefined;\n}\n\nexport interface SwcBridgeTransformOptions extends SwcBridgeDefaults {\n /** Absolute or project-relative filename for schema discovery and SWC config resolution. */\n filename: string;\n}\n\nexport interface SwcBridge {\n transform(code: string, options: SwcBridgeTransformOptions): Promise<SwcOutput>;\n transformFile(filename: string, options?: SwcBridgeDefaults): Promise<SwcOutput>;\n}\n\nfunction toTransformOptions(options?: ZodCompilerSwcOptions): TransformOptions {\n const output = options?.output ?? \"schema\";\n return {\n mode: options?.codegenMode ?? \"inline\",\n verbose: options?.verbose,\n zodCompat: output === \"schema\" || output === \"compact\",\n compact: output === \"compact\",\n autoDiscover: (options?.schemas ?? \"auto\") === \"auto\",\n hoist: options?.hoist,\n };\n}\n\n/**\n * Per-call options win key-by-key over factory defaults. The merge is\n * shallow: a per-call `swc.jsc` replaces the default `jsc` wholesale rather\n * than deep-merging parser/target settings.\n */\nfunction mergeOptions(\n defaults: SwcBridgeDefaults | undefined,\n options: SwcBridgeTransformOptions,\n): SwcBridgeTransformOptions {\n return {\n filename: options.filename,\n swc: { ...defaults?.swc, ...options.swc },\n zodCompiler: { ...defaults?.zodCompiler, ...options.zodCompiler },\n };\n}\n\nasync function loadSwc(): Promise<SwcCoreLike> {\n try {\n return (await import(\"@swc/core\")) as unknown as SwcCoreLike;\n } catch (error) {\n const cause = error instanceof Error ? `: ${error.message}` : \"\";\n throw new Error(\n `zod-compiler/swc requires @swc/core to be installed by the consuming project${cause}`,\n );\n }\n}\n\n/**\n * Last content seen per filename. Discovery executes schema files from DISK\n * through a module cache that outlives transform calls, so when a host (dev\n * server, watch-mode test runner) re-transforms a file with new content, the\n * stale executions must be dropped or the compiled validators keep\n * reflecting the old schema. Same content-diff scheme as the unplugin\n * transform hook; tracked for every file fed to the bridge — an excluded\n * file can still be a dependency a schema file executed.\n */\nconst lastSeenCode = new Map<string, string>();\n\nfunction invalidateOnContentChange(filename: string, code: string): void {\n const key = path.resolve(filename);\n const previous = lastSeenCode.get(key);\n if (previous !== undefined && previous !== code) {\n invalidateModuleCache();\n }\n lastSeenCode.set(key, code);\n}\n\nasync function transformWith(\n swc: SwcCoreLike,\n code: string,\n options: SwcBridgeTransformOptions,\n): Promise<SwcOutput> {\n invalidateOnContentChange(options.filename, code);\n\n const zodResult = shouldTransform(options.filename, options.zodCompiler)\n ? await transformCodeWithMap(code, options.filename, toTransformOptions(options.zodCompiler))\n : null;\n\n const swcOptions: SwcOptions = {\n ...options.swc,\n filename: options.swc?.filename ?? options.filename,\n };\n if (zodResult?.map && swcOptions.inputSourceMap === undefined) {\n swcOptions.inputSourceMap = JSON.stringify(zodResult.map);\n }\n\n return swc.transform(zodResult?.code ?? code, swcOptions);\n}\n\nexport async function transform(\n code: string,\n options: SwcBridgeTransformOptions,\n): Promise<SwcOutput> {\n return transformWith(await loadSwc(), code, options);\n}\n\nexport async function transformFile(\n filename: string,\n options?: SwcBridgeDefaults,\n): Promise<SwcOutput> {\n const code = await fs.readFile(filename, \"utf8\");\n return transform(code, { ...options, filename });\n}\n\nexport function createSwcCompiler(defaults?: SwcBridgeDefaults): SwcBridge {\n return {\n transform(code, options) {\n return transform(code, mergeOptions(defaults, options));\n },\n async transformFile(filename, options) {\n const code = await fs.readFile(filename, \"utf8\");\n return transform(code, mergeOptions(defaults, { ...options, filename }));\n },\n };\n}\n\n/**\n * Test seam for the SWC bridge. It is exported because it is also useful for\n * custom hosts that already own a @swc/core-compatible transform function.\n */\nexport async function transformWithSwc(\n swc: SwcCoreLike,\n code: string,\n options: SwcBridgeTransformOptions,\n): Promise<SwcOutput> {\n return transformWith(swc, code, options);\n}\n\nexport default function zodCompiler(defaults?: SwcBridgeDefaults): SwcBridge {\n return createSwcCompiler(defaults);\n}\n"],"mappings":";;;;;AAiEA,SAAS,mBAAmB,SAAmD;CAC7E,MAAM,SAAS,SAAS,UAAU;CAClC,OAAO;EACL,MAAM,SAAS,eAAe;EAC9B,SAAS,SAAS;EAClB,WAAW,WAAW,YAAY,WAAW;EAC7C,SAAS,WAAW;EACpB,eAAe,SAAS,WAAW,YAAY;EAC/C,OAAO,SAAS;CAClB;AACF;;;;;;AAOA,SAAS,aACP,UACA,SAC2B;CAC3B,OAAO;EACL,UAAU,QAAQ;EAClB,KAAK;GAAE,GAAG,UAAU;GAAK,GAAG,QAAQ;EAAI;EACxC,aAAa;GAAE,GAAG,UAAU;GAAa,GAAG,QAAQ;EAAY;CAClE;AACF;AAEA,eAAe,UAAgC;CAC7C,IAAI;EACF,OAAQ,MAAM,OAAO;CACvB,SAAS,OAAO;EACd,MAAM,QAAQ,iBAAiB,QAAQ,KAAK,MAAM,YAAY;EAC9D,MAAM,IAAI,MACR,+EAA+E,OACjF;CACF;AACF;;;;;;;;;;AAWA,MAAM,+BAAe,IAAI,IAAoB;AAE7C,SAAS,0BAA0B,UAAkB,MAAoB;CACvE,MAAM,MAAM,KAAK,QAAQ,QAAQ;CACjC,MAAM,WAAW,aAAa,IAAI,GAAG;CACrC,IAAI,aAAa,KAAA,KAAa,aAAa,MACzC,sBAAsB;CAExB,aAAa,IAAI,KAAK,IAAI;AAC5B;AAEA,eAAe,cACb,KACA,MACA,SACoB;CACpB,0BAA0B,QAAQ,UAAU,IAAI;CAEhD,MAAM,YAAY,gBAAgB,QAAQ,UAAU,QAAQ,WAAW,IACnE,MAAM,qBAAqB,MAAM,QAAQ,UAAU,mBAAmB,QAAQ,WAAW,CAAC,IAC1F;CAEJ,MAAM,aAAyB;EAC7B,GAAG,QAAQ;EACX,UAAU,QAAQ,KAAK,YAAY,QAAQ;CAC7C;CACA,IAAI,WAAW,OAAO,WAAW,mBAAmB,KAAA,GAClD,WAAW,iBAAiB,KAAK,UAAU,UAAU,GAAG;CAG1D,OAAO,IAAI,UAAU,WAAW,QAAQ,MAAM,UAAU;AAC1D;AAEA,eAAsB,UACpB,MACA,SACoB;CACpB,OAAO,cAAc,MAAM,QAAQ,GAAG,MAAM,OAAO;AACrD;AAEA,eAAsB,cACpB,UACA,SACoB;CAEpB,OAAO,UAAU,MADE,GAAG,SAAS,UAAU,MAAM,GACxB;EAAE,GAAG;EAAS;CAAS,CAAC;AACjD;AAEA,SAAgB,kBAAkB,UAAyC;CACzE,OAAO;EACL,UAAU,MAAM,SAAS;GACvB,OAAO,UAAU,MAAM,aAAa,UAAU,OAAO,CAAC;EACxD;EACA,MAAM,cAAc,UAAU,SAAS;GAErC,OAAO,UAAU,MADE,GAAG,SAAS,UAAU,MAAM,GACxB,aAAa,UAAU;IAAE,GAAG;IAAS;GAAS,CAAC,CAAC;EACzE;CACF;AACF;;;;;AAMA,eAAsB,iBACpB,KACA,MACA,SACoB;CACpB,OAAO,cAAc,KAAK,MAAM,OAAO;AACzC;AAEA,SAAwB,YAAY,UAAyC;CAC3E,OAAO,kBAAkB,QAAQ;AACnC"}
1
+ {"version":3,"file":"swc.js","names":[],"sources":["../src/swc.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { invalidateModuleCache } from \"./loader.js\";\nimport { shouldTransform, transformCodeWithMap } from \"./unplugin/transform.js\";\nimport type { TransformOptions, ZodCompilerPluginOptions } from \"./unplugin/types.js\";\n\n/**\n * Minimal @swc/core option shape. zod-compiler keeps @swc/core optional so\n * non-SWC users do not install a native dependency just by using the package.\n * `inputSourceMap` matches @swc/core: a JSON string or a boolean — swc does\n * not accept map objects.\n */\nexport interface SwcOptions {\n filename?: string | undefined;\n inputSourceMap?: boolean | string | undefined;\n sourceMaps?: boolean | \"inline\" | undefined;\n [key: string]: unknown;\n}\n\nexport interface SwcOutput {\n code: string;\n map?: string | undefined;\n [key: string]: unknown;\n}\n\nexport interface SwcCoreLike {\n transform(code: string, options?: SwcOptions): Promise<SwcOutput>;\n}\n\n/**\n * Plugin options that make sense for a transformer host. `apply` is Vite\n * lifecycle and `cache` is the bundler disk cache — the bridge keeps no\n * persistent cache, so hosts that need one must key transform results on\n * content themselves. `include`/`exclude` are honored: files they reject\n * pass through to SWC without the zod-compiler step.\n *\n * No `parallel` either: this is a per-file transformer, so whatever drives it\n * decides how many files run at once. A pool owned by a single `transform()`\n * call would compete with that instead of adding to it.\n */\nexport type ZodCompilerSwcOptions = Omit<\n ZodCompilerPluginOptions,\n \"apply\" | \"cache\" | \"codegenMode\" | \"parallel\"\n> & {\n /**\n * SWC is a transformer, not a bundler plugin host, so inline is the safe\n * default. Lean mode may emit virtual runtime imports that SWC cannot resolve\n * unless another tool handles them after SWC.\n */\n codegenMode?: \"inline\" | \"lean\" | undefined;\n};\n\nexport interface SwcBridgeDefaults {\n /** Options passed through to @swc/core.transform. */\n swc?: SwcOptions | undefined;\n /** zod-compiler options. Defaults match the build plugins except codegenMode is \"inline\". */\n zodCompiler?: ZodCompilerSwcOptions | undefined;\n}\n\nexport interface SwcBridgeTransformOptions extends SwcBridgeDefaults {\n /** Absolute or project-relative filename for schema discovery and SWC config resolution. */\n filename: string;\n}\n\nexport interface SwcBridge {\n transform(code: string, options: SwcBridgeTransformOptions): Promise<SwcOutput>;\n transformFile(filename: string, options?: SwcBridgeDefaults): Promise<SwcOutput>;\n}\n\nfunction toTransformOptions(options?: ZodCompilerSwcOptions): TransformOptions {\n const output = options?.output ?? \"schema\";\n return {\n mode: options?.codegenMode ?? \"inline\",\n verbose: options?.verbose,\n zodCompat: output === \"schema\" || output === \"compact\",\n compact: output === \"compact\",\n autoDiscover: (options?.schemas ?? \"auto\") === \"auto\",\n hoist: options?.hoist,\n };\n}\n\n/**\n * Per-call options win key-by-key over factory defaults. The merge is\n * shallow: a per-call `swc.jsc` replaces the default `jsc` wholesale rather\n * than deep-merging parser/target settings.\n */\nfunction mergeOptions(\n defaults: SwcBridgeDefaults | undefined,\n options: SwcBridgeTransformOptions,\n): SwcBridgeTransformOptions {\n return {\n filename: options.filename,\n swc: { ...defaults?.swc, ...options.swc },\n zodCompiler: { ...defaults?.zodCompiler, ...options.zodCompiler },\n };\n}\n\nasync function loadSwc(): Promise<SwcCoreLike> {\n try {\n return (await import(\"@swc/core\")) as unknown as SwcCoreLike;\n } catch (error) {\n const cause = error instanceof Error ? `: ${error.message}` : \"\";\n throw new Error(\n `zod-compiler/swc requires @swc/core to be installed by the consuming project${cause}`,\n );\n }\n}\n\n/**\n * Last content seen per filename. Discovery executes schema files from DISK\n * through a module cache that outlives transform calls, so when a host (dev\n * server, watch-mode test runner) re-transforms a file with new content, the\n * stale executions must be dropped or the compiled validators keep\n * reflecting the old schema. Same content-diff scheme as the unplugin\n * transform hook; tracked for every file fed to the bridge — an excluded\n * file can still be a dependency a schema file executed.\n */\nconst lastSeenCode = new Map<string, string>();\n\nfunction invalidateOnContentChange(filename: string, code: string): void {\n const key = path.resolve(filename);\n const previous = lastSeenCode.get(key);\n if (previous !== undefined && previous !== code) {\n invalidateModuleCache();\n }\n lastSeenCode.set(key, code);\n}\n\nasync function transformWith(\n swc: SwcCoreLike,\n code: string,\n options: SwcBridgeTransformOptions,\n): Promise<SwcOutput> {\n invalidateOnContentChange(options.filename, code);\n\n const zodResult = shouldTransform(options.filename, options.zodCompiler)\n ? await transformCodeWithMap(code, options.filename, toTransformOptions(options.zodCompiler))\n : null;\n\n const swcOptions: SwcOptions = {\n ...options.swc,\n filename: options.swc?.filename ?? options.filename,\n };\n if (zodResult?.map && swcOptions.inputSourceMap === undefined) {\n swcOptions.inputSourceMap = JSON.stringify(zodResult.map);\n }\n\n return swc.transform(zodResult?.code ?? code, swcOptions);\n}\n\nexport async function transform(\n code: string,\n options: SwcBridgeTransformOptions,\n): Promise<SwcOutput> {\n return transformWith(await loadSwc(), code, options);\n}\n\nexport async function transformFile(\n filename: string,\n options?: SwcBridgeDefaults,\n): Promise<SwcOutput> {\n const code = await fs.readFile(filename, \"utf8\");\n return transform(code, { ...options, filename });\n}\n\nexport function createSwcCompiler(defaults?: SwcBridgeDefaults): SwcBridge {\n return {\n transform(code, options) {\n return transform(code, mergeOptions(defaults, options));\n },\n async transformFile(filename, options) {\n const code = await fs.readFile(filename, \"utf8\");\n return transform(code, mergeOptions(defaults, { ...options, filename }));\n },\n };\n}\n\n/**\n * Test seam for the SWC bridge. It is exported because it is also useful for\n * custom hosts that already own a @swc/core-compatible transform function.\n */\nexport async function transformWithSwc(\n swc: SwcCoreLike,\n code: string,\n options: SwcBridgeTransformOptions,\n): Promise<SwcOutput> {\n return transformWith(swc, code, options);\n}\n\nexport default function zodCompiler(defaults?: SwcBridgeDefaults): SwcBridge {\n return createSwcCompiler(defaults);\n}\n"],"mappings":";;;;;AAqEA,SAAS,mBAAmB,SAAmD;CAC7E,MAAM,SAAS,SAAS,UAAU;CAClC,OAAO;EACL,MAAM,SAAS,eAAe;EAC9B,SAAS,SAAS;EAClB,WAAW,WAAW,YAAY,WAAW;EAC7C,SAAS,WAAW;EACpB,eAAe,SAAS,WAAW,YAAY;EAC/C,OAAO,SAAS;CAClB;AACF;;;;;;AAOA,SAAS,aACP,UACA,SAC2B;CAC3B,OAAO;EACL,UAAU,QAAQ;EAClB,KAAK;GAAE,GAAG,UAAU;GAAK,GAAG,QAAQ;EAAI;EACxC,aAAa;GAAE,GAAG,UAAU;GAAa,GAAG,QAAQ;EAAY;CAClE;AACF;AAEA,eAAe,UAAgC;CAC7C,IAAI;EACF,OAAQ,MAAM,OAAO;CACvB,SAAS,OAAO;EACd,MAAM,QAAQ,iBAAiB,QAAQ,KAAK,MAAM,YAAY;EAC9D,MAAM,IAAI,MACR,+EAA+E,OACjF;CACF;AACF;;;;;;;;;;AAWA,MAAM,+BAAe,IAAI,IAAoB;AAE7C,SAAS,0BAA0B,UAAkB,MAAoB;CACvE,MAAM,MAAM,KAAK,QAAQ,QAAQ;CACjC,MAAM,WAAW,aAAa,IAAI,GAAG;CACrC,IAAI,aAAa,KAAA,KAAa,aAAa,MACzC,sBAAsB;CAExB,aAAa,IAAI,KAAK,IAAI;AAC5B;AAEA,eAAe,cACb,KACA,MACA,SACoB;CACpB,0BAA0B,QAAQ,UAAU,IAAI;CAEhD,MAAM,YAAY,gBAAgB,QAAQ,UAAU,QAAQ,WAAW,IACnE,MAAM,qBAAqB,MAAM,QAAQ,UAAU,mBAAmB,QAAQ,WAAW,CAAC,IAC1F;CAEJ,MAAM,aAAyB;EAC7B,GAAG,QAAQ;EACX,UAAU,QAAQ,KAAK,YAAY,QAAQ;CAC7C;CACA,IAAI,WAAW,OAAO,WAAW,mBAAmB,KAAA,GAClD,WAAW,iBAAiB,KAAK,UAAU,UAAU,GAAG;CAG1D,OAAO,IAAI,UAAU,WAAW,QAAQ,MAAM,UAAU;AAC1D;AAEA,eAAsB,UACpB,MACA,SACoB;CACpB,OAAO,cAAc,MAAM,QAAQ,GAAG,MAAM,OAAO;AACrD;AAEA,eAAsB,cACpB,UACA,SACoB;CAEpB,OAAO,UAAU,MADE,GAAG,SAAS,UAAU,MAAM,GACxB;EAAE,GAAG;EAAS;CAAS,CAAC;AACjD;AAEA,SAAgB,kBAAkB,UAAyC;CACzE,OAAO;EACL,UAAU,MAAM,SAAS;GACvB,OAAO,UAAU,MAAM,aAAa,UAAU,OAAO,CAAC;EACxD;EACA,MAAM,cAAc,UAAU,SAAS;GAErC,OAAO,UAAU,MADE,GAAG,SAAS,UAAU,MAAM,GACxB,aAAa,UAAU;IAAE,GAAG;IAAS;GAAS,CAAC,CAAC;EACzE;CACF;AACF;;;;;AAMA,eAAsB,iBACpB,KACA,MACA,SACoB;CACpB,OAAO,cAAc,KAAK,MAAM,OAAO;AACzC;AAEA,SAAwB,YAAY,UAAyC;CAC3E,OAAO,kBAAkB,QAAQ;AACnC"}
@@ -12,8 +12,13 @@ import { TransformSourceMap } from "./unplugin/transform.js";
12
12
  * `buildEnd` flush that a loader has no equivalent for, and loader hosts keep
13
13
  * their own persistent result cache — Turbopack's is keyed on content plus the
14
14
  * dependencies declared below, which is what this would have re-implemented.
15
+ *
16
+ * No `parallel`: the concurrency is the host's to own. Turbopack already runs
17
+ * loaders across its own worker pool, so a pool per loader invocation would
18
+ * multiply threads against a machine that is already saturated — and the
19
+ * module-cache staleness stamps below assume one shared execution cache.
15
20
  */
16
- type ZodCompilerTurbopackOptions = Omit<ZodCompilerPluginOptions, "apply" | "cache" | "codegenMode" | "hoist"> & {
21
+ type ZodCompilerTurbopackOptions = Omit<ZodCompilerPluginOptions, "apply" | "cache" | "codegenMode" | "hoist" | "parallel"> & {
17
22
  hoist?: boolean | {
18
23
  schemaNamePattern?: string | null | undefined;
19
24
  } | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"turbopack.d.ts","names":[],"sources":["../src/turbopack.ts"],"mappings":";;;;;;;;;;;;;;;KA+DY,8BAA8B,KACxC;EAGA;IAAoB;;;;;;;;;;;;;;;;;;;;EAmBpB;;;;;;;UAQe;EACf;EACA,UAAU,OAAO,cAAc,eAAe,MAAM;EACpD,eAAe;EACf,eAAe;EACf,WAAW;EACX;;iBAwMsB,kBACtB,MAAM,0BACN,gBACA,WAAW"}
1
+ {"version":3,"file":"turbopack.d.ts","names":[],"sources":["../src/turbopack.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;KAoEY,8BAA8B,KACxC;EAGA;IAAoB;;;;;;;;;;;;;;;;;;;;EAmBpB;;;;;;;UAQe;EACf;EACA,UAAU,OAAO,cAAc,eAAe,MAAM;EACpD,eAAe;EACf,eAAe;EACf,WAAW;EACX;;iBAwMsB,kBACtB,MAAM,0BACN,gBACA,WAAW"}
@@ -1 +1 @@
1
- {"version":3,"file":"turbopack.js","names":[],"sources":["../src/turbopack.ts"],"sourcesContent":["/**\n * Webpack-loader entry point, for Turbopack (Next.js) and any other host that\n * runs webpack loaders but not webpack plugins.\n *\n * Turbopack deliberately supports no webpack plugins, so the unplugin build\n * plugins cannot reach it. Loaders it does run, through the real `loader-runner`\n * library, and that is enough: this module is the whole zod-compiler transform\n * behind `this.async()`.\n *\n * It emits TypeScript, not JavaScript. A `turbopack.rules` entry that sets no\n * `as`/`type` leaves the loader's output to be parsed as whatever the file\n * already was, so Turbopack's own SWC pass handles the syntax — there is no\n * second transpile here and no @swc/core dependency.\n *\n * `codegenMode: \"lean\"` is available here — it imports the shared helpers from\n * `zod-compiler/runtime`, a real package subpath, rather than the `virtual:` id\n * the build plugins answer from a resolve hook that a loader does not have. It\n * is opt-in; see the option's doc for why.\n *\n * // next.config.ts\n * export default {\n * turbopack: {\n * rules: {\n * \"*.{ts,tsx}\": {\n * condition: { all: [{ not: \"foreign\" }, { content: /[Zz]od/ }] },\n * loaders: [\"zod-compiler/turbopack\"],\n * },\n * },\n * },\n * };\n *\n * The `content` condition is the Turbopack equivalent of the plugins' own code\n * filter and must stay as loose as ZOD_MENTION: narrowing it to the literal\n * specifier `\"zod\"` would silently skip `zod/v4`, `zod/mini` and the\n * `zod-compiler` import that drives `schemas: \"explicit\"` — no error, those\n * schemas just quietly stay uncompiled.\n */\n\nimport fs from \"node:fs\";\nimport remapping from \"@jridgewell/remapping\";\nimport { getFirstPartyModulePaths, invalidateModuleCache } from \"./loader.js\";\nimport { resetDepGraphMemo, transformDependencies } from \"./unplugin/dep-graph.js\";\nimport {\n log,\n shouldTransform,\n type TransformSourceMap,\n transformCodeWithMap,\n} from \"./unplugin/transform.js\";\nimport type { ZodCompilerPluginOptions } from \"./unplugin/types.js\";\nimport { RUNTIME_PACKAGE_ID } from \"./unplugin/virtual.js\";\n\n/**\n * Plugin options minus the ones a loader host cannot express.\n *\n * Turbopack serializes loader options through `next.config`, so they have to be\n * plain JSON — hence no `apply` (a Vite lifecycle function) and a string-only\n * `schemaNamePattern` where the plugin also accepts a RegExp.\n *\n * No `cache` either: the disk cache's dependency bookkeeping leans on a\n * `buildEnd` flush that a loader has no equivalent for, and loader hosts keep\n * their own persistent result cache — Turbopack's is keyed on content plus the\n * dependencies declared below, which is what this would have re-implemented.\n */\nexport type ZodCompilerTurbopackOptions = Omit<\n ZodCompilerPluginOptions,\n \"apply\" | \"cache\" | \"codegenMode\" | \"hoist\"\n> & {\n hoist?: boolean | { schemaNamePattern?: string | null | undefined } | undefined;\n /**\n * `\"inline\"` (default) emits the shared helpers into every transformed file.\n *\n * `\"lean\"` imports them from `zod-compiler/runtime` instead, so a bundle\n * carries one copy however many files were transformed. That specifier is a\n * real package subpath, which is what makes it usable from a loader at all —\n * the `virtual:` id the build plugins emit needs a `resolveId` hook, and a\n * loader has none.\n *\n * Opt-in rather than the default because it only holds when the host BUNDLES\n * the import. Next.js does for client and App Router server code, but Pages\n * Router server code externalizes node_modules imports unless\n * `bundlePagesRouterDependencies` is set — and `zod-compiler` is normally a\n * devDependency, so a production install prunes it and the route throws\n * ERR_MODULE_NOT_FOUND on the first request. A bigger bundle is the better\n * default than a runtime failure that no build step reports.\n * @default \"inline\"\n */\n codegenMode?: \"lean\" | \"inline\" | undefined;\n};\n\n/**\n * The slice of webpack's loader context this uses. Structural rather than\n * imported from webpack: the package must not take a webpack dependency to\n * serve a host that is not webpack.\n */\nexport interface ZodCompilerLoaderContext {\n resourcePath: string;\n async(): (error: Error | null, code?: string, map?: TransformSourceMap) => void;\n getOptions?(): ZodCompilerTurbopackOptions | undefined;\n addDependency?(file: string): void;\n cacheable?(flag: boolean): void;\n query?: unknown;\n}\n\n/**\n * Disk stamps of every module the shared execution cache is currently holding.\n *\n * Discovery executes schema files from DISK through a module cache that outlives\n * loader calls. The bundler plugins evict it from `watchChange`; a loader has no\n * such hook, so staleness has to be detected here or compiled validators keep\n * reflecting whatever the files said when they were first executed.\n *\n * Diffing the entry's own content is NOT enough, and the gap is exactly the case\n * `addDependency` exists to handle: when an imported constant changes, the host\n * re-runs the loader for a schema file whose content is UNCHANGED.\n *\n * Tracked GLOBALLY rather than per file, because that is what it describes: one\n * process-wide module cache, stale the moment any file behind it changes. A\n * per-file dependency list cannot express that — a file being transformed for\n * the FIRST time has no list yet, but the cache it is about to read from is\n * already warm and may already be stale. Keying on the cache's own inventory\n * also means one edit costs one eviction rather than one per dependent file.\n *\n * `getFirstPartyModulePaths()` is that inventory, which is why it is sound here\n * while being unsound as a per-file dependency list (see transformDependencies):\n * the question is \"what is cached\", not \"what does this file need\". It covers\n * dependencies the host never feeds through this loader at all — a constants\n * file that never mentions zod, or one a rule's `exclude` skips — but only the\n * ones jiti holds, so the two known gaps are its gaps:\n *\n * - `.js`/`.mjs` deps go through native `import()` (see loader.ts), which has no\n * evictable cache. The build plugins are equally stale there; on Bun and Deno\n * that is every module, and this whole mechanism is inert.\n * - A file being CREATED changes nothing's stamp, so a new module that shadows\n * an existing resolution (`limits.ts` beside `limits/index.ts`) keeps serving\n * the old one until some stamped file also changes. `watchChange` gives the\n * plugins a signal for this that a loader host has no equivalent of.\n *\n * mtime+size like the disk cache's fast path, minus its content hashes: those\n * exist to survive checkouts across processes, and this map dies with the\n * process.\n */\nconst executedModuleStamps = new Map<string, string>();\n\nfunction stampOf(file: string): string {\n try {\n const stat = fs.statSync(file, { throwIfNoEntry: false });\n return stat === undefined ? \"\" : `${stat.mtimeMs}:${stat.size}`;\n } catch {\n // Non-ENOENT (EACCES, ELOOP, ENOTDIR): unreadable is indistinguishable from\n // changed, and must never fail the build.\n return \"\";\n }\n}\n\n/**\n * Drop cached module executions when any file behind them changed on disk.\n *\n * Eviction is global (see invalidateModuleCache), so it is deliberately paired\n * with the dep-graph memos the plugin also resets in `watchChange`: a resolution\n * memoized before a file existed would otherwise pin that importer to an\n * unanalyzable graph for the life of the process.\n *\n * Costs one stat per executed module, paid by every file that reaches the\n * transform — ~3 ms per file against the 1,900-module project dep-graph.ts\n * cites, the same order as the closure walk `transformDependencies` does a few\n * lines later, and far below the discovery it is protecting.\n */\nfunction invalidateStaleExecutions(): void {\n let stale = false;\n for (const [file, stamp] of executedModuleStamps) {\n if (stampOf(file) !== stamp) {\n stale = true;\n break;\n }\n }\n if (!stale) return;\n invalidateModuleCache();\n resetDepGraphMemo();\n executedModuleStamps.clear();\n}\n\n/**\n * Stamp whatever discovery just executed.\n *\n * Keeps the FIRST stamp for a file rather than refreshing: a file edited between\n * its execution and this call must keep its pre-edit stamp, or the next run\n * would see it as fresh and pin the stale execution permanently.\n *\n * `null` means no jiti instance exists — a runtime whose module cache cannot be\n * evicted at all (loader.ts), so there is nothing to track and nothing to fix.\n */\nfunction recordExecutedModules(): void {\n for (const file of getFirstPartyModulePaths() ?? []) {\n if (!executedModuleStamps.has(file)) executedModuleStamps.set(file, stampOf(file));\n }\n}\n\nfunction readOptions(context: ZodCompilerLoaderContext): ZodCompilerTurbopackOptions {\n const options = context.getOptions?.();\n if (options !== undefined) return options;\n // `getOptions` is standard but not universal; `query` is the older shape.\n return typeof context.query === \"object\" && context.query !== null\n ? (context.query as ZodCompilerTurbopackOptions)\n : {};\n}\n\n/**\n * Declare what a rebuild must watch. The host caches this file's output keyed on\n * its own content, but discovery executed the whole import graph — without these\n * an edit to an imported constant leaves a stale validator in the bundle.\n */\nfunction declareDependencies(\n context: ZodCompilerLoaderContext,\n id: string,\n verbose: boolean,\n): void {\n const { files, complete } = transformDependencies(id);\n for (const file of files) context.addDependency?.(file);\n if (complete) return;\n // The graph could not be analyzed (a non-literal dynamic import anywhere in\n // the closure), so the list above is just this file — not enough for the host\n // to know when to re-run us. Asking to be re-run every build is the only\n // answer left. Freshness itself does not depend on this: whether the loader\n // is re-invoked once or every time, invalidateStaleExecutions decides what\n // discovery may reuse.\n context.cacheable?.(false);\n if (verbose) {\n log(`Cannot analyze the import graph of ${id} — re-running its transform every build`);\n }\n}\n\ninterface LoaderResult {\n code: string;\n map?: TransformSourceMap | undefined;\n}\n\n/** The transform itself, split out so the loader shell stays callback-only. */\nasync function run(\n context: ZodCompilerLoaderContext,\n source: string,\n inputMap: TransformSourceMap | undefined,\n): Promise<LoaderResult> {\n const id = context.resourcePath;\n const options = readOptions(context);\n\n if (!shouldTransform(id, options)) return { code: source, map: inputMap };\n\n // Before discovery can read a stale execution. Note this covers an edit to\n // THIS file too: discovery executed it, so it carries a stamp of its own —\n // there is no separate host-content diff, which would only ever fire for\n // content that differs from the disk discovery actually reads.\n invalidateStaleExecutions();\n\n const output = options.output ?? \"schema\";\n let discoveryRan = false;\n const result = await transformCodeWithMap(source, id, {\n mode: options.codegenMode ?? \"inline\",\n runtimeId: RUNTIME_PACKAGE_ID,\n verbose: options.verbose,\n // \"compact\" keeps the Zod schema (its safeParse IS the cold error path), so\n // only \"bag\" drops Zod compatibility.\n zodCompat: output === \"schema\" || output === \"compact\",\n compact: output === \"compact\",\n autoDiscover: (options.schemas ?? \"auto\") === \"auto\",\n hoist: options.hoist,\n onDiscovery: () => {\n discoveryRan = true;\n },\n onUncacheableResult: () => {\n // Discovery recovered from a process.exit (an env guard in a secret-less\n // build): the result is a function of the ENVIRONMENT, not the file, so it\n // must not be cached against this content.\n context.cacheable?.(false);\n },\n });\n\n // Only discovery reads other files. A hoist-only or bailed-out transform is a\n // pure function of this file's content, which the host already keys on.\n if (discoveryRan) {\n recordExecutedModules();\n declareDependencies(context, id, options.verbose === true);\n }\n\n if (result === null) return { code: source, map: inputMap };\n return { code: result.code, map: composeMaps(result.map, inputMap) };\n}\n\n/** Chain this transform's map onto an earlier loader's, newest first. */\nfunction composeMaps(\n map: TransformSourceMap | null,\n inputMap: TransformSourceMap | undefined,\n): TransformSourceMap | undefined {\n if (map === null) return inputMap;\n if (inputMap === undefined) return map;\n return remapping(\n [map, inputMap] as Parameters<typeof remapping>[0],\n () => null,\n ) as unknown as TransformSourceMap;\n}\n\nexport default function zodCompilerLoader(\n this: ZodCompilerLoaderContext,\n source: string,\n inputMap?: TransformSourceMap,\n): void {\n const callback = this.async();\n const succeed = (result: LoaderResult): void => callback(null, result.code, result.map);\n const fail = (error: unknown): void =>\n callback(error instanceof Error ? error : new Error(String(error)));\n\n // Two `then` handlers rather than one plus a try/catch: loader-runner invokes\n // the host's continuation synchronously from `callback`, so a throw inside it\n // would re-enter a catch and call back a second time. As rejection handlers of\n // the SAME promise, exactly one of these can ever run.\n run(this, source, inputMap).then(succeed, fail);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6IA,MAAM,uCAAuB,IAAI,IAAoB;AAErD,SAAS,QAAQ,MAAsB;CACrC,IAAI;EACF,MAAM,OAAO,GAAG,SAAS,MAAM,EAAE,gBAAgB,MAAM,CAAC;EACxD,OAAO,SAAS,KAAA,IAAY,KAAK,GAAG,KAAK,QAAQ,GAAG,KAAK;CAC3D,QAAQ;EAGN,OAAO;CACT;AACF;;;;;;;;;;;;;;AAeA,SAAS,4BAAkC;CACzC,IAAI,QAAQ;CACZ,KAAK,MAAM,CAAC,MAAM,UAAU,sBAC1B,IAAI,QAAQ,IAAI,MAAM,OAAO;EAC3B,QAAQ;EACR;CACF;CAEF,IAAI,CAAC,OAAO;CACZ,sBAAsB;CACtB,kBAAkB;CAClB,qBAAqB,MAAM;AAC7B;;;;;;;;;;;AAYA,SAAS,wBAA8B;CACrC,KAAK,MAAM,QAAQ,yBAAyB,KAAK,CAAC,GAChD,IAAI,CAAC,qBAAqB,IAAI,IAAI,GAAG,qBAAqB,IAAI,MAAM,QAAQ,IAAI,CAAC;AAErF;AAEA,SAAS,YAAY,SAAgE;CACnF,MAAM,UAAU,QAAQ,aAAa;CACrC,IAAI,YAAY,KAAA,GAAW,OAAO;CAElC,OAAO,OAAO,QAAQ,UAAU,YAAY,QAAQ,UAAU,OACzD,QAAQ,QACT,CAAC;AACP;;;;;;AAOA,SAAS,oBACP,SACA,IACA,SACM;CACN,MAAM,EAAE,OAAO,aAAa,sBAAsB,EAAE;CACpD,KAAK,MAAM,QAAQ,OAAO,QAAQ,gBAAgB,IAAI;CACtD,IAAI,UAAU;CAOd,QAAQ,YAAY,KAAK;CACzB,IAAI,SACF,IAAI,sCAAsC,GAAG,wCAAwC;AAEzF;;AAQA,eAAe,IACb,SACA,QACA,UACuB;CACvB,MAAM,KAAK,QAAQ;CACnB,MAAM,UAAU,YAAY,OAAO;CAEnC,IAAI,CAAC,gBAAgB,IAAI,OAAO,GAAG,OAAO;EAAE,MAAM;EAAQ,KAAK;CAAS;CAMxE,0BAA0B;CAE1B,MAAM,SAAS,QAAQ,UAAU;CACjC,IAAI,eAAe;CACnB,MAAM,SAAS,MAAM,qBAAqB,QAAQ,IAAI;EACpD,MAAM,QAAQ,eAAe;EAC7B,WAAW;EACX,SAAS,QAAQ;EAGjB,WAAW,WAAW,YAAY,WAAW;EAC7C,SAAS,WAAW;EACpB,eAAe,QAAQ,WAAW,YAAY;EAC9C,OAAO,QAAQ;EACf,mBAAmB;GACjB,eAAe;EACjB;EACA,2BAA2B;GAIzB,QAAQ,YAAY,KAAK;EAC3B;CACF,CAAC;CAID,IAAI,cAAc;EAChB,sBAAsB;EACtB,oBAAoB,SAAS,IAAI,QAAQ,YAAY,IAAI;CAC3D;CAEA,IAAI,WAAW,MAAM,OAAO;EAAE,MAAM;EAAQ,KAAK;CAAS;CAC1D,OAAO;EAAE,MAAM,OAAO;EAAM,KAAK,YAAY,OAAO,KAAK,QAAQ;CAAE;AACrE;;AAGA,SAAS,YACP,KACA,UACgC;CAChC,IAAI,QAAQ,MAAM,OAAO;CACzB,IAAI,aAAa,KAAA,GAAW,OAAO;CACnC,OAAO,UACL,CAAC,KAAK,QAAQ,SACR,IACR;AACF;AAEA,SAAwB,kBAEtB,QACA,UACM;CACN,MAAM,WAAW,KAAK,MAAM;CAC5B,MAAM,WAAW,WAA+B,SAAS,MAAM,OAAO,MAAM,OAAO,GAAG;CACtF,MAAM,QAAQ,UACZ,SAAS,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,CAAC;CAMpE,IAAI,MAAM,QAAQ,QAAQ,CAAC,CAAC,KAAK,SAAS,IAAI;AAChD"}
1
+ {"version":3,"file":"turbopack.js","names":[],"sources":["../src/turbopack.ts"],"sourcesContent":["/**\n * Webpack-loader entry point, for Turbopack (Next.js) and any other host that\n * runs webpack loaders but not webpack plugins.\n *\n * Turbopack deliberately supports no webpack plugins, so the unplugin build\n * plugins cannot reach it. Loaders it does run, through the real `loader-runner`\n * library, and that is enough: this module is the whole zod-compiler transform\n * behind `this.async()`.\n *\n * It emits TypeScript, not JavaScript. A `turbopack.rules` entry that sets no\n * `as`/`type` leaves the loader's output to be parsed as whatever the file\n * already was, so Turbopack's own SWC pass handles the syntax — there is no\n * second transpile here and no @swc/core dependency.\n *\n * `codegenMode: \"lean\"` is available here — it imports the shared helpers from\n * `zod-compiler/runtime`, a real package subpath, rather than the `virtual:` id\n * the build plugins answer from a resolve hook that a loader does not have. It\n * is opt-in; see the option's doc for why.\n *\n * // next.config.ts\n * export default {\n * turbopack: {\n * rules: {\n * \"*.{ts,tsx}\": {\n * condition: { all: [{ not: \"foreign\" }, { content: /[Zz]od/ }] },\n * loaders: [\"zod-compiler/turbopack\"],\n * },\n * },\n * },\n * };\n *\n * The `content` condition is the Turbopack equivalent of the plugins' own code\n * filter and must stay as loose as ZOD_MENTION: narrowing it to the literal\n * specifier `\"zod\"` would silently skip `zod/v4`, `zod/mini` and the\n * `zod-compiler` import that drives `schemas: \"explicit\"` — no error, those\n * schemas just quietly stay uncompiled.\n */\n\nimport fs from \"node:fs\";\nimport remapping from \"@jridgewell/remapping\";\nimport { getFirstPartyModulePaths, invalidateModuleCache } from \"./loader.js\";\nimport { resetDepGraphMemo, transformDependencies } from \"./unplugin/dep-graph.js\";\nimport {\n log,\n shouldTransform,\n type TransformSourceMap,\n transformCodeWithMap,\n} from \"./unplugin/transform.js\";\nimport type { ZodCompilerPluginOptions } from \"./unplugin/types.js\";\nimport { RUNTIME_PACKAGE_ID } from \"./unplugin/virtual.js\";\n\n/**\n * Plugin options minus the ones a loader host cannot express.\n *\n * Turbopack serializes loader options through `next.config`, so they have to be\n * plain JSON — hence no `apply` (a Vite lifecycle function) and a string-only\n * `schemaNamePattern` where the plugin also accepts a RegExp.\n *\n * No `cache` either: the disk cache's dependency bookkeeping leans on a\n * `buildEnd` flush that a loader has no equivalent for, and loader hosts keep\n * their own persistent result cache — Turbopack's is keyed on content plus the\n * dependencies declared below, which is what this would have re-implemented.\n *\n * No `parallel`: the concurrency is the host's to own. Turbopack already runs\n * loaders across its own worker pool, so a pool per loader invocation would\n * multiply threads against a machine that is already saturated — and the\n * module-cache staleness stamps below assume one shared execution cache.\n */\nexport type ZodCompilerTurbopackOptions = Omit<\n ZodCompilerPluginOptions,\n \"apply\" | \"cache\" | \"codegenMode\" | \"hoist\" | \"parallel\"\n> & {\n hoist?: boolean | { schemaNamePattern?: string | null | undefined } | undefined;\n /**\n * `\"inline\"` (default) emits the shared helpers into every transformed file.\n *\n * `\"lean\"` imports them from `zod-compiler/runtime` instead, so a bundle\n * carries one copy however many files were transformed. That specifier is a\n * real package subpath, which is what makes it usable from a loader at all —\n * the `virtual:` id the build plugins emit needs a `resolveId` hook, and a\n * loader has none.\n *\n * Opt-in rather than the default because it only holds when the host BUNDLES\n * the import. Next.js does for client and App Router server code, but Pages\n * Router server code externalizes node_modules imports unless\n * `bundlePagesRouterDependencies` is set — and `zod-compiler` is normally a\n * devDependency, so a production install prunes it and the route throws\n * ERR_MODULE_NOT_FOUND on the first request. A bigger bundle is the better\n * default than a runtime failure that no build step reports.\n * @default \"inline\"\n */\n codegenMode?: \"lean\" | \"inline\" | undefined;\n};\n\n/**\n * The slice of webpack's loader context this uses. Structural rather than\n * imported from webpack: the package must not take a webpack dependency to\n * serve a host that is not webpack.\n */\nexport interface ZodCompilerLoaderContext {\n resourcePath: string;\n async(): (error: Error | null, code?: string, map?: TransformSourceMap) => void;\n getOptions?(): ZodCompilerTurbopackOptions | undefined;\n addDependency?(file: string): void;\n cacheable?(flag: boolean): void;\n query?: unknown;\n}\n\n/**\n * Disk stamps of every module the shared execution cache is currently holding.\n *\n * Discovery executes schema files from DISK through a module cache that outlives\n * loader calls. The bundler plugins evict it from `watchChange`; a loader has no\n * such hook, so staleness has to be detected here or compiled validators keep\n * reflecting whatever the files said when they were first executed.\n *\n * Diffing the entry's own content is NOT enough, and the gap is exactly the case\n * `addDependency` exists to handle: when an imported constant changes, the host\n * re-runs the loader for a schema file whose content is UNCHANGED.\n *\n * Tracked GLOBALLY rather than per file, because that is what it describes: one\n * process-wide module cache, stale the moment any file behind it changes. A\n * per-file dependency list cannot express that — a file being transformed for\n * the FIRST time has no list yet, but the cache it is about to read from is\n * already warm and may already be stale. Keying on the cache's own inventory\n * also means one edit costs one eviction rather than one per dependent file.\n *\n * `getFirstPartyModulePaths()` is that inventory, which is why it is sound here\n * while being unsound as a per-file dependency list (see transformDependencies):\n * the question is \"what is cached\", not \"what does this file need\". It covers\n * dependencies the host never feeds through this loader at all — a constants\n * file that never mentions zod, or one a rule's `exclude` skips — but only the\n * ones jiti holds, so the two known gaps are its gaps:\n *\n * - `.js`/`.mjs` deps go through native `import()` (see loader.ts), which has no\n * evictable cache. The build plugins are equally stale there; on Bun and Deno\n * that is every module, and this whole mechanism is inert.\n * - A file being CREATED changes nothing's stamp, so a new module that shadows\n * an existing resolution (`limits.ts` beside `limits/index.ts`) keeps serving\n * the old one until some stamped file also changes. `watchChange` gives the\n * plugins a signal for this that a loader host has no equivalent of.\n *\n * mtime+size like the disk cache's fast path, minus its content hashes: those\n * exist to survive checkouts across processes, and this map dies with the\n * process.\n */\nconst executedModuleStamps = new Map<string, string>();\n\nfunction stampOf(file: string): string {\n try {\n const stat = fs.statSync(file, { throwIfNoEntry: false });\n return stat === undefined ? \"\" : `${stat.mtimeMs}:${stat.size}`;\n } catch {\n // Non-ENOENT (EACCES, ELOOP, ENOTDIR): unreadable is indistinguishable from\n // changed, and must never fail the build.\n return \"\";\n }\n}\n\n/**\n * Drop cached module executions when any file behind them changed on disk.\n *\n * Eviction is global (see invalidateModuleCache), so it is deliberately paired\n * with the dep-graph memos the plugin also resets in `watchChange`: a resolution\n * memoized before a file existed would otherwise pin that importer to an\n * unanalyzable graph for the life of the process.\n *\n * Costs one stat per executed module, paid by every file that reaches the\n * transform — ~3 ms per file against the 1,900-module project dep-graph.ts\n * cites, the same order as the closure walk `transformDependencies` does a few\n * lines later, and far below the discovery it is protecting.\n */\nfunction invalidateStaleExecutions(): void {\n let stale = false;\n for (const [file, stamp] of executedModuleStamps) {\n if (stampOf(file) !== stamp) {\n stale = true;\n break;\n }\n }\n if (!stale) return;\n invalidateModuleCache();\n resetDepGraphMemo();\n executedModuleStamps.clear();\n}\n\n/**\n * Stamp whatever discovery just executed.\n *\n * Keeps the FIRST stamp for a file rather than refreshing: a file edited between\n * its execution and this call must keep its pre-edit stamp, or the next run\n * would see it as fresh and pin the stale execution permanently.\n *\n * `null` means no jiti instance exists — a runtime whose module cache cannot be\n * evicted at all (loader.ts), so there is nothing to track and nothing to fix.\n */\nfunction recordExecutedModules(): void {\n for (const file of getFirstPartyModulePaths() ?? []) {\n if (!executedModuleStamps.has(file)) executedModuleStamps.set(file, stampOf(file));\n }\n}\n\nfunction readOptions(context: ZodCompilerLoaderContext): ZodCompilerTurbopackOptions {\n const options = context.getOptions?.();\n if (options !== undefined) return options;\n // `getOptions` is standard but not universal; `query` is the older shape.\n return typeof context.query === \"object\" && context.query !== null\n ? (context.query as ZodCompilerTurbopackOptions)\n : {};\n}\n\n/**\n * Declare what a rebuild must watch. The host caches this file's output keyed on\n * its own content, but discovery executed the whole import graph — without these\n * an edit to an imported constant leaves a stale validator in the bundle.\n */\nfunction declareDependencies(\n context: ZodCompilerLoaderContext,\n id: string,\n verbose: boolean,\n): void {\n const { files, complete } = transformDependencies(id);\n for (const file of files) context.addDependency?.(file);\n if (complete) return;\n // The graph could not be analyzed (a non-literal dynamic import anywhere in\n // the closure), so the list above is just this file — not enough for the host\n // to know when to re-run us. Asking to be re-run every build is the only\n // answer left. Freshness itself does not depend on this: whether the loader\n // is re-invoked once or every time, invalidateStaleExecutions decides what\n // discovery may reuse.\n context.cacheable?.(false);\n if (verbose) {\n log(`Cannot analyze the import graph of ${id} — re-running its transform every build`);\n }\n}\n\ninterface LoaderResult {\n code: string;\n map?: TransformSourceMap | undefined;\n}\n\n/** The transform itself, split out so the loader shell stays callback-only. */\nasync function run(\n context: ZodCompilerLoaderContext,\n source: string,\n inputMap: TransformSourceMap | undefined,\n): Promise<LoaderResult> {\n const id = context.resourcePath;\n const options = readOptions(context);\n\n if (!shouldTransform(id, options)) return { code: source, map: inputMap };\n\n // Before discovery can read a stale execution. Note this covers an edit to\n // THIS file too: discovery executed it, so it carries a stamp of its own —\n // there is no separate host-content diff, which would only ever fire for\n // content that differs from the disk discovery actually reads.\n invalidateStaleExecutions();\n\n const output = options.output ?? \"schema\";\n let discoveryRan = false;\n const result = await transformCodeWithMap(source, id, {\n mode: options.codegenMode ?? \"inline\",\n runtimeId: RUNTIME_PACKAGE_ID,\n verbose: options.verbose,\n // \"compact\" keeps the Zod schema (its safeParse IS the cold error path), so\n // only \"bag\" drops Zod compatibility.\n zodCompat: output === \"schema\" || output === \"compact\",\n compact: output === \"compact\",\n autoDiscover: (options.schemas ?? \"auto\") === \"auto\",\n hoist: options.hoist,\n onDiscovery: () => {\n discoveryRan = true;\n },\n onUncacheableResult: () => {\n // Discovery recovered from a process.exit (an env guard in a secret-less\n // build): the result is a function of the ENVIRONMENT, not the file, so it\n // must not be cached against this content.\n context.cacheable?.(false);\n },\n });\n\n // Only discovery reads other files. A hoist-only or bailed-out transform is a\n // pure function of this file's content, which the host already keys on.\n if (discoveryRan) {\n recordExecutedModules();\n declareDependencies(context, id, options.verbose === true);\n }\n\n if (result === null) return { code: source, map: inputMap };\n return { code: result.code, map: composeMaps(result.map, inputMap) };\n}\n\n/** Chain this transform's map onto an earlier loader's, newest first. */\nfunction composeMaps(\n map: TransformSourceMap | null,\n inputMap: TransformSourceMap | undefined,\n): TransformSourceMap | undefined {\n if (map === null) return inputMap;\n if (inputMap === undefined) return map;\n return remapping(\n [map, inputMap] as Parameters<typeof remapping>[0],\n () => null,\n ) as unknown as TransformSourceMap;\n}\n\nexport default function zodCompilerLoader(\n this: ZodCompilerLoaderContext,\n source: string,\n inputMap?: TransformSourceMap,\n): void {\n const callback = this.async();\n const succeed = (result: LoaderResult): void => callback(null, result.code, result.map);\n const fail = (error: unknown): void =>\n callback(error instanceof Error ? error : new Error(String(error)));\n\n // Two `then` handlers rather than one plus a try/catch: loader-runner invokes\n // the host's continuation synchronously from `callback`, so a throw inside it\n // would re-enter a catch and call back a second time. As rejection handlers of\n // the SAME promise, exactly one of these can ever run.\n run(this, source, inputMap).then(succeed, fail);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkJA,MAAM,uCAAuB,IAAI,IAAoB;AAErD,SAAS,QAAQ,MAAsB;CACrC,IAAI;EACF,MAAM,OAAO,GAAG,SAAS,MAAM,EAAE,gBAAgB,MAAM,CAAC;EACxD,OAAO,SAAS,KAAA,IAAY,KAAK,GAAG,KAAK,QAAQ,GAAG,KAAK;CAC3D,QAAQ;EAGN,OAAO;CACT;AACF;;;;;;;;;;;;;;AAeA,SAAS,4BAAkC;CACzC,IAAI,QAAQ;CACZ,KAAK,MAAM,CAAC,MAAM,UAAU,sBAC1B,IAAI,QAAQ,IAAI,MAAM,OAAO;EAC3B,QAAQ;EACR;CACF;CAEF,IAAI,CAAC,OAAO;CACZ,sBAAsB;CACtB,kBAAkB;CAClB,qBAAqB,MAAM;AAC7B;;;;;;;;;;;AAYA,SAAS,wBAA8B;CACrC,KAAK,MAAM,QAAQ,yBAAyB,KAAK,CAAC,GAChD,IAAI,CAAC,qBAAqB,IAAI,IAAI,GAAG,qBAAqB,IAAI,MAAM,QAAQ,IAAI,CAAC;AAErF;AAEA,SAAS,YAAY,SAAgE;CACnF,MAAM,UAAU,QAAQ,aAAa;CACrC,IAAI,YAAY,KAAA,GAAW,OAAO;CAElC,OAAO,OAAO,QAAQ,UAAU,YAAY,QAAQ,UAAU,OACzD,QAAQ,QACT,CAAC;AACP;;;;;;AAOA,SAAS,oBACP,SACA,IACA,SACM;CACN,MAAM,EAAE,OAAO,aAAa,sBAAsB,EAAE;CACpD,KAAK,MAAM,QAAQ,OAAO,QAAQ,gBAAgB,IAAI;CACtD,IAAI,UAAU;CAOd,QAAQ,YAAY,KAAK;CACzB,IAAI,SACF,IAAI,sCAAsC,GAAG,wCAAwC;AAEzF;;AAQA,eAAe,IACb,SACA,QACA,UACuB;CACvB,MAAM,KAAK,QAAQ;CACnB,MAAM,UAAU,YAAY,OAAO;CAEnC,IAAI,CAAC,gBAAgB,IAAI,OAAO,GAAG,OAAO;EAAE,MAAM;EAAQ,KAAK;CAAS;CAMxE,0BAA0B;CAE1B,MAAM,SAAS,QAAQ,UAAU;CACjC,IAAI,eAAe;CACnB,MAAM,SAAS,MAAM,qBAAqB,QAAQ,IAAI;EACpD,MAAM,QAAQ,eAAe;EAC7B,WAAW;EACX,SAAS,QAAQ;EAGjB,WAAW,WAAW,YAAY,WAAW;EAC7C,SAAS,WAAW;EACpB,eAAe,QAAQ,WAAW,YAAY;EAC9C,OAAO,QAAQ;EACf,mBAAmB;GACjB,eAAe;EACjB;EACA,2BAA2B;GAIzB,QAAQ,YAAY,KAAK;EAC3B;CACF,CAAC;CAID,IAAI,cAAc;EAChB,sBAAsB;EACtB,oBAAoB,SAAS,IAAI,QAAQ,YAAY,IAAI;CAC3D;CAEA,IAAI,WAAW,MAAM,OAAO;EAAE,MAAM;EAAQ,KAAK;CAAS;CAC1D,OAAO;EAAE,MAAM,OAAO;EAAM,KAAK,YAAY,OAAO,KAAK,QAAQ;CAAE;AACrE;;AAGA,SAAS,YACP,KACA,UACgC;CAChC,IAAI,QAAQ,MAAM,OAAO;CACzB,IAAI,aAAa,KAAA,GAAW,OAAO;CACnC,OAAO,UACL,CAAC,KAAK,QAAQ,SACR,IACR;AACF;AAEA,SAAwB,kBAEtB,QACA,UACM;CACN,MAAM,WAAW,KAAK,MAAM;CAC5B,MAAM,WAAW,WAA+B,SAAS,MAAM,OAAO,MAAM,OAAO,GAAG;CACtF,MAAM,QAAQ,UACZ,SAAS,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,CAAC;CAMpE,IAAI,MAAM,QAAQ,QAAQ,CAAC,CAAC,KAAK,SAAS,IAAI;AAChD"}
@@ -1 +1 @@
1
- {"version":3,"file":"disk-cache.d.ts","names":[],"sources":["../../src/unplugin/disk-cache.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA4DiB;EACf;EACA;;UASe;;EAEf;;EAEA;;EAEA,QAAQ;;EAER,MAAM;;;UAIS;EACf;EACA;EACA;EACA;EACA;EACA;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiEc,wBAAwB;;iBAiGxB;cAoBH;mBACM;mBACA;;mBAEA;UACT;UACA;EAEI,YAAA,aAAa,oBAAoB;;;;;;SAWtC,WAAW;EAOlB,IAAI,YAAY;UAIR;UAIA;;;;;;;UAUA;;;;;;;;;;UAiCA;;EA0CR,KAAK,cAAc;;;;;;UAoBX;;;;;;;UAoCA;;UAeA;UAaA;;EA+BR,KACE,aACA,uBACA,6BACA,QAAQ,iBACR,MAAM;;;;;;;EAeR,aACE,aACA,uBACA,QAAQ,iBACR,MAAM;;;;;EAWR;;;;;;EAoBA"}
1
+ {"version":3,"file":"disk-cache.d.ts","names":[],"sources":["../../src/unplugin/disk-cache.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA4DiB;EACf;EACA;;UASe;;EAEf;;EAEA;;EAEA,QAAQ;;EAER,MAAM;;;UAIS;EACf;EACA;EACA;EACA;EACA;EACA;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiEc,wBAAwB;;iBAiGxB;cAoBH;mBACM;mBACA;;mBAEA;UACT;UACA;EAEI,YAAA,aAAa,oBAAoB;;;;;;SAWtC,WAAW;EAOlB,IAAI,YAAY;UAIR;UAIA;;;;;;;UAUA;;;;;;;;;;UAiCA;;EA0CR,KAAK,cAAc;;;;;;UAoBX;;;;;;;UAoCA;;UAeA;UAaA;;EA+BR,KACE,aACA,uBACA,6BACA,QAAQ,iBACR,MAAM;;;;;;;EAeR,aACE,aACA,uBACA,QAAQ,iBACR,MAAM;;;;;EAWR;;;;;;EA2BA"}
@@ -437,6 +437,7 @@ var DiskCache = class {
437
437
  this.pending = [];
438
438
  const snapshot = this.superset === null ? null : this.superset();
439
439
  if (snapshot === null) return;
440
+ if (snapshot.length === 0) return;
440
441
  this.ensureDir();
441
442
  const built = this.buildDepset(snapshot);
442
443
  if (built === null) return;
@@ -1 +1 @@
1
- {"version":3,"file":"disk-cache.js","names":["fs","path"],"sources":["../../src/unplugin/disk-cache.ts"],"sourcesContent":["/**\n * Persistent transform-result cache.\n *\n * The expensive part of the unplugin transform is discovery: executing the\n * schema file (and transitively its whole first-party import graph) through\n * jiti inside the bundler's single-threaded server process. The in-memory\n * caches die with the process, so every `vitest run` / build re-pays that\n * cost even when nothing changed — which is exactly the loop integration\n * tests live in.\n *\n * Entries are keyed by a hash of (plugin version, zod version, transform\n * options, file id, file content) and validated against a recorded snapshot\n * of first-party files. Validation uses an mtime+size fast path and falls\n * back to content hashing, so a `touch` without changes still hits. A\n * superset of true dependencies only over-invalidates, never serves stale\n * output.\n *\n * Layout (CACHE_FORMAT 2): dependency snapshots are CONTENT-ADDRESSED and\n * shared — `deps/<sha1>.json` holds the {path → hash/mtime/size} map, and\n * each entry stores only the dep-set id. The v1 format inlined the full dep\n * map into every entry; in a large-codebase field report 835 superset-\n * fallback entries each embedded a ~1,900-file point-in-time snapshot (813\n * DISTINCT snapshots — the executed-modules superset grows as the build\n * progresses, so per-entry copies cannot even dedupe), totalling 283 MB\n * that any commit invalidated wholesale. Sharing dep-sets also means each\n * unique set is parsed and validated once per process instead of once per\n * entry.\n *\n * Superset fallbacks (entries whose static dep crawl was incomplete) are\n * DEFERRED: queued in memory and flushed in buildEnd / process exit against\n * a single end-of-build superset snapshot, so every superset entry of a\n * build shares ONE dep-set file (recording the still-growing snapshot at\n * save time is what produced 813 distinct copies). Deferred entries are\n * dropped on watchChange — a dependency edited between queueing and flush\n * would pair post-change hashes with a pre-change result, which is the one\n * combination that can serve stale output. A killed process loses only its\n * pending superset entries (the static-complete majority persists\n * immediately); the next run re-pays discovery for those files alone.\n *\n * Writes are atomic (tmp file + rename) so concurrent bundler processes\n * (vitest workspace projects) can share one cache directory safely.\n */\n\nimport { createHash } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/** Bump when the on-disk layout changes; mismatched directories are wiped. */\nconst CACHE_FORMAT = 2;\nconst META_FILE = \"_meta.json\";\nconst GC_MARKER = \"_gc\";\nconst DEPSET_DIR = \"deps\";\n/** GC at most once per day per cache directory. */\nconst GC_INTERVAL_MS = 24 * 60 * 60 * 1000;\n/** Entries unread for this long are presumed orphaned by key churn. */\nconst MAX_ENTRY_AGE_MS = 30 * 24 * 60 * 60 * 1000;\n/** Stale atomic-write temp files (crashed processes) older than this are removed. */\nconst MAX_TMP_AGE_MS = 60 * 60 * 1000;\n\nexport interface CacheEntryStats {\n schemas: number;\n optimized: number;\n}\n\ninterface DepRecord {\n hash: string;\n mtimeMs: number;\n size: number;\n}\n\nexport interface CacheEntry {\n /** Transformed code, or null when the transform produced no change. */\n result: string | null;\n /** Content-addressed id of the shared dep-set file (deps/<id>.json). */\n depset: string;\n /** Build stats to replay on cache hits (only present when schemas compiled). */\n stats?: CacheEntryStats;\n /** Composed sourcemap for `result` (original → transformed). */\n map?: CacheSourceMap | null;\n}\n\n/** JSON shape of the persisted sourcemap (mirrors TransformSourceMap). */\nexport interface CacheSourceMap {\n version: number;\n sources: (string | null)[];\n sourcesContent?: (string | null)[];\n names: string[];\n mappings: string;\n file?: string | null;\n}\n\ninterface DepsetFile {\n files: Record<string, DepRecord>;\n}\n\ninterface PendingEntry {\n key: string;\n result: string | null;\n stats?: CacheEntryStats | undefined;\n map?: CacheSourceMap | null | undefined;\n}\n\nfunction sha1(data: string | Buffer): string {\n return createHash(\"sha1\").update(data).digest(\"hex\");\n}\n\n/** Read own package version once for cache keying (src and dist both sit two levels below the package root). */\nfunction readPluginVersion(): string {\n try {\n const pkgPath = new URL(\"../../package.json\", import.meta.url);\n const pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf8\")) as { version?: string };\n return pkg.version ?? \"0\";\n } catch {\n return \"0\";\n }\n}\n\n/** Resolve the installed zod version — generated code depends on zod internals. */\nfunction readZodVersion(): string {\n try {\n const pkgPath = path.join(process.cwd(), \"node_modules\", \"zod\", \"package.json\");\n const pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf8\")) as { version?: string };\n return pkg.version ?? \"0\";\n } catch {\n return \"0\";\n }\n}\n\n/**\n * Build fingerprint: a content hash of the package's own source trees.\n *\n * The published version string alone is not enough — file: installs, linked\n * monorepo packages, and canary builds rebuild the compiler without a version\n * bump, and serving codegen from an older compiler build would be silently\n * stale.\n *\n * CONTENT-addressed, for the same reason depset ids are: mtimes do not survive\n * an install. pnpm rewrites every mtime under its `copy` import method, and\n * copy is what you get whenever the store sits on a different filesystem than\n * the workspace — the default on CI runners that mount the store as its own\n * volume (`npm_config_package_import_method=copy`). An mtime fingerprint\n * therefore rotates on every `pnpm install --frozen-lockfile`, which changes\n * every cache key and makes a restored CI cache wholly unreachable: the archive\n * unpacks, and nothing in it is ever looked up. Hardlink and APFS-clone installs\n * DO preserve mtimes, so the bug hides locally and reproduces only on the\n * runners that need the cache most.\n *\n * A content hash is also strictly tighter than mtime for the stated purpose: a\n * rebuild that reproduces identical bytes no longer discards the whole cache.\n *\n * Hashing ~0.7 MB across ~250 files costs ~5ms — once per process, and only if\n * a key is actually built (see `keyPrefix`).\n */\nexport function computeBuildFingerprint(root: string): string {\n const found: string[] = [];\n const walk = (dir: string): void => {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n const p = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n walk(p);\n } else if (/\\.(?:js|ts|json)$/.test(entry.name)) {\n // Relative + POSIX-normalised: the absolute prefix differs per checkout\n // and the separator differs per platform, neither of which is a rebuild.\n found.push(path.relative(root, p).split(path.sep).join(\"/\"));\n }\n }\n };\n for (const sub of [\"dist\", \"src\"]) {\n walk(path.join(root, sub));\n }\n found.sort();\n const hash = createHash(\"sha1\");\n for (const rel of found) {\n hash.update(rel);\n hash.update(\"\\0\");\n try {\n hash.update(fs.readFileSync(path.join(root, rel)));\n } catch {\n // Unreadable file: fold in a marker rather than abort. Collapsing to a\n // constant on error would make genuinely different builds share a key.\n hash.update(\"<unreadable>\");\n }\n }\n return `${hash.digest(\"hex\")}:${found.length}`;\n}\n\nfunction readBuildFingerprint(): string {\n try {\n return computeBuildFingerprint(fileURLToPath(new URL(\"../..\", import.meta.url)));\n } catch {\n return \"0\";\n }\n}\n\n/**\n * Version + build identity shared by every key in the process. Computed on\n * first key, not at import: reading two package.json files and hashing dist is\n * pure waste for a build that never enables the cache.\n */\nlet keyPrefixMemo: string | null = null;\n\nfunction keyPrefix(): string {\n keyPrefixMemo ??= `${readPluginVersion()}\\0${readBuildFingerprint()}\\0${readZodVersion()}`;\n return keyPrefixMemo;\n}\n\n/**\n * Per-process memo: path → current stat (+ lazily computed content hash).\n * One fs.stat per file per process regardless of how many dep-sets\n * reference it; content is read at most once.\n */\nconst depStatMemo = new Map<string, { mtimeMs: number; size: number; hash?: string } | null>();\n\n/** Per-process memo: dep-set file path → validation verdict. */\nconst depsetVerdictMemo = new Map<string, boolean>();\n\nfunction statDep(depPath: string): { mtimeMs: number; size: number; hash?: string } | null {\n const memo = depStatMemo.get(depPath);\n if (memo !== undefined) return memo;\n let result: { mtimeMs: number; size: number; hash?: string } | null;\n try {\n const stat = fs.statSync(depPath);\n result = { mtimeMs: stat.mtimeMs, size: stat.size };\n } catch {\n result = null;\n }\n depStatMemo.set(depPath, result);\n return result;\n}\n\nfunction hashDep(\n depPath: string,\n stat: { mtimeMs: number; size: number; hash?: string },\n): string | null {\n if (stat.hash !== undefined) return stat.hash;\n try {\n stat.hash = sha1(fs.readFileSync(depPath));\n return stat.hash;\n } catch {\n return null;\n }\n}\n\n/** Reset the per-process dep validation memos (watch-mode file changes). */\nexport function resetDepValidationMemo(): void {\n depStatMemo.clear();\n depsetVerdictMemo.clear();\n}\n\n/** Instances with pending deferred entries, flushed on process exit. */\nconst flushOnExit = new Set<DiskCache>();\nlet exitHookInstalled = false;\n\nfunction registerExitFlush(cache: DiskCache): void {\n flushOnExit.add(cache);\n if (!exitHookInstalled) {\n exitHookInstalled = true;\n // 'exit' allows only synchronous work; every write below is sync.\n process.once(\"exit\", () => {\n for (const c of flushOnExit) c.flushDeferred();\n });\n }\n}\n\nexport class DiskCache {\n private readonly dir: string;\n private readonly optionsKey: string;\n /** Superset snapshot provider (loader's executed first-party modules). */\n private readonly superset: (() => string[] | null) | null;\n private pending: PendingEntry[] = [];\n private initialized = false;\n\n constructor(dir: string, optionsKey: string, superset?: () => string[] | null) {\n this.dir = dir;\n this.optionsKey = optionsKey;\n this.superset = superset ?? null;\n }\n\n /**\n * Resolve the cache directory: an explicit string wins; otherwise\n * node_modules/.cache/zod-compiler under cwd (falling back to a\n * project-local .zod-compiler-cache when node_modules doesn't exist).\n */\n static resolveDir(cacheOption: string | true): string {\n if (typeof cacheOption === \"string\") return path.resolve(cacheOption);\n const nm = path.join(process.cwd(), \"node_modules\");\n if (fs.existsSync(nm)) return path.join(nm, \".cache\", \"zod-compiler\");\n return path.join(process.cwd(), \".zod-compiler-cache\");\n }\n\n key(id: string, code: string): string {\n return sha1(`${keyPrefix()}\\0${this.optionsKey}\\0${id}\\0${code}`);\n }\n\n private entryPath(key: string): string {\n return path.join(this.dir, `${key}.json`);\n }\n\n private depsetPath(id: string): string {\n return path.join(this.dir, DEPSET_DIR, `${id}.json`);\n }\n\n /**\n * One-time directory init: wipe on format mismatch (v1 inline-deps caches\n * reached 283 MB in the field — disposable by definition), then a\n * throttled GC pass. Best-effort throughout; a concurrent wipe/GC from\n * another process can only cause cache misses, never stale hits.\n */\n private ensureDir(): void {\n if (this.initialized) return;\n this.initialized = true;\n try {\n const metaPath = path.join(this.dir, META_FILE);\n let format = 0;\n try {\n format = (JSON.parse(fs.readFileSync(metaPath, \"utf8\")) as { format?: number }).format ?? 0;\n } catch {\n // missing or unreadable marker — treat as foreign format\n }\n if (format !== CACHE_FORMAT) {\n fs.rmSync(this.dir, { recursive: true, force: true });\n fs.mkdirSync(path.join(this.dir, DEPSET_DIR), { recursive: true });\n fs.writeFileSync(metaPath, JSON.stringify({ format: CACHE_FORMAT }));\n } else {\n fs.mkdirSync(path.join(this.dir, DEPSET_DIR), { recursive: true });\n this.maybeGc();\n }\n } catch {\n // cache stays best-effort\n }\n }\n\n /**\n * Throttled sweep: entries older than MAX_ENTRY_AGE_MS (orphaned by key\n * churn — content/version/options keys never repeat once inputs change)\n * and dep-set files no surviving entry references. Runs at most once per\n * GC_INTERVAL_MS per directory; the marker is claimed BEFORE sweeping so\n * concurrent processes skip. Deleting a dep-set raced by a concurrent\n * entry write only costs that entry a future miss — save() re-creates\n * absent dep-set files.\n */\n private maybeGc(): void {\n const marker = path.join(this.dir, GC_MARKER);\n try {\n const stat = fs.statSync(marker, { throwIfNoEntry: false });\n if (stat !== undefined && Date.now() - stat.mtimeMs < GC_INTERVAL_MS) return;\n fs.writeFileSync(marker, \"\");\n\n const now = Date.now();\n const referenced = new Set<string>();\n for (const name of fs.readdirSync(this.dir)) {\n const p = path.join(this.dir, name);\n if (name.endsWith(\".tmp\")) {\n const st = fs.statSync(p, { throwIfNoEntry: false });\n if (st !== undefined && now - st.mtimeMs > MAX_TMP_AGE_MS) fs.rmSync(p, { force: true });\n continue;\n }\n if (!name.endsWith(\".json\") || name === META_FILE) continue;\n try {\n const st = fs.statSync(p);\n if (now - st.mtimeMs > MAX_ENTRY_AGE_MS) {\n fs.rmSync(p, { force: true });\n continue;\n }\n const entry = JSON.parse(fs.readFileSync(p, \"utf8\")) as { depset?: string };\n if (typeof entry.depset === \"string\") referenced.add(entry.depset);\n } catch {\n fs.rmSync(p, { force: true });\n }\n }\n for (const name of fs.readdirSync(path.join(this.dir, DEPSET_DIR))) {\n if (!name.endsWith(\".json\")) continue;\n const id = name.slice(0, -\".json\".length);\n if (!referenced.has(id)) {\n fs.rmSync(path.join(this.dir, DEPSET_DIR, name), { force: true });\n }\n }\n } catch {\n // best effort\n }\n }\n\n /** Load an entry and validate its dep-set. Any failure → null. */\n load(key: string): CacheEntry | null {\n this.ensureDir();\n let entry: CacheEntry;\n try {\n entry = JSON.parse(fs.readFileSync(this.entryPath(key), \"utf8\")) as CacheEntry;\n } catch {\n return null;\n }\n if (entry === null || typeof entry !== \"object\" || typeof entry.depset !== \"string\") {\n return null;\n }\n if (!this.validateDepset(entry.depset)) return null;\n return entry;\n }\n\n /**\n * Validate every dep in a dep-set, once per process per set: superset\n * entries all share one set, so the ~N-file validation (and the JSON\n * parse) happens once instead of once per entry.\n */\n private validateDepset(id: string): boolean {\n const depsetFile = this.depsetPath(id);\n const memo = depsetVerdictMemo.get(depsetFile);\n if (memo !== undefined) return memo;\n let verdict = true;\n try {\n const parsed = JSON.parse(fs.readFileSync(depsetFile, \"utf8\")) as DepsetFile;\n if (parsed === null || typeof parsed !== \"object\" || typeof parsed.files !== \"object\") {\n verdict = false;\n } else {\n for (const [depPath, recorded] of Object.entries(parsed.files)) {\n const current = statDep(depPath);\n if (current === null) {\n verdict = false;\n break;\n }\n if (current.mtimeMs === recorded.mtimeMs && current.size === recorded.size) continue;\n if (hashDep(depPath, current) !== recorded.hash) {\n verdict = false;\n break;\n }\n }\n }\n } catch {\n verdict = false;\n }\n depsetVerdictMemo.set(depsetFile, verdict);\n return verdict;\n }\n\n /**\n * Stat + hash every dep into a content-addressed record map. The id hashes\n * sorted (path, content-hash) pairs ONLY — mtimes are validation fast-path\n * hints and must not fork the file name across checkouts/touches. Returns\n * null when any dep cannot be read (an unvalidatable set must not persist).\n */\n private buildDepset(depPaths: readonly string[]): { id: string; content: DepsetFile } | null {\n const records: Record<string, DepRecord> = {};\n for (const depPath of depPaths) {\n if (records[depPath] !== undefined) continue;\n const stat = statDep(depPath);\n const hash = stat === null ? null : hashDep(depPath, stat);\n if (stat === null || hash === null) return null;\n records[depPath] = { hash, mtimeMs: stat.mtimeMs, size: stat.size };\n }\n const sorted = Object.keys(records).sort();\n const id = sha1(sorted.map((p) => `${p}\\0${(records[p] as DepRecord).hash}`).join(\"\\n\"));\n return { id, content: { files: records } };\n }\n\n /** Write a dep-set file if absent (content-addressed: same id ⟹ same bytes). */\n private writeDepset(id: string, content: DepsetFile): boolean {\n const file = this.depsetPath(id);\n try {\n if (fs.existsSync(file)) return true;\n const tmp = `${file}.${process.pid}.tmp`;\n fs.writeFileSync(tmp, JSON.stringify(content));\n fs.renameSync(tmp, file);\n return true;\n } catch {\n return false;\n }\n }\n\n private writeEntry(\n key: string,\n depsetId: string,\n result: string | null,\n stats?: CacheEntryStats,\n map?: CacheSourceMap | null,\n ): void {\n const entry: CacheEntry = { result, depset: depsetId };\n if (stats) entry.stats = stats;\n if (map !== undefined && map !== null) {\n // The remapping result is a class instance; persist its JSON fields.\n entry.map = {\n version: map.version,\n sources: map.sources,\n ...(map.sourcesContent !== undefined ? { sourcesContent: map.sourcesContent } : {}),\n names: map.names,\n mappings: map.mappings,\n ...(map.file !== undefined ? { file: map.file } : {}),\n };\n }\n try {\n const file = this.entryPath(key);\n const tmp = `${file}.${process.pid}.tmp`;\n fs.writeFileSync(tmp, JSON.stringify(entry));\n fs.renameSync(tmp, file);\n } catch {\n // Cache writes are best-effort; failures only cost a recompute.\n }\n }\n\n /** Persist an entry whose dependency set is fully known (static crawl complete). */\n save(\n key: string,\n result: string | null,\n depPaths: readonly string[],\n stats?: CacheEntryStats,\n map?: CacheSourceMap | null,\n ): void {\n this.ensureDir();\n const built = this.buildDepset(depPaths);\n if (built === null) return;\n if (!this.writeDepset(built.id, built.content)) return;\n this.writeEntry(key, built.id, result, stats, map);\n }\n\n /**\n * Queue an entry whose static dep crawl was incomplete. Persisted by\n * flushDeferred() against ONE end-of-build superset snapshot — recording\n * the snapshot at save time gave every entry a distinct point-in-time\n * copy (the executed-modules set grows as discovery progresses).\n */\n saveDeferred(\n key: string,\n result: string | null,\n stats?: CacheEntryStats,\n map?: CacheSourceMap | null,\n ): void {\n if (this.superset === null) return;\n this.pending.push({ key, result, stats, map });\n registerExitFlush(this);\n }\n\n /**\n * Flush queued superset entries against the current loader snapshot.\n * Wired to buildEnd and (as a fallback) process exit; idempotent.\n */\n flushDeferred(): void {\n if (this.pending.length === 0) return;\n const pending = this.pending;\n this.pending = [];\n const snapshot = this.superset === null ? null : this.superset();\n if (snapshot === null) return;\n this.ensureDir();\n const built = this.buildDepset(snapshot);\n if (built === null) return;\n if (!this.writeDepset(built.id, built.content)) return;\n for (const p of pending) {\n this.writeEntry(p.key, built.id, p.result, p.stats, p.map);\n }\n }\n\n /**\n * Discard queued superset entries (watch-mode file change): their results\n * predate the change, but a flush would record post-change dep hashes —\n * the one pairing that could validate a stale result.\n */\n dropDeferred(): void {\n this.pending = [];\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"disk-cache.js","names":["fs","path"],"sources":["../../src/unplugin/disk-cache.ts"],"sourcesContent":["/**\n * Persistent transform-result cache.\n *\n * The expensive part of the unplugin transform is discovery: executing the\n * schema file (and transitively its whole first-party import graph) through\n * jiti inside the bundler's single-threaded server process. The in-memory\n * caches die with the process, so every `vitest run` / build re-pays that\n * cost even when nothing changed — which is exactly the loop integration\n * tests live in.\n *\n * Entries are keyed by a hash of (plugin version, zod version, transform\n * options, file id, file content) and validated against a recorded snapshot\n * of first-party files. Validation uses an mtime+size fast path and falls\n * back to content hashing, so a `touch` without changes still hits. A\n * superset of true dependencies only over-invalidates, never serves stale\n * output.\n *\n * Layout (CACHE_FORMAT 2): dependency snapshots are CONTENT-ADDRESSED and\n * shared — `deps/<sha1>.json` holds the {path → hash/mtime/size} map, and\n * each entry stores only the dep-set id. The v1 format inlined the full dep\n * map into every entry; in a large-codebase field report 835 superset-\n * fallback entries each embedded a ~1,900-file point-in-time snapshot (813\n * DISTINCT snapshots — the executed-modules superset grows as the build\n * progresses, so per-entry copies cannot even dedupe), totalling 283 MB\n * that any commit invalidated wholesale. Sharing dep-sets also means each\n * unique set is parsed and validated once per process instead of once per\n * entry.\n *\n * Superset fallbacks (entries whose static dep crawl was incomplete) are\n * DEFERRED: queued in memory and flushed in buildEnd / process exit against\n * a single end-of-build superset snapshot, so every superset entry of a\n * build shares ONE dep-set file (recording the still-growing snapshot at\n * save time is what produced 813 distinct copies). Deferred entries are\n * dropped on watchChange — a dependency edited between queueing and flush\n * would pair post-change hashes with a pre-change result, which is the one\n * combination that can serve stale output. A killed process loses only its\n * pending superset entries (the static-complete majority persists\n * immediately); the next run re-pays discovery for those files alone.\n *\n * Writes are atomic (tmp file + rename) so concurrent bundler processes\n * (vitest workspace projects) can share one cache directory safely.\n */\n\nimport { createHash } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/** Bump when the on-disk layout changes; mismatched directories are wiped. */\nconst CACHE_FORMAT = 2;\nconst META_FILE = \"_meta.json\";\nconst GC_MARKER = \"_gc\";\nconst DEPSET_DIR = \"deps\";\n/** GC at most once per day per cache directory. */\nconst GC_INTERVAL_MS = 24 * 60 * 60 * 1000;\n/** Entries unread for this long are presumed orphaned by key churn. */\nconst MAX_ENTRY_AGE_MS = 30 * 24 * 60 * 60 * 1000;\n/** Stale atomic-write temp files (crashed processes) older than this are removed. */\nconst MAX_TMP_AGE_MS = 60 * 60 * 1000;\n\nexport interface CacheEntryStats {\n schemas: number;\n optimized: number;\n}\n\ninterface DepRecord {\n hash: string;\n mtimeMs: number;\n size: number;\n}\n\nexport interface CacheEntry {\n /** Transformed code, or null when the transform produced no change. */\n result: string | null;\n /** Content-addressed id of the shared dep-set file (deps/<id>.json). */\n depset: string;\n /** Build stats to replay on cache hits (only present when schemas compiled). */\n stats?: CacheEntryStats;\n /** Composed sourcemap for `result` (original → transformed). */\n map?: CacheSourceMap | null;\n}\n\n/** JSON shape of the persisted sourcemap (mirrors TransformSourceMap). */\nexport interface CacheSourceMap {\n version: number;\n sources: (string | null)[];\n sourcesContent?: (string | null)[];\n names: string[];\n mappings: string;\n file?: string | null;\n}\n\ninterface DepsetFile {\n files: Record<string, DepRecord>;\n}\n\ninterface PendingEntry {\n key: string;\n result: string | null;\n stats?: CacheEntryStats | undefined;\n map?: CacheSourceMap | null | undefined;\n}\n\nfunction sha1(data: string | Buffer): string {\n return createHash(\"sha1\").update(data).digest(\"hex\");\n}\n\n/** Read own package version once for cache keying (src and dist both sit two levels below the package root). */\nfunction readPluginVersion(): string {\n try {\n const pkgPath = new URL(\"../../package.json\", import.meta.url);\n const pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf8\")) as { version?: string };\n return pkg.version ?? \"0\";\n } catch {\n return \"0\";\n }\n}\n\n/** Resolve the installed zod version — generated code depends on zod internals. */\nfunction readZodVersion(): string {\n try {\n const pkgPath = path.join(process.cwd(), \"node_modules\", \"zod\", \"package.json\");\n const pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf8\")) as { version?: string };\n return pkg.version ?? \"0\";\n } catch {\n return \"0\";\n }\n}\n\n/**\n * Build fingerprint: a content hash of the package's own source trees.\n *\n * The published version string alone is not enough — file: installs, linked\n * monorepo packages, and canary builds rebuild the compiler without a version\n * bump, and serving codegen from an older compiler build would be silently\n * stale.\n *\n * CONTENT-addressed, for the same reason depset ids are: mtimes do not survive\n * an install. pnpm rewrites every mtime under its `copy` import method, and\n * copy is what you get whenever the store sits on a different filesystem than\n * the workspace — the default on CI runners that mount the store as its own\n * volume (`npm_config_package_import_method=copy`). An mtime fingerprint\n * therefore rotates on every `pnpm install --frozen-lockfile`, which changes\n * every cache key and makes a restored CI cache wholly unreachable: the archive\n * unpacks, and nothing in it is ever looked up. Hardlink and APFS-clone installs\n * DO preserve mtimes, so the bug hides locally and reproduces only on the\n * runners that need the cache most.\n *\n * A content hash is also strictly tighter than mtime for the stated purpose: a\n * rebuild that reproduces identical bytes no longer discards the whole cache.\n *\n * Hashing ~0.7 MB across ~250 files costs ~5ms — once per process, and only if\n * a key is actually built (see `keyPrefix`).\n */\nexport function computeBuildFingerprint(root: string): string {\n const found: string[] = [];\n const walk = (dir: string): void => {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n const p = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n walk(p);\n } else if (/\\.(?:js|ts|json)$/.test(entry.name)) {\n // Relative + POSIX-normalised: the absolute prefix differs per checkout\n // and the separator differs per platform, neither of which is a rebuild.\n found.push(path.relative(root, p).split(path.sep).join(\"/\"));\n }\n }\n };\n for (const sub of [\"dist\", \"src\"]) {\n walk(path.join(root, sub));\n }\n found.sort();\n const hash = createHash(\"sha1\");\n for (const rel of found) {\n hash.update(rel);\n hash.update(\"\\0\");\n try {\n hash.update(fs.readFileSync(path.join(root, rel)));\n } catch {\n // Unreadable file: fold in a marker rather than abort. Collapsing to a\n // constant on error would make genuinely different builds share a key.\n hash.update(\"<unreadable>\");\n }\n }\n return `${hash.digest(\"hex\")}:${found.length}`;\n}\n\nfunction readBuildFingerprint(): string {\n try {\n return computeBuildFingerprint(fileURLToPath(new URL(\"../..\", import.meta.url)));\n } catch {\n return \"0\";\n }\n}\n\n/**\n * Version + build identity shared by every key in the process. Computed on\n * first key, not at import: reading two package.json files and hashing dist is\n * pure waste for a build that never enables the cache.\n */\nlet keyPrefixMemo: string | null = null;\n\nfunction keyPrefix(): string {\n keyPrefixMemo ??= `${readPluginVersion()}\\0${readBuildFingerprint()}\\0${readZodVersion()}`;\n return keyPrefixMemo;\n}\n\n/**\n * Per-process memo: path → current stat (+ lazily computed content hash).\n * One fs.stat per file per process regardless of how many dep-sets\n * reference it; content is read at most once.\n */\nconst depStatMemo = new Map<string, { mtimeMs: number; size: number; hash?: string } | null>();\n\n/** Per-process memo: dep-set file path → validation verdict. */\nconst depsetVerdictMemo = new Map<string, boolean>();\n\nfunction statDep(depPath: string): { mtimeMs: number; size: number; hash?: string } | null {\n const memo = depStatMemo.get(depPath);\n if (memo !== undefined) return memo;\n let result: { mtimeMs: number; size: number; hash?: string } | null;\n try {\n const stat = fs.statSync(depPath);\n result = { mtimeMs: stat.mtimeMs, size: stat.size };\n } catch {\n result = null;\n }\n depStatMemo.set(depPath, result);\n return result;\n}\n\nfunction hashDep(\n depPath: string,\n stat: { mtimeMs: number; size: number; hash?: string },\n): string | null {\n if (stat.hash !== undefined) return stat.hash;\n try {\n stat.hash = sha1(fs.readFileSync(depPath));\n return stat.hash;\n } catch {\n return null;\n }\n}\n\n/** Reset the per-process dep validation memos (watch-mode file changes). */\nexport function resetDepValidationMemo(): void {\n depStatMemo.clear();\n depsetVerdictMemo.clear();\n}\n\n/** Instances with pending deferred entries, flushed on process exit. */\nconst flushOnExit = new Set<DiskCache>();\nlet exitHookInstalled = false;\n\nfunction registerExitFlush(cache: DiskCache): void {\n flushOnExit.add(cache);\n if (!exitHookInstalled) {\n exitHookInstalled = true;\n // 'exit' allows only synchronous work; every write below is sync.\n process.once(\"exit\", () => {\n for (const c of flushOnExit) c.flushDeferred();\n });\n }\n}\n\nexport class DiskCache {\n private readonly dir: string;\n private readonly optionsKey: string;\n /** Superset snapshot provider (loader's executed first-party modules). */\n private readonly superset: (() => string[] | null) | null;\n private pending: PendingEntry[] = [];\n private initialized = false;\n\n constructor(dir: string, optionsKey: string, superset?: () => string[] | null) {\n this.dir = dir;\n this.optionsKey = optionsKey;\n this.superset = superset ?? null;\n }\n\n /**\n * Resolve the cache directory: an explicit string wins; otherwise\n * node_modules/.cache/zod-compiler under cwd (falling back to a\n * project-local .zod-compiler-cache when node_modules doesn't exist).\n */\n static resolveDir(cacheOption: string | true): string {\n if (typeof cacheOption === \"string\") return path.resolve(cacheOption);\n const nm = path.join(process.cwd(), \"node_modules\");\n if (fs.existsSync(nm)) return path.join(nm, \".cache\", \"zod-compiler\");\n return path.join(process.cwd(), \".zod-compiler-cache\");\n }\n\n key(id: string, code: string): string {\n return sha1(`${keyPrefix()}\\0${this.optionsKey}\\0${id}\\0${code}`);\n }\n\n private entryPath(key: string): string {\n return path.join(this.dir, `${key}.json`);\n }\n\n private depsetPath(id: string): string {\n return path.join(this.dir, DEPSET_DIR, `${id}.json`);\n }\n\n /**\n * One-time directory init: wipe on format mismatch (v1 inline-deps caches\n * reached 283 MB in the field — disposable by definition), then a\n * throttled GC pass. Best-effort throughout; a concurrent wipe/GC from\n * another process can only cause cache misses, never stale hits.\n */\n private ensureDir(): void {\n if (this.initialized) return;\n this.initialized = true;\n try {\n const metaPath = path.join(this.dir, META_FILE);\n let format = 0;\n try {\n format = (JSON.parse(fs.readFileSync(metaPath, \"utf8\")) as { format?: number }).format ?? 0;\n } catch {\n // missing or unreadable marker — treat as foreign format\n }\n if (format !== CACHE_FORMAT) {\n fs.rmSync(this.dir, { recursive: true, force: true });\n fs.mkdirSync(path.join(this.dir, DEPSET_DIR), { recursive: true });\n fs.writeFileSync(metaPath, JSON.stringify({ format: CACHE_FORMAT }));\n } else {\n fs.mkdirSync(path.join(this.dir, DEPSET_DIR), { recursive: true });\n this.maybeGc();\n }\n } catch {\n // cache stays best-effort\n }\n }\n\n /**\n * Throttled sweep: entries older than MAX_ENTRY_AGE_MS (orphaned by key\n * churn — content/version/options keys never repeat once inputs change)\n * and dep-set files no surviving entry references. Runs at most once per\n * GC_INTERVAL_MS per directory; the marker is claimed BEFORE sweeping so\n * concurrent processes skip. Deleting a dep-set raced by a concurrent\n * entry write only costs that entry a future miss — save() re-creates\n * absent dep-set files.\n */\n private maybeGc(): void {\n const marker = path.join(this.dir, GC_MARKER);\n try {\n const stat = fs.statSync(marker, { throwIfNoEntry: false });\n if (stat !== undefined && Date.now() - stat.mtimeMs < GC_INTERVAL_MS) return;\n fs.writeFileSync(marker, \"\");\n\n const now = Date.now();\n const referenced = new Set<string>();\n for (const name of fs.readdirSync(this.dir)) {\n const p = path.join(this.dir, name);\n if (name.endsWith(\".tmp\")) {\n const st = fs.statSync(p, { throwIfNoEntry: false });\n if (st !== undefined && now - st.mtimeMs > MAX_TMP_AGE_MS) fs.rmSync(p, { force: true });\n continue;\n }\n if (!name.endsWith(\".json\") || name === META_FILE) continue;\n try {\n const st = fs.statSync(p);\n if (now - st.mtimeMs > MAX_ENTRY_AGE_MS) {\n fs.rmSync(p, { force: true });\n continue;\n }\n const entry = JSON.parse(fs.readFileSync(p, \"utf8\")) as { depset?: string };\n if (typeof entry.depset === \"string\") referenced.add(entry.depset);\n } catch {\n fs.rmSync(p, { force: true });\n }\n }\n for (const name of fs.readdirSync(path.join(this.dir, DEPSET_DIR))) {\n if (!name.endsWith(\".json\")) continue;\n const id = name.slice(0, -\".json\".length);\n if (!referenced.has(id)) {\n fs.rmSync(path.join(this.dir, DEPSET_DIR, name), { force: true });\n }\n }\n } catch {\n // best effort\n }\n }\n\n /** Load an entry and validate its dep-set. Any failure → null. */\n load(key: string): CacheEntry | null {\n this.ensureDir();\n let entry: CacheEntry;\n try {\n entry = JSON.parse(fs.readFileSync(this.entryPath(key), \"utf8\")) as CacheEntry;\n } catch {\n return null;\n }\n if (entry === null || typeof entry !== \"object\" || typeof entry.depset !== \"string\") {\n return null;\n }\n if (!this.validateDepset(entry.depset)) return null;\n return entry;\n }\n\n /**\n * Validate every dep in a dep-set, once per process per set: superset\n * entries all share one set, so the ~N-file validation (and the JSON\n * parse) happens once instead of once per entry.\n */\n private validateDepset(id: string): boolean {\n const depsetFile = this.depsetPath(id);\n const memo = depsetVerdictMemo.get(depsetFile);\n if (memo !== undefined) return memo;\n let verdict = true;\n try {\n const parsed = JSON.parse(fs.readFileSync(depsetFile, \"utf8\")) as DepsetFile;\n if (parsed === null || typeof parsed !== \"object\" || typeof parsed.files !== \"object\") {\n verdict = false;\n } else {\n for (const [depPath, recorded] of Object.entries(parsed.files)) {\n const current = statDep(depPath);\n if (current === null) {\n verdict = false;\n break;\n }\n if (current.mtimeMs === recorded.mtimeMs && current.size === recorded.size) continue;\n if (hashDep(depPath, current) !== recorded.hash) {\n verdict = false;\n break;\n }\n }\n }\n } catch {\n verdict = false;\n }\n depsetVerdictMemo.set(depsetFile, verdict);\n return verdict;\n }\n\n /**\n * Stat + hash every dep into a content-addressed record map. The id hashes\n * sorted (path, content-hash) pairs ONLY — mtimes are validation fast-path\n * hints and must not fork the file name across checkouts/touches. Returns\n * null when any dep cannot be read (an unvalidatable set must not persist).\n */\n private buildDepset(depPaths: readonly string[]): { id: string; content: DepsetFile } | null {\n const records: Record<string, DepRecord> = {};\n for (const depPath of depPaths) {\n if (records[depPath] !== undefined) continue;\n const stat = statDep(depPath);\n const hash = stat === null ? null : hashDep(depPath, stat);\n if (stat === null || hash === null) return null;\n records[depPath] = { hash, mtimeMs: stat.mtimeMs, size: stat.size };\n }\n const sorted = Object.keys(records).sort();\n const id = sha1(sorted.map((p) => `${p}\\0${(records[p] as DepRecord).hash}`).join(\"\\n\"));\n return { id, content: { files: records } };\n }\n\n /** Write a dep-set file if absent (content-addressed: same id ⟹ same bytes). */\n private writeDepset(id: string, content: DepsetFile): boolean {\n const file = this.depsetPath(id);\n try {\n if (fs.existsSync(file)) return true;\n const tmp = `${file}.${process.pid}.tmp`;\n fs.writeFileSync(tmp, JSON.stringify(content));\n fs.renameSync(tmp, file);\n return true;\n } catch {\n return false;\n }\n }\n\n private writeEntry(\n key: string,\n depsetId: string,\n result: string | null,\n stats?: CacheEntryStats,\n map?: CacheSourceMap | null,\n ): void {\n const entry: CacheEntry = { result, depset: depsetId };\n if (stats) entry.stats = stats;\n if (map !== undefined && map !== null) {\n // The remapping result is a class instance; persist its JSON fields.\n entry.map = {\n version: map.version,\n sources: map.sources,\n ...(map.sourcesContent !== undefined ? { sourcesContent: map.sourcesContent } : {}),\n names: map.names,\n mappings: map.mappings,\n ...(map.file !== undefined ? { file: map.file } : {}),\n };\n }\n try {\n const file = this.entryPath(key);\n const tmp = `${file}.${process.pid}.tmp`;\n fs.writeFileSync(tmp, JSON.stringify(entry));\n fs.renameSync(tmp, file);\n } catch {\n // Cache writes are best-effort; failures only cost a recompute.\n }\n }\n\n /** Persist an entry whose dependency set is fully known (static crawl complete). */\n save(\n key: string,\n result: string | null,\n depPaths: readonly string[],\n stats?: CacheEntryStats,\n map?: CacheSourceMap | null,\n ): void {\n this.ensureDir();\n const built = this.buildDepset(depPaths);\n if (built === null) return;\n if (!this.writeDepset(built.id, built.content)) return;\n this.writeEntry(key, built.id, result, stats, map);\n }\n\n /**\n * Queue an entry whose static dep crawl was incomplete. Persisted by\n * flushDeferred() against ONE end-of-build superset snapshot — recording\n * the snapshot at save time gave every entry a distinct point-in-time\n * copy (the executed-modules set grows as discovery progresses).\n */\n saveDeferred(\n key: string,\n result: string | null,\n stats?: CacheEntryStats,\n map?: CacheSourceMap | null,\n ): void {\n if (this.superset === null) return;\n this.pending.push({ key, result, stats, map });\n registerExitFlush(this);\n }\n\n /**\n * Flush queued superset entries against the current loader snapshot.\n * Wired to buildEnd and (as a fallback) process exit; idempotent.\n */\n flushDeferred(): void {\n if (this.pending.length === 0) return;\n const pending = this.pending;\n this.pending = [];\n const snapshot = this.superset === null ? null : this.superset();\n if (snapshot === null) return;\n // Every pending entry got here by RUNNING discovery, so the set of modules\n // that discovery executed cannot be empty. An empty snapshot means the\n // provider lost track of them — the loader was invalidated between save\n // and flush, or transforms ran somewhere this process cannot observe — and\n // an entry with no deps validates as fresh forever. Declining costs one\n // recompute; persisting would serve that stale result indefinitely.\n if (snapshot.length === 0) return;\n this.ensureDir();\n const built = this.buildDepset(snapshot);\n if (built === null) return;\n if (!this.writeDepset(built.id, built.content)) return;\n for (const p of pending) {\n this.writeEntry(p.key, built.id, p.result, p.stats, p.map);\n }\n }\n\n /**\n * Discard queued superset entries (watch-mode file change): their results\n * predate the change, but a flush would record post-change dep hashes —\n * the one pairing that could validate a stale result.\n */\n dropDeferred(): void {\n this.pending = [];\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/unplugin/index.ts"],"mappings":";;cA4Da,6BAAQ,iBAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/unplugin/index.ts"],"mappings":";;cAoEa,6BAAQ,iBAAA"}
@@ -1,8 +1,9 @@
1
1
  import { getFirstPartyModulePaths, invalidateModuleCache } from "../loader.js";
2
2
  import { RESOLVED_RUNTIME_ID_FILTER, RUNTIME_SPECIFIER_FILTER, VIRTUAL_RUNTIME_ID, WP_RUNTIME_ID, loadVirtual, resolveVirtualId } from "./virtual.js";
3
- import { TRANSFORM_ID_FILTER, log, shouldTransform, transformCodeFilter, transformCodeWithMap } from "./transform.js";
3
+ import { TRANSFORM_ID_FILTER, log, shouldTransform, transformCodeFilter, transformCodeWithMap, warn } from "./transform.js";
4
4
  import { collectStaticDeps, resetDepGraphMemo } from "./dep-graph.js";
5
5
  import { DiskCache, resetDepValidationMemo } from "./disk-cache.js";
6
+ import { PoolUnavailableError, TransformPool, poolTransformOptions, resolvePoolSize } from "./pool.js";
6
7
  import { BuildStatsAccumulator } from "./types.js";
7
8
  import process from "node:process";
8
9
  import { createUnplugin } from "unplugin";
@@ -50,13 +51,38 @@ const unplugin = createUnplugin((options, meta) => {
50
51
  const mode = options?.codegenMode ?? (VIRTUAL_MODULE_FRAMEWORKS.has(meta.framework) ? "lean" : "inline");
51
52
  const runtimeId = WP_FRAMEWORKS.has(meta.framework) ? WP_RUNTIME_ID : VIRTUAL_RUNTIME_ID;
52
53
  const cacheOption = options?.cache ?? true;
54
+ const poolSize = resolvePoolSize(options?.parallel);
55
+ const pool = TransformPool.create(poolSize);
56
+ /** Dedupes the in-process-fallback warning to one per build. */
57
+ let warnedPoolFallback = false;
58
+ if (poolSize > 0 && pool === null) warn("parallel was requested but the transform worker entry could not be located (is zod-compiler bundled into your build?). Falling back to in-process transforms.");
59
+ /**
60
+ * Executed-module superset for the disk cache's deferred entries.
61
+ *
62
+ * With a pool, discovery happens in the workers, so this process's own
63
+ * loader has executed nothing and `getFirstPartyModulePaths()` alone would
64
+ * report an empty (or absent) set — silently dropping every deferred
65
+ * entry, which on large graphs is most of them. Both sides are unioned:
66
+ * the workers' reports, plus anything the in-process fallback executed
67
+ * here. A worker that cannot track modules at all (Bun/Deno native import)
68
+ * reports null, and null wins — the cache declines to persist rather than
69
+ * record a set it knows is incomplete.
70
+ */
71
+ const executedModuleSuperset = () => {
72
+ const local = getFirstPartyModulePaths();
73
+ if (pool === null) return local;
74
+ const remote = pool.firstPartyModulePaths();
75
+ if (remote === null) return null;
76
+ if (local === null) return remote;
77
+ return [.../* @__PURE__ */ new Set([...local, ...remote])];
78
+ };
53
79
  const diskCache = cacheOption === false ? null : new DiskCache(DiskCache.resolveDir(cacheOption), JSON.stringify({
54
80
  mode,
55
81
  runtimeId,
56
82
  output: outputMode,
57
83
  schemas: schemasMode,
58
84
  hoist: typeof options?.hoist === "object" ? String(options.hoist.schemaNamePattern ?? "default") : options?.hoist ?? true
59
- }), getFirstPartyModulePaths);
85
+ }), executedModuleSuperset);
60
86
  const viteApply = options?.apply === "all" ? void 0 : options?.apply ?? ((_config, env) => env.command === "build" || env.mode === "test" || process.env["VITEST"] !== void 0);
61
87
  const codeFilter = transformCodeFilter(options);
62
88
  return {
@@ -111,18 +137,21 @@ const unplugin = createUnplugin((options, meta) => {
111
137
  };
112
138
  }
113
139
  }
114
- let discoveryRan = false;
115
- let substantialWork = false;
116
- let uncacheable = false;
117
- let fileStats = null;
118
- const output = await transformCodeWithMap(code, id, {
140
+ const transformOptions = {
119
141
  mode,
120
142
  runtimeId,
121
143
  verbose,
122
144
  zodCompat,
123
145
  compact,
124
146
  autoDiscover,
125
- hoist: options?.hoist,
147
+ hoist: options?.hoist
148
+ };
149
+ let discoveryRan = false;
150
+ let substantialWork = false;
151
+ let uncacheable = false;
152
+ let fileStats = null;
153
+ const inProcess = async () => transformCodeWithMap(code, id, {
154
+ ...transformOptions,
126
155
  onDiscovery() {
127
156
  discoveryRan = true;
128
157
  },
@@ -133,10 +162,27 @@ const unplugin = createUnplugin((options, meta) => {
133
162
  uncacheable = true;
134
163
  },
135
164
  onBuildStats(s) {
136
- stats.add(s);
137
165
  fileStats = s;
138
166
  }
139
167
  });
168
+ let output;
169
+ if (pool === null) output = await inProcess();
170
+ else try {
171
+ const r = await pool.run(code, id, poolTransformOptions(transformOptions));
172
+ output = r.output;
173
+ discoveryRan = r.discoveryRan;
174
+ substantialWork = r.substantialWork;
175
+ uncacheable = r.uncacheable;
176
+ fileStats = r.stats;
177
+ } catch (error) {
178
+ if (!(error instanceof PoolUnavailableError)) throw error;
179
+ if (!warnedPoolFallback) {
180
+ warnedPoolFallback = true;
181
+ warn(`${error.message} — compiling ${id} in-process instead. Later files take the same path without repeating this warning.`);
182
+ }
183
+ output = await inProcess();
184
+ }
185
+ if (fileStats !== null) stats.add(fileStats);
140
186
  const result = output === null ? null : output.code;
141
187
  const map = output === null ? null : output.map;
142
188
  cache.set(id, {
@@ -168,6 +214,7 @@ const unplugin = createUnplugin((options, meta) => {
168
214
  if (!SOURCE_LIKE.test(id)) return;
169
215
  if (id.includes("node_modules")) return;
170
216
  invalidateModuleCache();
217
+ pool?.invalidate();
171
218
  resetDepValidationMemo();
172
219
  resetDepGraphMemo();
173
220
  diskCache?.dropDeferred();
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/unplugin/index.ts"],"sourcesContent":["import process from \"node:process\";\nimport { createUnplugin, type UnpluginContextMeta } from \"unplugin\";\nimport type { CodegenMode } from \"../core/codegen/context.js\";\nimport { getFirstPartyModulePaths, invalidateModuleCache } from \"../loader.js\";\nimport { collectStaticDeps, resetDepGraphMemo } from \"./dep-graph.js\";\nimport { DiskCache, resetDepValidationMemo } from \"./disk-cache.js\";\nimport {\n log,\n shouldTransform,\n TRANSFORM_ID_FILTER,\n transformCodeFilter,\n type TransformSourceMap,\n transformCodeWithMap,\n} from \"./transform.js\";\nimport type { BuildStats, ZodCompilerPluginOptions } from \"./types.js\";\nimport { BuildStatsAccumulator } from \"./types.js\";\nimport {\n loadVirtual,\n RESOLVED_RUNTIME_ID_FILTER,\n resolveVirtualId,\n RUNTIME_SPECIFIER_FILTER,\n VIRTUAL_RUNTIME_ID,\n WP_RUNTIME_ID,\n} from \"./virtual.js\";\n\n/**\n * Frameworks whose resolveId/load hooks receive any import specifier, including\n * `virtual:` URIs and bare specifiers, so lean-mode cross-file dedup works.\n * webpack / rspack reject the `virtual:` URI scheme but accept bare specifiers,\n * so they use WP_RUNTIME_ID (`__zod-compiler-runtime__`) instead of VIRTUAL_RUNTIME_ID.\n */\nconst VIRTUAL_MODULE_FRAMEWORKS = new Set([\n \"vite\",\n \"rollup\",\n \"rolldown\",\n \"esbuild\",\n \"farm\",\n \"bun\",\n \"rspack\",\n \"webpack\",\n]);\n\n/** Frameworks that need the bare-specifier runtime ID instead of `virtual:`. */\nconst WP_FRAMEWORKS = new Set([\"rspack\", \"webpack\"]);\n\n/** File extensions whose changes can affect a schema module graph. */\nconst SOURCE_LIKE = /\\.([cm]?[jt]sx?|json)$/;\n\n/**\n * esbuild has no hook-filter support of its own: unplugin registers a single\n * `onLoad` covering both the load and transform hooks, and its transform shim\n * reads the file off disk before any JS-side filter runs. `onLoadFilter` is\n * the one filter esbuild itself applies (in Go, before calling into JS), so it\n * carries the union of what either hook can handle — source files for\n * transform, the resolved runtime module for load (derived from the load\n * filter so the two cannot drift; a miss there would leave the lean-mode\n * runtime import unresolved). The hooks' own filters narrow it further.\n */\nconst ESBUILD_LOAD_FILTER = new RegExp(`\\\\.[cm]?[jt]sx?$|${RESOLVED_RUNTIME_ID_FILTER.source}`);\n\nexport const unplugin = createUnplugin(\n (options: ZodCompilerPluginOptions | undefined, meta: UnpluginContextMeta) => {\n const schemasMode = options?.schemas ?? \"auto\";\n const autoDiscover = schemasMode === \"auto\";\n const outputMode = options?.output ?? \"schema\";\n // \"compact\" keeps the Zod schema (its safeParse IS the cold error path) — so\n // it is zod-compatible like \"schema\"; only \"bag\" drops the schema object.\n const zodCompat = outputMode === \"schema\" || outputMode === \"compact\";\n const compact = outputMode === \"compact\";\n const stats = new BuildStatsAccumulator();\n // Transform results keyed by id, validated by content: bundlers re-run\n // transform for the same file (multiple environments, watch rebuilds) —\n // identical content returns the cached result, changed content recomputes.\n const cache = new Map<\n string,\n { code: string; result: string | null; map?: TransformSourceMap | null }\n >();\n const verbose = options?.verbose === true;\n const mode: CodegenMode =\n options?.codegenMode ?? (VIRTUAL_MODULE_FRAMEWORKS.has(meta.framework) ? \"lean\" : \"inline\");\n const runtimeId = WP_FRAMEWORKS.has(meta.framework) ? WP_RUNTIME_ID : VIRTUAL_RUNTIME_ID;\n // Persistent transform-result cache: discovery executes schema files (and\n // their import graphs) in-process, and the in-memory caches die with the\n // process — without a disk cache every test run / build re-pays that cost.\n // Entries self-validate against dep content hashes, so watch invalidation\n // semantics carry across processes.\n const cacheOption = options?.cache ?? true;\n const diskCache =\n cacheOption === false\n ? null\n : new DiskCache(\n DiskCache.resolveDir(cacheOption),\n JSON.stringify({\n mode,\n runtimeId,\n output: outputMode,\n schemas: schemasMode,\n hoist:\n typeof options?.hoist === \"object\"\n ? String(options.hoist.schemaNamePattern ?? \"default\")\n : (options?.hoist ?? true),\n }),\n getFirstPartyModulePaths,\n );\n // Vite only (other bundlers ignore the field): when the plugin runs.\n // The default compiles production builds AND test runs — tests should\n // exercise (and benefit from) the validators that ship — while plain dev\n // servers skip AOT cost and use the Zod fallback. Vitest runs Vite in\n // serve mode but is detectable via the VITEST env var / \"test\" mode.\n const viteApply =\n options?.apply === \"all\"\n ? undefined\n : (options?.apply ??\n ((_config: unknown, env: { command: string; mode: string }) =>\n env.command === \"build\" || env.mode === \"test\" || process.env[\"VITEST\"] !== undefined));\n\n // Hook filters (unplugin object hooks): bundlers that support them\n // natively — Rolldown, Vite, Rollup 4.40+ — reject a module before ever\n // calling into JS, and unplugin applies the same patterns itself\n // everywhere else. The `code` filter is the big one: a file that never\n // mentions zod cannot produce output, so it no longer costs a hook call,\n // a content hash and an import scan per build.\n const codeFilter = transformCodeFilter(options);\n const transformFilter =\n codeFilter === undefined\n ? { id: TRANSFORM_ID_FILTER }\n : { code: codeFilter, id: TRANSFORM_ID_FILTER };\n\n return {\n name: \"zod-compiler\",\n enforce: \"pre\" as const,\n\n vite: viteApply === undefined ? {} : { apply: viteApply },\n\n esbuild: { onLoadFilter: ESBUILD_LOAD_FILTER },\n\n resolveId: {\n filter: { id: RUNTIME_SPECIFIER_FILTER },\n handler(id: string) {\n return resolveVirtualId(id);\n },\n },\n\n load: {\n filter: { id: RESOLVED_RUNTIME_ID_FILTER },\n handler(id: string) {\n return loadVirtual(id);\n },\n },\n\n transform: {\n filter: transformFilter,\n async handler(code: string, id: string) {\n // The filter covers the static half of shouldTransform(); the\n // include/exclude options — picomatch `contains` semantics, which the\n // native filters do not reproduce — are applied here, as is the whole\n // check for hosts that ignore filters.\n if (!shouldTransform(id, options)) return;\n\n const cached = cache.get(id);\n if (cached && cached.code === code) {\n return cached.result === null\n ? undefined\n : { code: cached.result, map: cached.map ?? null };\n }\n if (cached) {\n // Content changed but no watchChange fired (bundlers without the\n // hook): drop stale module executions before re-discovering.\n invalidateModuleCache();\n }\n\n // Disk cache: skip static-filtering, discovery (file execution!) and\n // codegen entirely when a previous process already transformed this\n // exact content and every dep it executed is unchanged.\n const diskKey = diskCache === null ? null : diskCache.key(id, code);\n if (diskCache !== null && diskKey !== null) {\n const entry = diskCache.load(diskKey);\n if (entry !== null) {\n cache.set(id, { code, result: entry.result, map: entry.map ?? null });\n if (entry.stats) {\n stats.add({\n files: 1,\n schemas: entry.stats.schemas,\n optimized: entry.stats.optimized,\n failed: 0,\n });\n }\n if (verbose && entry.result !== null) {\n log(`Using cached transform for ${id}`);\n }\n return entry.result === null\n ? undefined\n : { code: entry.result, map: entry.map ?? null };\n }\n }\n\n let discoveryRan = false;\n let substantialWork = false;\n let uncacheable = false;\n let fileStats: BuildStats | null = null;\n const output = await transformCodeWithMap(code, id, {\n mode,\n runtimeId,\n verbose,\n zodCompat,\n compact,\n autoDiscover,\n hoist: options?.hoist,\n onDiscovery() {\n discoveryRan = true;\n },\n onSubstantialWork() {\n substantialWork = true;\n },\n onUncacheableResult() {\n uncacheable = true;\n },\n onBuildStats(s) {\n stats.add(s);\n fileStats = s;\n },\n });\n const result = output === null ? null : output.code;\n const map = output === null ? null : output.map;\n cache.set(id, { code, result, map });\n\n // Persist when the transform did real work: produced output, ran\n // discovery, or did parse-level work (hoist scan / static filter) —\n // even when that work concluded \"no transform needed\". Null results\n // used to be skipped on the theory that bail-outs are cheaper than a\n // cache probe; that holds for textual bail-outs (still never\n // persisted) but not for the scans: hoist-only mode re-paid a full\n // scan per zod-importing file per run (35.8s/run in a field report)\n // purely to re-derive nulls.\n if (\n diskCache !== null &&\n diskKey !== null &&\n !uncacheable &&\n (result !== null || discoveryRan || substantialWork)\n ) {\n // TS narrows fileStats to null here (assignment happens inside a\n // callback it cannot track) — widen back.\n const s = fileStats as BuildStats | null;\n const entryStats =\n s === null ? undefined : { schemas: s.schemas, optimized: s.optimized };\n if (!discoveryRan) {\n // Discovery-free results (hoist scans, static-filter rejections,\n // hoist-only rewrites) are pure functions of the file content:\n // no deps.\n diskCache.save(diskKey, result, [], entryStats, map);\n } else {\n // Per-file dependency sets: the file's static first-party import\n // graph, so editing an unrelated file no longer invalidates this\n // entry (the global superset recorded the whole project — in\n // large codebases every commit wiped the entire cache). The\n // entry file itself is always a dep: the cache key hashes the\n // content the BUNDLER passed, but discovery executed the file\n // from DISK — recording it guards the (rare) divergence between\n // the two. When the graph cannot be fully analyzed (non-literal\n // dynamic imports, unresolvable relative specifiers), the entry\n // is deferred and flushed in buildEnd against ONE end-of-build\n // executed-modules superset — immediate snapshots gave every\n // entry a distinct point-in-time copy (283 MB in the field).\n const staticDeps = collectStaticDeps(id);\n if (staticDeps.complete) {\n diskCache.save(diskKey, result, [id, ...staticDeps.deps], entryStats, map);\n } else {\n diskCache.saveDeferred(diskKey, result, entryStats, map);\n }\n }\n }\n\n if (!result) return;\n return { code: result, map };\n },\n },\n\n watchChange(id: string) {\n if (!SOURCE_LIKE.test(id)) return;\n if (id.includes(\"node_modules\")) return;\n // The changed file may be a dependency of any schema file, so both\n // the module cache (executions) and the transform result cache are\n // invalidated wholesale. node_modules stay warm in the loader. Disk\n // cache entries self-validate via dep hashes — only the per-process\n // stat memo needs resetting so changed files re-hash. Pending\n // deferred entries predate the change and must not flush against\n // post-change dep hashes.\n invalidateModuleCache();\n resetDepValidationMemo();\n resetDepGraphMemo();\n diskCache?.dropDeferred();\n cache.clear();\n },\n\n buildEnd() {\n // The loader's executed-modules superset is final here — persist the\n // queued incomplete-crawl entries against one shared snapshot.\n diskCache?.flushDeferred();\n if (!verbose) return;\n if (stats.schemas === 0) return;\n log(\n `Build summary: ${stats.optimized}/${stats.schemas} schemas optimized across ${stats.files} file(s)` +\n (stats.failed > 0 ? `, ${stats.failed} failed` : \"\"),\n );\n stats.reset();\n },\n };\n },\n);\n\nexport type { ZodCompilerPluginOptions } from \"./types.js\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/unplugin/index.ts"],"sourcesContent":["import process from \"node:process\";\nimport { createUnplugin, type UnpluginContextMeta } from \"unplugin\";\nimport type { CodegenMode } from \"../core/codegen/context.js\";\nimport { getFirstPartyModulePaths, invalidateModuleCache } from \"../loader.js\";\nimport { collectStaticDeps, resetDepGraphMemo } from \"./dep-graph.js\";\nimport { DiskCache, resetDepValidationMemo } from \"./disk-cache.js\";\nimport {\n poolTransformOptions,\n PoolUnavailableError,\n resolvePoolSize,\n TransformPool,\n} from \"./pool.js\";\nimport {\n log,\n shouldTransform,\n TRANSFORM_ID_FILTER,\n transformCodeFilter,\n type TransformOutput,\n type TransformSourceMap,\n transformCodeWithMap,\n warn,\n} from \"./transform.js\";\nimport type { BuildStats, TransformOptions, ZodCompilerPluginOptions } from \"./types.js\";\nimport { BuildStatsAccumulator } from \"./types.js\";\nimport {\n loadVirtual,\n RESOLVED_RUNTIME_ID_FILTER,\n resolveVirtualId,\n RUNTIME_SPECIFIER_FILTER,\n VIRTUAL_RUNTIME_ID,\n WP_RUNTIME_ID,\n} from \"./virtual.js\";\n\n/**\n * Frameworks whose resolveId/load hooks receive any import specifier, including\n * `virtual:` URIs and bare specifiers, so lean-mode cross-file dedup works.\n * webpack / rspack reject the `virtual:` URI scheme but accept bare specifiers,\n * so they use WP_RUNTIME_ID (`__zod-compiler-runtime__`) instead of VIRTUAL_RUNTIME_ID.\n */\nconst VIRTUAL_MODULE_FRAMEWORKS = new Set([\n \"vite\",\n \"rollup\",\n \"rolldown\",\n \"esbuild\",\n \"farm\",\n \"bun\",\n \"rspack\",\n \"webpack\",\n]);\n\n/** Frameworks that need the bare-specifier runtime ID instead of `virtual:`. */\nconst WP_FRAMEWORKS = new Set([\"rspack\", \"webpack\"]);\n\n/** File extensions whose changes can affect a schema module graph. */\nconst SOURCE_LIKE = /\\.([cm]?[jt]sx?|json)$/;\n\n/**\n * esbuild has no hook-filter support of its own: unplugin registers a single\n * `onLoad` covering both the load and transform hooks, and its transform shim\n * reads the file off disk before any JS-side filter runs. `onLoadFilter` is\n * the one filter esbuild itself applies (in Go, before calling into JS), so it\n * carries the union of what either hook can handle — source files for\n * transform, the resolved runtime module for load (derived from the load\n * filter so the two cannot drift; a miss there would leave the lean-mode\n * runtime import unresolved). The hooks' own filters narrow it further.\n */\nconst ESBUILD_LOAD_FILTER = new RegExp(`\\\\.[cm]?[jt]sx?$|${RESOLVED_RUNTIME_ID_FILTER.source}`);\n\nexport const unplugin = createUnplugin(\n (options: ZodCompilerPluginOptions | undefined, meta: UnpluginContextMeta) => {\n const schemasMode = options?.schemas ?? \"auto\";\n const autoDiscover = schemasMode === \"auto\";\n const outputMode = options?.output ?? \"schema\";\n // \"compact\" keeps the Zod schema (its safeParse IS the cold error path) — so\n // it is zod-compatible like \"schema\"; only \"bag\" drops the schema object.\n const zodCompat = outputMode === \"schema\" || outputMode === \"compact\";\n const compact = outputMode === \"compact\";\n const stats = new BuildStatsAccumulator();\n // Transform results keyed by id, validated by content: bundlers re-run\n // transform for the same file (multiple environments, watch rebuilds) —\n // identical content returns the cached result, changed content recomputes.\n const cache = new Map<\n string,\n { code: string; result: string | null; map?: TransformSourceMap | null }\n >();\n const verbose = options?.verbose === true;\n const mode: CodegenMode =\n options?.codegenMode ?? (VIRTUAL_MODULE_FRAMEWORKS.has(meta.framework) ? \"lean\" : \"inline\");\n const runtimeId = WP_FRAMEWORKS.has(meta.framework) ? WP_RUNTIME_ID : VIRTUAL_RUNTIME_ID;\n // Persistent transform-result cache: discovery executes schema files (and\n // their import graphs) in-process, and the in-memory caches die with the\n // process — without a disk cache every test run / build re-pays that cost.\n // Entries self-validate against dep content hashes, so watch invalidation\n // semantics carry across processes.\n const cacheOption = options?.cache ?? true;\n // Opt-in worker pool. Null when disabled, or when the worker entry cannot\n // be located (the plugin has been bundled into a single file by a\n // consumer) — transforms then run in-process exactly as before.\n const poolSize = resolvePoolSize(options?.parallel);\n const pool = TransformPool.create(poolSize);\n /** Dedupes the in-process-fallback warning to one per build. */\n let warnedPoolFallback = false;\n if (poolSize > 0 && pool === null) {\n warn(\n \"parallel was requested but the transform worker entry could not be located \" +\n \"(is zod-compiler bundled into your build?). Falling back to in-process transforms.\",\n );\n }\n /**\n * Executed-module superset for the disk cache's deferred entries.\n *\n * With a pool, discovery happens in the workers, so this process's own\n * loader has executed nothing and `getFirstPartyModulePaths()` alone would\n * report an empty (or absent) set — silently dropping every deferred\n * entry, which on large graphs is most of them. Both sides are unioned:\n * the workers' reports, plus anything the in-process fallback executed\n * here. A worker that cannot track modules at all (Bun/Deno native import)\n * reports null, and null wins — the cache declines to persist rather than\n * record a set it knows is incomplete.\n */\n const executedModuleSuperset = (): string[] | null => {\n const local = getFirstPartyModulePaths();\n if (pool === null) return local;\n const remote = pool.firstPartyModulePaths();\n if (remote === null) return null;\n if (local === null) return remote;\n return [...new Set([...local, ...remote])];\n };\n const diskCache =\n cacheOption === false\n ? null\n : new DiskCache(\n DiskCache.resolveDir(cacheOption),\n JSON.stringify({\n mode,\n runtimeId,\n output: outputMode,\n schemas: schemasMode,\n hoist:\n typeof options?.hoist === \"object\"\n ? String(options.hoist.schemaNamePattern ?? \"default\")\n : (options?.hoist ?? true),\n }),\n executedModuleSuperset,\n );\n // Vite only (other bundlers ignore the field): when the plugin runs.\n // The default compiles production builds AND test runs — tests should\n // exercise (and benefit from) the validators that ship — while plain dev\n // servers skip AOT cost and use the Zod fallback. Vitest runs Vite in\n // serve mode but is detectable via the VITEST env var / \"test\" mode.\n const viteApply =\n options?.apply === \"all\"\n ? undefined\n : (options?.apply ??\n ((_config: unknown, env: { command: string; mode: string }) =>\n env.command === \"build\" || env.mode === \"test\" || process.env[\"VITEST\"] !== undefined));\n\n // Hook filters (unplugin object hooks): bundlers that support them\n // natively — Rolldown, Vite, Rollup 4.40+ — reject a module before ever\n // calling into JS, and unplugin applies the same patterns itself\n // everywhere else. The `code` filter is the big one: a file that never\n // mentions zod cannot produce output, so it no longer costs a hook call,\n // a content hash and an import scan per build.\n const codeFilter = transformCodeFilter(options);\n const transformFilter =\n codeFilter === undefined\n ? { id: TRANSFORM_ID_FILTER }\n : { code: codeFilter, id: TRANSFORM_ID_FILTER };\n\n return {\n name: \"zod-compiler\",\n enforce: \"pre\" as const,\n\n vite: viteApply === undefined ? {} : { apply: viteApply },\n\n esbuild: { onLoadFilter: ESBUILD_LOAD_FILTER },\n\n resolveId: {\n filter: { id: RUNTIME_SPECIFIER_FILTER },\n handler(id: string) {\n return resolveVirtualId(id);\n },\n },\n\n load: {\n filter: { id: RESOLVED_RUNTIME_ID_FILTER },\n handler(id: string) {\n return loadVirtual(id);\n },\n },\n\n transform: {\n filter: transformFilter,\n async handler(code: string, id: string) {\n // The filter covers the static half of shouldTransform(); the\n // include/exclude options — picomatch `contains` semantics, which the\n // native filters do not reproduce — are applied here, as is the whole\n // check for hosts that ignore filters.\n if (!shouldTransform(id, options)) return;\n\n const cached = cache.get(id);\n if (cached && cached.code === code) {\n return cached.result === null\n ? undefined\n : { code: cached.result, map: cached.map ?? null };\n }\n if (cached) {\n // Content changed but no watchChange fired (bundlers without the\n // hook): drop stale module executions before re-discovering.\n invalidateModuleCache();\n }\n\n // Disk cache: skip static-filtering, discovery (file execution!) and\n // codegen entirely when a previous process already transformed this\n // exact content and every dep it executed is unchanged.\n const diskKey = diskCache === null ? null : diskCache.key(id, code);\n if (diskCache !== null && diskKey !== null) {\n const entry = diskCache.load(diskKey);\n if (entry !== null) {\n cache.set(id, { code, result: entry.result, map: entry.map ?? null });\n if (entry.stats) {\n stats.add({\n files: 1,\n schemas: entry.stats.schemas,\n optimized: entry.stats.optimized,\n failed: 0,\n });\n }\n if (verbose && entry.result !== null) {\n log(`Using cached transform for ${id}`);\n }\n return entry.result === null\n ? undefined\n : { code: entry.result, map: entry.map ?? null };\n }\n }\n\n const transformOptions: TransformOptions = {\n mode,\n runtimeId,\n verbose,\n zodCompat,\n compact,\n autoDiscover,\n hoist: options?.hoist,\n };\n\n let discoveryRan = false;\n let substantialWork = false;\n let uncacheable = false;\n let fileStats: BuildStats | null = null;\n\n const inProcess = async (): Promise<TransformOutput | null> =>\n transformCodeWithMap(code, id, {\n ...transformOptions,\n onDiscovery() {\n discoveryRan = true;\n },\n onSubstantialWork() {\n substantialWork = true;\n },\n onUncacheableResult() {\n uncacheable = true;\n },\n onBuildStats(s) {\n fileStats = s;\n },\n });\n\n let output: TransformOutput | null;\n if (pool === null) {\n output = await inProcess();\n } else {\n try {\n // The worker reports through flags what the in-process path\n // reports through callbacks; everything downstream reads the\n // same four values either way.\n const r = await pool.run(code, id, poolTransformOptions(transformOptions));\n output = r.output;\n discoveryRan = r.discoveryRan;\n substantialWork = r.substantialWork;\n uncacheable = r.uncacheable;\n fileStats = r.stats;\n } catch (error) {\n // A transform's own error propagates — it will fail identically\n // in-process, and retrying would only pay discovery twice. Only\n // a pool fault (a worker that died holding this task) is worth\n // redoing on this thread, so a single unhealthy worker degrades\n // the build's speed rather than breaking it.\n if (!(error instanceof PoolUnavailableError)) throw error;\n // Once per build. A pool that is broken rather than unlucky\n // fails for every remaining file, and a warning per file would\n // bury the build's real output under thousands of copies.\n if (!warnedPoolFallback) {\n warnedPoolFallback = true;\n warn(\n `${error.message} — compiling ${id} in-process instead. ` +\n `Later files take the same path without repeating this warning.`,\n );\n }\n output = await inProcess();\n }\n }\n\n if (fileStats !== null) stats.add(fileStats);\n const result = output === null ? null : output.code;\n const map = output === null ? null : output.map;\n cache.set(id, { code, result, map });\n\n // Persist when the transform did real work: produced output, ran\n // discovery, or did parse-level work (hoist scan / static filter) —\n // even when that work concluded \"no transform needed\". Null results\n // used to be skipped on the theory that bail-outs are cheaper than a\n // cache probe; that holds for textual bail-outs (still never\n // persisted) but not for the scans: hoist-only mode re-paid a full\n // scan per zod-importing file per run (35.8s/run in a field report)\n // purely to re-derive nulls.\n if (\n diskCache !== null &&\n diskKey !== null &&\n !uncacheable &&\n (result !== null || discoveryRan || substantialWork)\n ) {\n // TS narrows fileStats to null here (assignment happens inside a\n // callback it cannot track) — widen back.\n const s = fileStats as BuildStats | null;\n const entryStats =\n s === null ? undefined : { schemas: s.schemas, optimized: s.optimized };\n if (!discoveryRan) {\n // Discovery-free results (hoist scans, static-filter rejections,\n // hoist-only rewrites) are pure functions of the file content:\n // no deps.\n diskCache.save(diskKey, result, [], entryStats, map);\n } else {\n // Per-file dependency sets: the file's static first-party import\n // graph, so editing an unrelated file no longer invalidates this\n // entry (the global superset recorded the whole project — in\n // large codebases every commit wiped the entire cache). The\n // entry file itself is always a dep: the cache key hashes the\n // content the BUNDLER passed, but discovery executed the file\n // from DISK — recording it guards the (rare) divergence between\n // the two. When the graph cannot be fully analyzed (non-literal\n // dynamic imports, unresolvable relative specifiers), the entry\n // is deferred and flushed in buildEnd against ONE end-of-build\n // executed-modules superset — immediate snapshots gave every\n // entry a distinct point-in-time copy (283 MB in the field).\n const staticDeps = collectStaticDeps(id);\n if (staticDeps.complete) {\n diskCache.save(diskKey, result, [id, ...staticDeps.deps], entryStats, map);\n } else {\n diskCache.saveDeferred(diskKey, result, entryStats, map);\n }\n }\n }\n\n if (!result) return;\n return { code: result, map };\n },\n },\n\n watchChange(id: string) {\n if (!SOURCE_LIKE.test(id)) return;\n if (id.includes(\"node_modules\")) return;\n // The changed file may be a dependency of any schema file, so both\n // the module cache (executions) and the transform result cache are\n // invalidated wholesale. node_modules stay warm in the loader. Disk\n // cache entries self-validate via dep hashes — only the per-process\n // stat memo needs resetting so changed files re-hash. Pending\n // deferred entries predate the change and must not flush against\n // post-change dep hashes.\n invalidateModuleCache();\n // Workers hold their own module caches; the broadcast is ordered ahead\n // of every transform posted after this point.\n pool?.invalidate();\n resetDepValidationMemo();\n resetDepGraphMemo();\n diskCache?.dropDeferred();\n cache.clear();\n },\n\n buildEnd() {\n // The loader's executed-modules superset is final here — persist the\n // queued incomplete-crawl entries against one shared snapshot.\n diskCache?.flushDeferred();\n if (!verbose) return;\n if (stats.schemas === 0) return;\n log(\n `Build summary: ${stats.optimized}/${stats.schemas} schemas optimized across ${stats.files} file(s)` +\n (stats.failed > 0 ? `, ${stats.failed} failed` : \"\"),\n );\n stats.reset();\n },\n };\n },\n);\n\nexport type { ZodCompilerPluginOptions } from \"./types.js\";\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {}