zod-compiler 1.23.7 → 1.24.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.
Files changed (96) hide show
  1. package/README.md +116 -12
  2. package/dist/core/codegen/build-path.d.ts +31 -1
  3. package/dist/core/codegen/build-path.d.ts.map +1 -1
  4. package/dist/core/codegen/build-path.js +44 -4
  5. package/dist/core/codegen/build-path.js.map +1 -1
  6. package/dist/core/codegen/context.d.ts +17 -1
  7. package/dist/core/codegen/context.d.ts.map +1 -1
  8. package/dist/core/codegen/context.js +18 -2
  9. package/dist/core/codegen/context.js.map +1 -1
  10. package/dist/core/codegen/emit-issue.d.ts +46 -6
  11. package/dist/core/codegen/emit-issue.d.ts.map +1 -1
  12. package/dist/core/codegen/emit-issue.js +48 -18
  13. package/dist/core/codegen/emit-issue.js.map +1 -1
  14. package/dist/core/codegen/index.js +3 -3
  15. package/dist/core/codegen/index.js.map +1 -1
  16. package/dist/core/codegen/issue-decls.d.ts +62 -2
  17. package/dist/core/codegen/issue-decls.d.ts.map +1 -1
  18. package/dist/core/codegen/issue-decls.js +64 -11
  19. package/dist/core/codegen/issue-decls.js.map +1 -1
  20. package/dist/core/codegen/schemas/array.d.ts.map +1 -1
  21. package/dist/core/codegen/schemas/array.js +2 -0
  22. package/dist/core/codegen/schemas/array.js.map +1 -1
  23. package/dist/core/codegen/schemas/bigint.js +1 -1
  24. package/dist/core/codegen/schemas/bigint.js.map +1 -1
  25. package/dist/core/codegen/schemas/discriminated-union.js +2 -2
  26. package/dist/core/codegen/schemas/discriminated-union.js.map +1 -1
  27. package/dist/core/codegen/schemas/effect.js +1 -1
  28. package/dist/core/codegen/schemas/effect.js.map +1 -1
  29. package/dist/core/codegen/schemas/file.d.ts.map +1 -1
  30. package/dist/core/codegen/schemas/file.js +2 -0
  31. package/dist/core/codegen/schemas/file.js.map +1 -1
  32. package/dist/core/codegen/schemas/map.js +2 -2
  33. package/dist/core/codegen/schemas/map.js.map +1 -1
  34. package/dist/core/codegen/schemas/number.js +6 -3
  35. package/dist/core/codegen/schemas/number.js.map +1 -1
  36. package/dist/core/codegen/schemas/record.d.ts.map +1 -1
  37. package/dist/core/codegen/schemas/record.js +23 -9
  38. package/dist/core/codegen/schemas/record.js.map +1 -1
  39. package/dist/core/codegen/schemas/set.d.ts.map +1 -1
  40. package/dist/core/codegen/schemas/set.js +2 -0
  41. package/dist/core/codegen/schemas/set.js.map +1 -1
  42. package/dist/core/codegen/schemas/sizeable.d.ts +37 -0
  43. package/dist/core/codegen/schemas/sizeable.d.ts.map +1 -0
  44. package/dist/core/codegen/schemas/sizeable.js +115 -0
  45. package/dist/core/codegen/schemas/sizeable.js.map +1 -0
  46. package/dist/core/codegen/schemas/string.d.ts.map +1 -1
  47. package/dist/core/codegen/schemas/string.js +10 -4
  48. package/dist/core/codegen/schemas/string.js.map +1 -1
  49. package/dist/core/codegen/schemas/tuple.d.ts.map +1 -1
  50. package/dist/core/codegen/schemas/tuple.js +25 -20
  51. package/dist/core/codegen/schemas/tuple.js.map +1 -1
  52. package/dist/core/codegen/schemas/union.d.ts.map +1 -1
  53. package/dist/core/codegen/schemas/union.js +5 -2
  54. package/dist/core/codegen/schemas/union.js.map +1 -1
  55. package/dist/core/extract/extractors/object.d.ts.map +1 -1
  56. package/dist/core/extract/extractors/object.js +21 -1
  57. package/dist/core/extract/extractors/object.js.map +1 -1
  58. package/dist/core/extract/extractors/optional.d.ts.map +1 -1
  59. package/dist/core/extract/extractors/optional.js +31 -1
  60. package/dist/core/extract/extractors/optional.js.map +1 -1
  61. package/dist/core/extract/extractors/tuple.d.ts.map +1 -1
  62. package/dist/core/extract/extractors/tuple.js +23 -1
  63. package/dist/core/extract/extractors/tuple.js.map +1 -1
  64. package/dist/core/extract/types.d.ts +9 -0
  65. package/dist/core/extract/types.d.ts.map +1 -1
  66. package/dist/core/iife.d.ts +17 -0
  67. package/dist/core/iife.d.ts.map +1 -1
  68. package/dist/core/iife.js +19 -2
  69. package/dist/core/iife.js.map +1 -1
  70. package/dist/core/types.d.ts +27 -0
  71. package/dist/core/types.d.ts.map +1 -1
  72. package/dist/runtime.d.ts +53 -0
  73. package/dist/runtime.js +55 -0
  74. package/dist/turbopack.d.ts +56 -0
  75. package/dist/turbopack.d.ts.map +1 -0
  76. package/dist/turbopack.js +200 -0
  77. package/dist/turbopack.js.map +1 -0
  78. package/dist/unplugin/dep-graph.d.ts +45 -2
  79. package/dist/unplugin/dep-graph.d.ts.map +1 -1
  80. package/dist/unplugin/dep-graph.js +42 -2
  81. package/dist/unplugin/dep-graph.js.map +1 -1
  82. package/dist/unplugin/edits.d.ts +13 -1
  83. package/dist/unplugin/edits.d.ts.map +1 -1
  84. package/dist/unplugin/edits.js +35 -1
  85. package/dist/unplugin/edits.js.map +1 -1
  86. package/dist/unplugin/hoist.d.ts.map +1 -1
  87. package/dist/unplugin/hoist.js +2 -26
  88. package/dist/unplugin/hoist.js.map +1 -1
  89. package/dist/unplugin/transform.d.ts.map +1 -1
  90. package/dist/unplugin/transform.js +38 -19
  91. package/dist/unplugin/transform.js.map +1 -1
  92. package/dist/unplugin/virtual.d.ts +28 -1
  93. package/dist/unplugin/virtual.d.ts.map +1 -1
  94. package/dist/unplugin/virtual.js +27 -1
  95. package/dist/unplugin/virtual.js.map +1 -1
  96. package/package.json +12 -1
@@ -0,0 +1,200 @@
1
+ import { getFirstPartyModulePaths, invalidateModuleCache } from "./loader.js";
2
+ import { RUNTIME_PACKAGE_ID } from "./unplugin/virtual.js";
3
+ import { log, shouldTransform, transformCodeWithMap } from "./unplugin/transform.js";
4
+ import { resetDepGraphMemo, transformDependencies } from "./unplugin/dep-graph.js";
5
+ import remapping from "@jridgewell/remapping";
6
+ import fs from "node:fs";
7
+ //#region src/turbopack.ts
8
+ /**
9
+ * Webpack-loader entry point, for Turbopack (Next.js) and any other host that
10
+ * runs webpack loaders but not webpack plugins.
11
+ *
12
+ * Turbopack deliberately supports no webpack plugins, so the unplugin build
13
+ * plugins cannot reach it. Loaders it does run, through the real `loader-runner`
14
+ * library, and that is enough: this module is the whole zod-compiler transform
15
+ * behind `this.async()`.
16
+ *
17
+ * It emits TypeScript, not JavaScript. A `turbopack.rules` entry that sets no
18
+ * `as`/`type` leaves the loader's output to be parsed as whatever the file
19
+ * already was, so Turbopack's own SWC pass handles the syntax — there is no
20
+ * second transpile here and no @swc/core dependency.
21
+ *
22
+ * `codegenMode: "lean"` is available here — it imports the shared helpers from
23
+ * `zod-compiler/runtime`, a real package subpath, rather than the `virtual:` id
24
+ * the build plugins answer from a resolve hook that a loader does not have. It
25
+ * is opt-in; see the option's doc for why.
26
+ *
27
+ * // next.config.ts
28
+ * export default {
29
+ * turbopack: {
30
+ * rules: {
31
+ * "*.{ts,tsx}": {
32
+ * condition: { all: [{ not: "foreign" }, { content: /[Zz]od/ }] },
33
+ * loaders: ["zod-compiler/turbopack"],
34
+ * },
35
+ * },
36
+ * },
37
+ * };
38
+ *
39
+ * The `content` condition is the Turbopack equivalent of the plugins' own code
40
+ * filter and must stay as loose as ZOD_MENTION: narrowing it to the literal
41
+ * specifier `"zod"` would silently skip `zod/v4`, `zod/mini` and the
42
+ * `zod-compiler` import that drives `schemas: "explicit"` — no error, those
43
+ * schemas just quietly stay uncompiled.
44
+ */
45
+ /**
46
+ * Disk stamps of every module the shared execution cache is currently holding.
47
+ *
48
+ * Discovery executes schema files from DISK through a module cache that outlives
49
+ * loader calls. The bundler plugins evict it from `watchChange`; a loader has no
50
+ * such hook, so staleness has to be detected here or compiled validators keep
51
+ * reflecting whatever the files said when they were first executed.
52
+ *
53
+ * Diffing the entry's own content is NOT enough, and the gap is exactly the case
54
+ * `addDependency` exists to handle: when an imported constant changes, the host
55
+ * re-runs the loader for a schema file whose content is UNCHANGED.
56
+ *
57
+ * Tracked GLOBALLY rather than per file, because that is what it describes: one
58
+ * process-wide module cache, stale the moment any file behind it changes. A
59
+ * per-file dependency list cannot express that — a file being transformed for
60
+ * the FIRST time has no list yet, but the cache it is about to read from is
61
+ * already warm and may already be stale. Keying on the cache's own inventory
62
+ * also means one edit costs one eviction rather than one per dependent file.
63
+ *
64
+ * `getFirstPartyModulePaths()` is that inventory, which is why it is sound here
65
+ * while being unsound as a per-file dependency list (see transformDependencies):
66
+ * the question is "what is cached", not "what does this file need". It covers
67
+ * dependencies the host never feeds through this loader at all — a constants
68
+ * file that never mentions zod, or one a rule's `exclude` skips — but only the
69
+ * ones jiti holds, so the two known gaps are its gaps:
70
+ *
71
+ * - `.js`/`.mjs` deps go through native `import()` (see loader.ts), which has no
72
+ * evictable cache. The build plugins are equally stale there; on Bun and Deno
73
+ * that is every module, and this whole mechanism is inert.
74
+ * - A file being CREATED changes nothing's stamp, so a new module that shadows
75
+ * an existing resolution (`limits.ts` beside `limits/index.ts`) keeps serving
76
+ * the old one until some stamped file also changes. `watchChange` gives the
77
+ * plugins a signal for this that a loader host has no equivalent of.
78
+ *
79
+ * mtime+size like the disk cache's fast path, minus its content hashes: those
80
+ * exist to survive checkouts across processes, and this map dies with the
81
+ * process.
82
+ */
83
+ const executedModuleStamps = /* @__PURE__ */ new Map();
84
+ function stampOf(file) {
85
+ try {
86
+ const stat = fs.statSync(file, { throwIfNoEntry: false });
87
+ return stat === void 0 ? "" : `${stat.mtimeMs}:${stat.size}`;
88
+ } catch {
89
+ return "";
90
+ }
91
+ }
92
+ /**
93
+ * Drop cached module executions when any file behind them changed on disk.
94
+ *
95
+ * Eviction is global (see invalidateModuleCache), so it is deliberately paired
96
+ * with the dep-graph memos the plugin also resets in `watchChange`: a resolution
97
+ * memoized before a file existed would otherwise pin that importer to an
98
+ * unanalyzable graph for the life of the process.
99
+ *
100
+ * Costs one stat per executed module, paid by every file that reaches the
101
+ * transform — ~3 ms per file against the 1,900-module project dep-graph.ts
102
+ * cites, the same order as the closure walk `transformDependencies` does a few
103
+ * lines later, and far below the discovery it is protecting.
104
+ */
105
+ function invalidateStaleExecutions() {
106
+ let stale = false;
107
+ for (const [file, stamp] of executedModuleStamps) if (stampOf(file) !== stamp) {
108
+ stale = true;
109
+ break;
110
+ }
111
+ if (!stale) return;
112
+ invalidateModuleCache();
113
+ resetDepGraphMemo();
114
+ executedModuleStamps.clear();
115
+ }
116
+ /**
117
+ * Stamp whatever discovery just executed.
118
+ *
119
+ * Keeps the FIRST stamp for a file rather than refreshing: a file edited between
120
+ * its execution and this call must keep its pre-edit stamp, or the next run
121
+ * would see it as fresh and pin the stale execution permanently.
122
+ *
123
+ * `null` means no jiti instance exists — a runtime whose module cache cannot be
124
+ * evicted at all (loader.ts), so there is nothing to track and nothing to fix.
125
+ */
126
+ function recordExecutedModules() {
127
+ for (const file of getFirstPartyModulePaths() ?? []) if (!executedModuleStamps.has(file)) executedModuleStamps.set(file, stampOf(file));
128
+ }
129
+ function readOptions(context) {
130
+ const options = context.getOptions?.();
131
+ if (options !== void 0) return options;
132
+ return typeof context.query === "object" && context.query !== null ? context.query : {};
133
+ }
134
+ /**
135
+ * Declare what a rebuild must watch. The host caches this file's output keyed on
136
+ * its own content, but discovery executed the whole import graph — without these
137
+ * an edit to an imported constant leaves a stale validator in the bundle.
138
+ */
139
+ function declareDependencies(context, id, verbose) {
140
+ const { files, complete } = transformDependencies(id);
141
+ for (const file of files) context.addDependency?.(file);
142
+ if (complete) return;
143
+ context.cacheable?.(false);
144
+ if (verbose) log(`Cannot analyze the import graph of ${id} — re-running its transform every build`);
145
+ }
146
+ /** The transform itself, split out so the loader shell stays callback-only. */
147
+ async function run(context, source, inputMap) {
148
+ const id = context.resourcePath;
149
+ const options = readOptions(context);
150
+ if (!shouldTransform(id, options)) return {
151
+ code: source,
152
+ map: inputMap
153
+ };
154
+ invalidateStaleExecutions();
155
+ const output = options.output ?? "schema";
156
+ let discoveryRan = false;
157
+ const result = await transformCodeWithMap(source, id, {
158
+ mode: options.codegenMode ?? "inline",
159
+ runtimeId: RUNTIME_PACKAGE_ID,
160
+ verbose: options.verbose,
161
+ zodCompat: output === "schema" || output === "compact",
162
+ compact: output === "compact",
163
+ autoDiscover: (options.schemas ?? "auto") === "auto",
164
+ hoist: options.hoist,
165
+ onDiscovery: () => {
166
+ discoveryRan = true;
167
+ },
168
+ onUncacheableResult: () => {
169
+ context.cacheable?.(false);
170
+ }
171
+ });
172
+ if (discoveryRan) {
173
+ recordExecutedModules();
174
+ declareDependencies(context, id, options.verbose === true);
175
+ }
176
+ if (result === null) return {
177
+ code: source,
178
+ map: inputMap
179
+ };
180
+ return {
181
+ code: result.code,
182
+ map: composeMaps(result.map, inputMap)
183
+ };
184
+ }
185
+ /** Chain this transform's map onto an earlier loader's, newest first. */
186
+ function composeMaps(map, inputMap) {
187
+ if (map === null) return inputMap;
188
+ if (inputMap === void 0) return map;
189
+ return remapping([map, inputMap], () => null);
190
+ }
191
+ function zodCompilerLoader(source, inputMap) {
192
+ const callback = this.async();
193
+ const succeed = (result) => callback(null, result.code, result.map);
194
+ const fail = (error) => callback(error instanceof Error ? error : new Error(String(error)));
195
+ run(this, source, inputMap).then(succeed, fail);
196
+ }
197
+ //#endregion
198
+ export { zodCompilerLoader as default };
199
+
200
+ //# sourceMappingURL=turbopack.js.map
@@ -0,0 +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,6 +1,7 @@
1
1
  //#region src/unplugin/dep-graph.d.ts
2
2
  /**
3
- * Static per-file dependency graphs for disk-cache invalidation.
3
+ * Static per-file dependency graphs, for disk-cache invalidation and for the
4
+ * watch dependencies a loader host has to declare (see transformDependencies).
4
5
  *
5
6
  * The disk cache used to record `getFirstPartyModulePaths()` — the superset
6
7
  * of EVERY first-party module the loader had executed so far — as each
@@ -50,6 +51,48 @@ declare function resetDepGraphMemo(): void;
50
51
  * The entry itself is excluded (its content is part of the cache key).
51
52
  */
52
53
  declare function collectStaticDeps(entryFile: string): StaticDeps;
54
+ interface TransformDependencies {
55
+ /**
56
+ * Files to declare as watch dependencies, entry first and deduped. Complete
57
+ * only when `complete` is true — otherwise it is just the entry.
58
+ */
59
+ files: string[];
60
+ /**
61
+ * False when the import graph could not be fully analyzed. `files` is then
62
+ * NOT sufficient, and the host has to reach for a coarser signal (a context
63
+ * dependency over the project, or refusing to cache the result) instead of
64
+ * trusting it.
65
+ */
66
+ complete: boolean;
67
+ }
68
+ /**
69
+ * Files whose contents can change what `id`'s transform produces — the set a
70
+ * host must declare as watch dependencies.
71
+ *
72
+ * Discovery EXECUTES the file's import graph, so a compiled validator reflects
73
+ * every constant, enum and helper that graph contributed. A host that keys its
74
+ * cache on `id`'s own content alone therefore serves a stale validator after an
75
+ * edit to any of them.
76
+ *
77
+ * Bundler plugins do not need this: their `watchChange` hook fires for every
78
+ * file in the project, so the plugin invalidates wholesale. Loader hosts
79
+ * (Turbopack, plain webpack loaders) have no such hook — their only lever is
80
+ * declaring dependencies per file, which is what this returns.
81
+ *
82
+ * The entry is always included, for the reason the disk cache records it too: a
83
+ * host keys on the content it PASSED, but discovery executed the file from disk.
84
+ *
85
+ * Deliberately NOT backed by `getFirstPartyModulePaths()` when the crawl comes
86
+ * up incomplete, unlike the disk cache's fallback. That superset is
87
+ * point-in-time, not cumulative — `invalidateModuleCache()` empties it — so
88
+ * after an unrelated file's discovery repopulates it, it describes THAT file's
89
+ * graph and can silently omit this one's. The disk cache survives that because
90
+ * `watchChange` invalidates wholesale and deferred entries flush against one
91
+ * end-of-build snapshot; a loader host has neither backstop, so a plausible but
92
+ * wrong list would go undetected. Reporting `complete: false` lets the caller
93
+ * do the sound thing instead.
94
+ */
95
+ declare function transformDependencies(id: string): TransformDependencies;
53
96
  //#endregion
54
- export { StaticDeps, collectStaticDeps, resetDepGraphMemo };
97
+ export { StaticDeps, TransformDependencies, collectStaticDeps, resetDepGraphMemo, transformDependencies };
55
98
  //# sourceMappingURL=dep-graph.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dep-graph.d.ts","names":[],"sources":["../../src/unplugin/dep-graph.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA4CiB;;EAEf;;EAEA;;;iBA0Cc;;;;;iBAsMA,kBAAkB,oBAAoB"}
1
+ {"version":3,"file":"dep-graph.d.ts","names":[],"sources":["../../src/unplugin/dep-graph.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA6CiB;;EAEf;;EAEA;;;iBA0Cc;;;;;iBAsMA,kBAAkB,oBAAoB;UA0BrC;;;;;EAKf;;;;;;;EAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8Bc,sBAAsB,aAAa"}
@@ -4,7 +4,8 @@ import * as path$1 from "node:path";
4
4
  import * as fs$1 from "node:fs";
5
5
  //#region src/unplugin/dep-graph.ts
6
6
  /**
7
- * Static per-file dependency graphs for disk-cache invalidation.
7
+ * Static per-file dependency graphs, for disk-cache invalidation and for the
8
+ * watch dependencies a loader host has to declare (see transformDependencies).
8
9
  *
9
10
  * The disk cache used to record `getFirstPartyModulePaths()` — the superset
10
11
  * of EVERY first-party module the loader had executed so far — as each
@@ -247,7 +248,46 @@ function collectStaticDeps(entryFile) {
247
248
  complete: true
248
249
  };
249
250
  }
251
+ /**
252
+ * Files whose contents can change what `id`'s transform produces — the set a
253
+ * host must declare as watch dependencies.
254
+ *
255
+ * Discovery EXECUTES the file's import graph, so a compiled validator reflects
256
+ * every constant, enum and helper that graph contributed. A host that keys its
257
+ * cache on `id`'s own content alone therefore serves a stale validator after an
258
+ * edit to any of them.
259
+ *
260
+ * Bundler plugins do not need this: their `watchChange` hook fires for every
261
+ * file in the project, so the plugin invalidates wholesale. Loader hosts
262
+ * (Turbopack, plain webpack loaders) have no such hook — their only lever is
263
+ * declaring dependencies per file, which is what this returns.
264
+ *
265
+ * The entry is always included, for the reason the disk cache records it too: a
266
+ * host keys on the content it PASSED, but discovery executed the file from disk.
267
+ *
268
+ * Deliberately NOT backed by `getFirstPartyModulePaths()` when the crawl comes
269
+ * up incomplete, unlike the disk cache's fallback. That superset is
270
+ * point-in-time, not cumulative — `invalidateModuleCache()` empties it — so
271
+ * after an unrelated file's discovery repopulates it, it describes THAT file's
272
+ * graph and can silently omit this one's. The disk cache survives that because
273
+ * `watchChange` invalidates wholesale and deferred entries flush against one
274
+ * end-of-build snapshot; a loader host has neither backstop, so a plausible but
275
+ * wrong list would go undetected. Reporting `complete: false` lets the caller
276
+ * do the sound thing instead.
277
+ */
278
+ function transformDependencies(id) {
279
+ const entry = realpathOf(path$1.resolve(id));
280
+ const staticDeps = collectStaticDeps(entry);
281
+ if (!staticDeps.complete) return {
282
+ files: [entry],
283
+ complete: false
284
+ };
285
+ return {
286
+ files: [entry, ...staticDeps.deps.filter((d) => d !== entry)],
287
+ complete: true
288
+ };
289
+ }
250
290
  //#endregion
251
- export { collectStaticDeps, resetDepGraphMemo };
291
+ export { collectStaticDeps, resetDepGraphMemo, transformDependencies };
252
292
 
253
293
  //# sourceMappingURL=dep-graph.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"dep-graph.js","names":["path","fs"],"sources":["../../src/unplugin/dep-graph.ts"],"sourcesContent":["/**\n * Static per-file dependency graphs for disk-cache invalidation.\n *\n * The disk cache used to record `getFirstPartyModulePaths()` — the superset\n * of EVERY first-party module the loader had executed so far — as each\n * entry's dependency set. Supersets never serve stale results, but in a\n * large codebase every entry ends up recording the whole project (~1,900\n * deps, ~400KB JSON per entry in the field report), so ANY commit\n * invalidates essentially the entire cache and CI re-pays full discovery\n * every run.\n *\n * This module computes the file's ACTUAL import graph statically: scan\n * import/export/require specifiers, resolve them (relative paths with\n * extension probing, tsconfig path aliases via the loader's config, bare\n * specifiers through node resolution), and BFS across first-party files.\n * No file is executed. Type-only imports are included (over-approximation\n * only over-invalidates).\n *\n * Soundness rule: when anything cannot be analyzed — a non-literal dynamic\n * import, an unresolvable relative specifier — the crawl reports\n * `complete: false` and the caller falls back to the superset. A static\n * crawl may only replace the superset when it provably covers everything\n * the file can load.\n *\n * Cost discipline (this runs serialized on the bundler's main process, once\n * per discovery file, over closures that reach thousands of files):\n * - every existence probe uses `statSync(p, { throwIfNoEntry: false })` —\n * a thrown ENOENT pays V8 error-plus-stack construction, and a cold crawl\n * issues these probes by the million (the dominant cost in field samples);\n * - resolution is memoized at every level: realpath per path, createRequire\n * per directory, resolved specifier per (directory, specifier), and the\n * full edge list per file (mtime-validated) — repeat crawls over the\n * shared core graph are Map lookups plus one cheap stat per file;\n * - the BFS aborts the moment the graph becomes unanalyzable: the caller\n * discards an incomplete result for the superset anyway, so finishing the\n * crawl would be pure waste (and incomplete is the COMMON outcome on\n * large graphs — one dynamic import anywhere poisons the whole closure).\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as path from \"node:path\";\nimport { resolveTsconfigPathCandidates } from \"../loader.js\";\n\nexport interface StaticDeps {\n /** Absolute real paths of first-party files reachable from the entry. */\n deps: string[];\n /** False when the graph could not be fully analyzed — use the superset. */\n complete: boolean;\n}\n\nconst SOURCE_EXTENSIONS = [\".ts\", \".tsx\", \".mts\", \".cts\", \".js\", \".jsx\", \".mjs\", \".cjs\"];\nconst NODE_MODULES_SEGMENT = `${path.sep}node_modules${path.sep}`;\n/** Specifier schemes that never resolve to project files. */\n// oxlint-disable-next-line no-control-regex -- rollup-convention virtual module ids begin with NUL\nconst SKIP_SPECIFIER = /^(?:node:|data:|virtual:|\\u0000)/;\n/** Runaway guard — graphs larger than this fall back to the superset. */\nconst MAX_FILES = 10_000;\n\nconst INCOMPLETE: StaticDeps = { deps: [], complete: false };\n\n/**\n * Resolution-failure sentinel. A unique symbol rather than a string literal:\n * `\"incomplete\" | string` collapses to `string`, so the type system could\n * not distinguish a resolved path from the failure marker.\n */\nconst UNRESOLVED = Symbol(\"unresolved\");\n\ninterface FileNode {\n mtimeMs: number;\n /** Resolved first-party imports (absolute real paths), deduped. */\n edges: string[];\n /** Non-literal dynamic import/require or unresolvable specifier. */\n incomplete: boolean;\n}\n\n/**\n * Per-process memos. The file node memo is revalidated by mtime on every\n * visit; the resolution memos (specifier, realpath, require) depend on the\n * filesystem layout rather than file contents and are only dropped by\n * resetDepGraphMemo() — the same wholesale invalidation watch mode already\n * applies to the module cache (a memoized resolution can only go stale when\n * files appear/move, which fires watchChange).\n */\nconst nodeMemo = new Map<string, FileNode | null>();\nconst resolveMemo = new Map<string, string | null | typeof UNRESOLVED>();\nconst realpathMemo = new Map<string, string>();\nconst requireMemo = new Map<string, ReturnType<typeof createRequire>>();\n\n/** Reset the per-process dep-graph memos (watch-mode file changes). */\nexport function resetDepGraphMemo(): void {\n nodeMemo.clear();\n resolveMemo.clear();\n realpathMemo.clear();\n requireMemo.clear();\n}\n\nconst STATIC_SPECIFIER =\n /(?:\\bfrom\\s*|\\bimport\\s+|\\bimport\\s*\\(\\s*|\\brequire\\s*\\(\\s*)[\"']([^\"'\\n]+)[\"']/g;\n// No /g flag: `.test()` on a global regex resumes from lastIndex of the\n// PREVIOUS file's match, silently missing dynamic imports (a missed\n// detection records a falsely-complete dep set — stale cache).\nconst DYNAMIC_CALL = /\\b(?:import|require)\\s*\\(\\s*(?![\"'])[^)\\s]/;\n\n/** statSync that reports ENOENT as undefined instead of an exception. */\nfunction tryStat(p: string): fs.Stats | undefined {\n try {\n return fs.statSync(p, { throwIfNoEntry: false });\n } catch {\n // Non-ENOENT errors (ENOTDIR from probing `file.ts/index.ts`, EACCES):\n // treat as absent.\n return undefined;\n }\n}\n\nfunction tryFile(p: string): string | null {\n return tryStat(p)?.isFile() ? p : null;\n}\n\n/** Probe a path (possibly extensionless) to an existing file. */\nfunction probeFile(base: string): string | null {\n const hasExtension = path.extname(base) !== \"\";\n if (hasExtension) {\n if (tryFile(base)) return base;\n // Extensioned path that doesn't exist as-is: TS source referenced with a\n // .js specifier (nodenext style). Probe the extension-family swap before\n // the append/index probes — in nodenext codebases this is the common hit\n // and skips ~16 dead probes per import.\n if (/\\.[cm]?js$/.test(base)) {\n const hit =\n tryFile(base.replace(/\\.([cm]?)js$/, \".$1ts\")) ??\n tryFile(base.replace(/\\.[cm]?js$/, \".tsx\"));\n if (hit) return hit;\n }\n }\n for (const ext of SOURCE_EXTENSIONS) {\n const hit = tryFile(base + ext);\n if (hit) return hit;\n }\n for (const ext of SOURCE_EXTENSIONS) {\n const hit = tryFile(path.join(base, `index${ext}`));\n if (hit) return hit;\n }\n // Extensionless path that exists exactly (e.g. ./bin/cli).\n return hasExtension ? null : tryFile(base);\n}\n\nfunction realpathOf(p: string): string {\n let real = realpathMemo.get(p);\n if (real === undefined) {\n try {\n real = fs.realpathSync(p);\n } catch {\n real = p; // keep the unresolved path\n }\n realpathMemo.set(p, real);\n }\n return real;\n}\n\nfunction firstPartyOrNull(resolved: string): string | null {\n const real = realpathOf(resolved);\n return real.includes(NODE_MODULES_SEGMENT) ? null : real;\n}\n\n/** Node resolution is a function of the importing DIRECTORY, not the file. */\nfunction requireFor(dir: string): ReturnType<typeof createRequire> {\n let req = requireMemo.get(dir);\n if (req === undefined) {\n req = createRequire(path.join(dir, \"__zod_compiler_resolve__.js\"));\n requireMemo.set(dir, req);\n }\n return req;\n}\n\n/**\n * Resolve one specifier from a directory. Returns:\n * - an absolute path (first-party file to record + crawl)\n * - null when the dep is intentionally out of scope (node_modules, builtins)\n * - UNRESOLVED when resolution failed and the graph cannot be trusted\n */\nfunction resolveSpecifier(rawSpec: string, fromDir: string): string | null | typeof UNRESOLVED {\n if (SKIP_SPECIFIER.test(rawSpec)) return null;\n const memoKey = `${fromDir}\\0${rawSpec}`;\n const memoized = resolveMemo.get(memoKey);\n if (memoized !== undefined) return memoized;\n const result = resolveSpecifierUncached(rawSpec, fromDir);\n resolveMemo.set(memoKey, result);\n return result;\n}\n\nfunction resolveSpecifierUncached(\n rawSpec: string,\n fromDir: string,\n): string | null | typeof UNRESOLVED {\n // Vite-style resource queries (./logo.svg?url, ./shader.glsl?raw) resolve\n // to the underlying file.\n const q = rawSpec.indexOf(\"?\");\n const spec = q === -1 ? rawSpec : rawSpec.slice(0, q);\n if (spec === \"\") return null;\n\n if (spec.startsWith(\"./\") || spec.startsWith(\"../\")) {\n const hit = probeFile(path.resolve(fromDir, spec));\n return hit === null ? UNRESOLVED : firstPartyOrNull(hit);\n }\n\n const { candidates, authoritative } = resolveTsconfigPathCandidates(fromDir, spec);\n for (const candidate of candidates) {\n const hit = probeFile(candidate);\n if (hit !== null) {\n return firstPartyOrNull(hit);\n }\n }\n // An authoritative miss (explicit `paths` pattern matched, or the paths\n // config is present but uninterpretable) means the import cannot be\n // trusted. Implicit baseUrl candidates are proposed for EVERY bare\n // specifier (npm packages, `#` imports) and only mean \"try baseDir\n // first\" — a miss falls through to node resolution below.\n if (authoritative) return UNRESOLVED;\n\n // Bare specifier (package) or package.json \"imports\" (#...): node\n // resolution from the importing directory. node_modules results are out of\n // scope (zod's version is part of the cache key); symlinked workspace\n // packages realpath outside node_modules and are crawled as first-party.\n try {\n const resolved = requireFor(fromDir).resolve(spec);\n if (!path.isAbsolute(resolved)) return null; // node builtin\n return firstPartyOrNull(resolved);\n } catch {\n // Unresolvable from Node's algorithm. jiti may still resolve it (e.g.\n // ESM-only packages without CJS entries) — the file itself stays a dep\n // through the entry, but its subtree is unknown for bare packages.\n // Treat unresolvable BARE specifiers as out-of-scope third-party\n // (caught by the zod-version/plugin key when relevant) only when they\n // cannot be project files; '#' subpath imports stay strict.\n return spec.startsWith(\"#\") ? UNRESOLVED : null;\n }\n}\n\n/**\n * Scan + resolve one file into its edge list. Memoized by mtime, so the\n * shared core graph is read and resolved once per process — each entry's\n * closure walk after that is one cheap stat plus Map lookups per file.\n * Returns null for unreadable files.\n */\nfunction fileNode(filePath: string): FileNode | null {\n const stat = tryStat(filePath);\n if (stat === undefined || !stat.isFile()) {\n nodeMemo.set(filePath, null);\n return null;\n }\n const memo = nodeMemo.get(filePath);\n if (memo !== undefined && memo !== null && memo.mtimeMs === stat.mtimeMs) return memo;\n\n let source: string;\n try {\n source = fs.readFileSync(filePath, \"utf8\");\n } catch {\n nodeMemo.set(filePath, null);\n return null;\n }\n // String/comment contents can false-positive both patterns; extra\n // specifiers only add deps (safe), a false `incomplete` only falls back\n // to the superset (safe).\n let incomplete = DYNAMIC_CALL.test(source);\n const edges: string[] = [];\n const seen = new Set<string>();\n const dir = path.dirname(filePath);\n for (const m of source.matchAll(STATIC_SPECIFIER)) {\n const spec = m[1];\n if (!spec) continue;\n const resolved = resolveSpecifier(spec, dir);\n if (resolved === UNRESOLVED) {\n incomplete = true;\n } else if (resolved !== null && !seen.has(resolved)) {\n seen.add(resolved);\n edges.push(resolved);\n }\n }\n const node: FileNode = { mtimeMs: stat.mtimeMs, edges, incomplete };\n nodeMemo.set(filePath, node);\n return node;\n}\n\n/**\n * Compute the static first-party dependency closure of `entryFile`.\n * The entry itself is excluded (its content is part of the cache key).\n */\nexport function collectStaticDeps(entryFile: string): StaticDeps {\n const entry = firstPartyOrNull(path.resolve(entryFile));\n if (entry === null) return INCOMPLETE;\n\n const visited = new Set<string>([entry]);\n const queue: string[] = [entry];\n\n while (queue.length > 0) {\n if (visited.size > MAX_FILES) return INCOMPLETE;\n const file = queue.pop() as string;\n const node = fileNode(file);\n // Any unanalyzable node poisons the whole closure; the caller falls back\n // to the superset, so abort instead of crawling to exhaustion.\n if (node === null || node.incomplete) return INCOMPLETE;\n for (const dep of node.edges) {\n if (!visited.has(dep)) {\n visited.add(dep);\n queue.push(dep);\n }\n }\n }\n\n visited.delete(entry);\n return { deps: [...visited], complete: true };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"dep-graph.js","names":["path","fs"],"sources":["../../src/unplugin/dep-graph.ts"],"sourcesContent":["/**\n * Static per-file dependency graphs, for disk-cache invalidation and for the\n * watch dependencies a loader host has to declare (see transformDependencies).\n *\n * The disk cache used to record `getFirstPartyModulePaths()` — the superset\n * of EVERY first-party module the loader had executed so far — as each\n * entry's dependency set. Supersets never serve stale results, but in a\n * large codebase every entry ends up recording the whole project (~1,900\n * deps, ~400KB JSON per entry in the field report), so ANY commit\n * invalidates essentially the entire cache and CI re-pays full discovery\n * every run.\n *\n * This module computes the file's ACTUAL import graph statically: scan\n * import/export/require specifiers, resolve them (relative paths with\n * extension probing, tsconfig path aliases via the loader's config, bare\n * specifiers through node resolution), and BFS across first-party files.\n * No file is executed. Type-only imports are included (over-approximation\n * only over-invalidates).\n *\n * Soundness rule: when anything cannot be analyzed — a non-literal dynamic\n * import, an unresolvable relative specifier — the crawl reports\n * `complete: false` and the caller falls back to the superset. A static\n * crawl may only replace the superset when it provably covers everything\n * the file can load.\n *\n * Cost discipline (this runs serialized on the bundler's main process, once\n * per discovery file, over closures that reach thousands of files):\n * - every existence probe uses `statSync(p, { throwIfNoEntry: false })` —\n * a thrown ENOENT pays V8 error-plus-stack construction, and a cold crawl\n * issues these probes by the million (the dominant cost in field samples);\n * - resolution is memoized at every level: realpath per path, createRequire\n * per directory, resolved specifier per (directory, specifier), and the\n * full edge list per file (mtime-validated) — repeat crawls over the\n * shared core graph are Map lookups plus one cheap stat per file;\n * - the BFS aborts the moment the graph becomes unanalyzable: the caller\n * discards an incomplete result for the superset anyway, so finishing the\n * crawl would be pure waste (and incomplete is the COMMON outcome on\n * large graphs — one dynamic import anywhere poisons the whole closure).\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as path from \"node:path\";\nimport { resolveTsconfigPathCandidates } from \"../loader.js\";\n\nexport interface StaticDeps {\n /** Absolute real paths of first-party files reachable from the entry. */\n deps: string[];\n /** False when the graph could not be fully analyzed — use the superset. */\n complete: boolean;\n}\n\nconst SOURCE_EXTENSIONS = [\".ts\", \".tsx\", \".mts\", \".cts\", \".js\", \".jsx\", \".mjs\", \".cjs\"];\nconst NODE_MODULES_SEGMENT = `${path.sep}node_modules${path.sep}`;\n/** Specifier schemes that never resolve to project files. */\n// oxlint-disable-next-line no-control-regex -- rollup-convention virtual module ids begin with NUL\nconst SKIP_SPECIFIER = /^(?:node:|data:|virtual:|\\u0000)/;\n/** Runaway guard — graphs larger than this fall back to the superset. */\nconst MAX_FILES = 10_000;\n\nconst INCOMPLETE: StaticDeps = { deps: [], complete: false };\n\n/**\n * Resolution-failure sentinel. A unique symbol rather than a string literal:\n * `\"incomplete\" | string` collapses to `string`, so the type system could\n * not distinguish a resolved path from the failure marker.\n */\nconst UNRESOLVED = Symbol(\"unresolved\");\n\ninterface FileNode {\n mtimeMs: number;\n /** Resolved first-party imports (absolute real paths), deduped. */\n edges: string[];\n /** Non-literal dynamic import/require or unresolvable specifier. */\n incomplete: boolean;\n}\n\n/**\n * Per-process memos. The file node memo is revalidated by mtime on every\n * visit; the resolution memos (specifier, realpath, require) depend on the\n * filesystem layout rather than file contents and are only dropped by\n * resetDepGraphMemo() — the same wholesale invalidation watch mode already\n * applies to the module cache (a memoized resolution can only go stale when\n * files appear/move, which fires watchChange).\n */\nconst nodeMemo = new Map<string, FileNode | null>();\nconst resolveMemo = new Map<string, string | null | typeof UNRESOLVED>();\nconst realpathMemo = new Map<string, string>();\nconst requireMemo = new Map<string, ReturnType<typeof createRequire>>();\n\n/** Reset the per-process dep-graph memos (watch-mode file changes). */\nexport function resetDepGraphMemo(): void {\n nodeMemo.clear();\n resolveMemo.clear();\n realpathMemo.clear();\n requireMemo.clear();\n}\n\nconst STATIC_SPECIFIER =\n /(?:\\bfrom\\s*|\\bimport\\s+|\\bimport\\s*\\(\\s*|\\brequire\\s*\\(\\s*)[\"']([^\"'\\n]+)[\"']/g;\n// No /g flag: `.test()` on a global regex resumes from lastIndex of the\n// PREVIOUS file's match, silently missing dynamic imports (a missed\n// detection records a falsely-complete dep set — stale cache).\nconst DYNAMIC_CALL = /\\b(?:import|require)\\s*\\(\\s*(?![\"'])[^)\\s]/;\n\n/** statSync that reports ENOENT as undefined instead of an exception. */\nfunction tryStat(p: string): fs.Stats | undefined {\n try {\n return fs.statSync(p, { throwIfNoEntry: false });\n } catch {\n // Non-ENOENT errors (ENOTDIR from probing `file.ts/index.ts`, EACCES):\n // treat as absent.\n return undefined;\n }\n}\n\nfunction tryFile(p: string): string | null {\n return tryStat(p)?.isFile() ? p : null;\n}\n\n/** Probe a path (possibly extensionless) to an existing file. */\nfunction probeFile(base: string): string | null {\n const hasExtension = path.extname(base) !== \"\";\n if (hasExtension) {\n if (tryFile(base)) return base;\n // Extensioned path that doesn't exist as-is: TS source referenced with a\n // .js specifier (nodenext style). Probe the extension-family swap before\n // the append/index probes — in nodenext codebases this is the common hit\n // and skips ~16 dead probes per import.\n if (/\\.[cm]?js$/.test(base)) {\n const hit =\n tryFile(base.replace(/\\.([cm]?)js$/, \".$1ts\")) ??\n tryFile(base.replace(/\\.[cm]?js$/, \".tsx\"));\n if (hit) return hit;\n }\n }\n for (const ext of SOURCE_EXTENSIONS) {\n const hit = tryFile(base + ext);\n if (hit) return hit;\n }\n for (const ext of SOURCE_EXTENSIONS) {\n const hit = tryFile(path.join(base, `index${ext}`));\n if (hit) return hit;\n }\n // Extensionless path that exists exactly (e.g. ./bin/cli).\n return hasExtension ? null : tryFile(base);\n}\n\nfunction realpathOf(p: string): string {\n let real = realpathMemo.get(p);\n if (real === undefined) {\n try {\n real = fs.realpathSync(p);\n } catch {\n real = p; // keep the unresolved path\n }\n realpathMemo.set(p, real);\n }\n return real;\n}\n\nfunction firstPartyOrNull(resolved: string): string | null {\n const real = realpathOf(resolved);\n return real.includes(NODE_MODULES_SEGMENT) ? null : real;\n}\n\n/** Node resolution is a function of the importing DIRECTORY, not the file. */\nfunction requireFor(dir: string): ReturnType<typeof createRequire> {\n let req = requireMemo.get(dir);\n if (req === undefined) {\n req = createRequire(path.join(dir, \"__zod_compiler_resolve__.js\"));\n requireMemo.set(dir, req);\n }\n return req;\n}\n\n/**\n * Resolve one specifier from a directory. Returns:\n * - an absolute path (first-party file to record + crawl)\n * - null when the dep is intentionally out of scope (node_modules, builtins)\n * - UNRESOLVED when resolution failed and the graph cannot be trusted\n */\nfunction resolveSpecifier(rawSpec: string, fromDir: string): string | null | typeof UNRESOLVED {\n if (SKIP_SPECIFIER.test(rawSpec)) return null;\n const memoKey = `${fromDir}\\0${rawSpec}`;\n const memoized = resolveMemo.get(memoKey);\n if (memoized !== undefined) return memoized;\n const result = resolveSpecifierUncached(rawSpec, fromDir);\n resolveMemo.set(memoKey, result);\n return result;\n}\n\nfunction resolveSpecifierUncached(\n rawSpec: string,\n fromDir: string,\n): string | null | typeof UNRESOLVED {\n // Vite-style resource queries (./logo.svg?url, ./shader.glsl?raw) resolve\n // to the underlying file.\n const q = rawSpec.indexOf(\"?\");\n const spec = q === -1 ? rawSpec : rawSpec.slice(0, q);\n if (spec === \"\") return null;\n\n if (spec.startsWith(\"./\") || spec.startsWith(\"../\")) {\n const hit = probeFile(path.resolve(fromDir, spec));\n return hit === null ? UNRESOLVED : firstPartyOrNull(hit);\n }\n\n const { candidates, authoritative } = resolveTsconfigPathCandidates(fromDir, spec);\n for (const candidate of candidates) {\n const hit = probeFile(candidate);\n if (hit !== null) {\n return firstPartyOrNull(hit);\n }\n }\n // An authoritative miss (explicit `paths` pattern matched, or the paths\n // config is present but uninterpretable) means the import cannot be\n // trusted. Implicit baseUrl candidates are proposed for EVERY bare\n // specifier (npm packages, `#` imports) and only mean \"try baseDir\n // first\" — a miss falls through to node resolution below.\n if (authoritative) return UNRESOLVED;\n\n // Bare specifier (package) or package.json \"imports\" (#...): node\n // resolution from the importing directory. node_modules results are out of\n // scope (zod's version is part of the cache key); symlinked workspace\n // packages realpath outside node_modules and are crawled as first-party.\n try {\n const resolved = requireFor(fromDir).resolve(spec);\n if (!path.isAbsolute(resolved)) return null; // node builtin\n return firstPartyOrNull(resolved);\n } catch {\n // Unresolvable from Node's algorithm. jiti may still resolve it (e.g.\n // ESM-only packages without CJS entries) — the file itself stays a dep\n // through the entry, but its subtree is unknown for bare packages.\n // Treat unresolvable BARE specifiers as out-of-scope third-party\n // (caught by the zod-version/plugin key when relevant) only when they\n // cannot be project files; '#' subpath imports stay strict.\n return spec.startsWith(\"#\") ? UNRESOLVED : null;\n }\n}\n\n/**\n * Scan + resolve one file into its edge list. Memoized by mtime, so the\n * shared core graph is read and resolved once per process — each entry's\n * closure walk after that is one cheap stat plus Map lookups per file.\n * Returns null for unreadable files.\n */\nfunction fileNode(filePath: string): FileNode | null {\n const stat = tryStat(filePath);\n if (stat === undefined || !stat.isFile()) {\n nodeMemo.set(filePath, null);\n return null;\n }\n const memo = nodeMemo.get(filePath);\n if (memo !== undefined && memo !== null && memo.mtimeMs === stat.mtimeMs) return memo;\n\n let source: string;\n try {\n source = fs.readFileSync(filePath, \"utf8\");\n } catch {\n nodeMemo.set(filePath, null);\n return null;\n }\n // String/comment contents can false-positive both patterns; extra\n // specifiers only add deps (safe), a false `incomplete` only falls back\n // to the superset (safe).\n let incomplete = DYNAMIC_CALL.test(source);\n const edges: string[] = [];\n const seen = new Set<string>();\n const dir = path.dirname(filePath);\n for (const m of source.matchAll(STATIC_SPECIFIER)) {\n const spec = m[1];\n if (!spec) continue;\n const resolved = resolveSpecifier(spec, dir);\n if (resolved === UNRESOLVED) {\n incomplete = true;\n } else if (resolved !== null && !seen.has(resolved)) {\n seen.add(resolved);\n edges.push(resolved);\n }\n }\n const node: FileNode = { mtimeMs: stat.mtimeMs, edges, incomplete };\n nodeMemo.set(filePath, node);\n return node;\n}\n\n/**\n * Compute the static first-party dependency closure of `entryFile`.\n * The entry itself is excluded (its content is part of the cache key).\n */\nexport function collectStaticDeps(entryFile: string): StaticDeps {\n const entry = firstPartyOrNull(path.resolve(entryFile));\n if (entry === null) return INCOMPLETE;\n\n const visited = new Set<string>([entry]);\n const queue: string[] = [entry];\n\n while (queue.length > 0) {\n if (visited.size > MAX_FILES) return INCOMPLETE;\n const file = queue.pop() as string;\n const node = fileNode(file);\n // Any unanalyzable node poisons the whole closure; the caller falls back\n // to the superset, so abort instead of crawling to exhaustion.\n if (node === null || node.incomplete) return INCOMPLETE;\n for (const dep of node.edges) {\n if (!visited.has(dep)) {\n visited.add(dep);\n queue.push(dep);\n }\n }\n }\n\n visited.delete(entry);\n return { deps: [...visited], complete: true };\n}\n\nexport interface TransformDependencies {\n /**\n * Files to declare as watch dependencies, entry first and deduped. Complete\n * only when `complete` is true — otherwise it is just the entry.\n */\n files: string[];\n /**\n * False when the import graph could not be fully analyzed. `files` is then\n * NOT sufficient, and the host has to reach for a coarser signal (a context\n * dependency over the project, or refusing to cache the result) instead of\n * trusting it.\n */\n complete: boolean;\n}\n\n/**\n * Files whose contents can change what `id`'s transform produces — the set a\n * host must declare as watch dependencies.\n *\n * Discovery EXECUTES the file's import graph, so a compiled validator reflects\n * every constant, enum and helper that graph contributed. A host that keys its\n * cache on `id`'s own content alone therefore serves a stale validator after an\n * edit to any of them.\n *\n * Bundler plugins do not need this: their `watchChange` hook fires for every\n * file in the project, so the plugin invalidates wholesale. Loader hosts\n * (Turbopack, plain webpack loaders) have no such hook — their only lever is\n * declaring dependencies per file, which is what this returns.\n *\n * The entry is always included, for the reason the disk cache records it too: a\n * host keys on the content it PASSED, but discovery executed the file from disk.\n *\n * Deliberately NOT backed by `getFirstPartyModulePaths()` when the crawl comes\n * up incomplete, unlike the disk cache's fallback. That superset is\n * point-in-time, not cumulative — `invalidateModuleCache()` empties it — so\n * after an unrelated file's discovery repopulates it, it describes THAT file's\n * graph and can silently omit this one's. The disk cache survives that because\n * `watchChange` invalidates wholesale and deferred entries flush against one\n * end-of-build snapshot; a loader host has neither backstop, so a plausible but\n * wrong list would go undetected. Reporting `complete: false` lets the caller\n * do the sound thing instead.\n */\nexport function transformDependencies(id: string): TransformDependencies {\n // realpath, matching how the crawl records every dep — otherwise a symlinked\n // or differently-cased spelling of the entry appears twice in one list.\n const entry = realpathOf(path.resolve(id));\n const staticDeps = collectStaticDeps(entry);\n if (!staticDeps.complete) return { files: [entry], complete: false };\n return { files: [entry, ...staticDeps.deps.filter((d) => d !== entry)], complete: true };\n}\n"],"mappings":"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"}
@@ -22,6 +22,18 @@ interface Insertion {
22
22
  offset: number;
23
23
  text: string;
24
24
  }
25
+ /**
26
+ * Offset after the shebang and the directive prologue ("use strict",
27
+ * "use client", "use server", ...) — where module-head insertions must go.
28
+ *
29
+ * A directive is only a directive while it is still part of the prologue, so
30
+ * anything prepended at offset 0 demotes it to a plain string expression.
31
+ * Bundlers that give directives meaning reject that outright: React Server
32
+ * Components builds fail with `The "use client" directive must be placed
33
+ * before other expressions`, which is how the generated runtime prologue used
34
+ * to break every Next.js file that exported a schema.
35
+ */
36
+ declare function moduleHeadOffset(code: string): number;
25
37
  /**
26
38
  * Apply non-overlapping edits (any order) and an optional insertion to a
27
39
  * string. Mirrors MagicString.overwrite + appendLeft semantics: an insertion
@@ -29,5 +41,5 @@ interface Insertion {
29
41
  */
30
42
  declare function applyEdits(code: string, edits: readonly Edit[], insert?: Insertion): string;
31
43
  //#endregion
32
- export { Edit, Insertion, applyEdits };
44
+ export { Edit, Insertion, applyEdits, moduleHeadOffset };
33
45
  //# sourceMappingURL=edits.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"edits.d.ts","names":[],"sources":["../../src/unplugin/edits.ts"],"mappings":";;;;;;;;;;;UAWiB;;EAEf;;EAEA;;EAEA;;UAGe;;EAEf;EACA;;;;;;;iBAQc,WAAW,cAAc,gBAAgB,QAAQ,SAAS"}
1
+ {"version":3,"file":"edits.d.ts","names":[],"sources":["../../src/unplugin/edits.ts"],"mappings":";;;;;;;;;;;UAWiB;;EAEf;;EAEA;;EAEA;;UAGe;;EAEf;EACA;;;;;;;;;;;;;iBAcc,iBAAiB;;;;;;iBAwCjB,WAAW,cAAc,gBAAgB,QAAQ,SAAS"}
@@ -1,5 +1,39 @@
1
1
  //#region src/unplugin/edits.ts
2
2
  /**
3
+ * Offset after the shebang and the directive prologue ("use strict",
4
+ * "use client", "use server", ...) — where module-head insertions must go.
5
+ *
6
+ * A directive is only a directive while it is still part of the prologue, so
7
+ * anything prepended at offset 0 demotes it to a plain string expression.
8
+ * Bundlers that give directives meaning reject that outright: React Server
9
+ * Components builds fail with `The "use client" directive must be placed
10
+ * before other expressions`, which is how the generated runtime prologue used
11
+ * to break every Next.js file that exported a schema.
12
+ */
13
+ function moduleHeadOffset(code) {
14
+ let i = 0;
15
+ if (code.startsWith("#!")) {
16
+ const nl = code.indexOf("\n");
17
+ i = nl === -1 ? code.length : nl + 1;
18
+ }
19
+ while (true) {
20
+ while (i < code.length) {
21
+ const ch = code[i];
22
+ if (/\s/.test(ch)) i++;
23
+ else if (ch === "/" && code[i + 1] === "/") {
24
+ const nl = code.indexOf("\n", i);
25
+ i = nl === -1 ? code.length : nl + 1;
26
+ } else if (ch === "/" && code[i + 1] === "*") {
27
+ const end = code.indexOf("*/", i + 2);
28
+ i = end === -1 ? code.length : end + 2;
29
+ } else break;
30
+ }
31
+ const directive = code.slice(i).match(/^(["'])use [^"'\n\\]*\1(?=[^\S\n]*([;\n"']|$))\s*;?[^\S\n]*\n?/);
32
+ if (!directive) return i;
33
+ i += directive[0].length;
34
+ }
35
+ }
36
+ /**
3
37
  * Apply non-overlapping edits (any order) and an optional insertion to a
4
38
  * string. Mirrors MagicString.overwrite + appendLeft semantics: an insertion
5
39
  * at an edit boundary lands before the edit's replacement.
@@ -22,6 +56,6 @@ function applyEdits(code, edits, insert) {
22
56
  return result;
23
57
  }
24
58
  //#endregion
25
- export { applyEdits };
59
+ export { applyEdits, moduleHeadOffset };
26
60
 
27
61
  //# sourceMappingURL=edits.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"edits.js","names":[],"sources":["../../src/unplugin/edits.ts"],"sourcesContent":["/**\n * Edit primitives shared by the transform pipeline stages.\n *\n * Every stage (hoist → hoisted-schema compile → rewrites → runtime inject)\n * describes its changes as non-overlapping `Edit` splices (plus at most one\n * insertion) against the STAGE INPUT. The same edit list is applied two\n * ways: `applyEdits()` produces the stage output string, and the pipeline\n * applies it to a MagicString to produce the stage's sourcemap — deriving\n * both from one list makes code/map divergence impossible.\n */\n\nexport interface Edit {\n /** Start offset in the stage-input string. */\n start: number;\n /** End offset (exclusive) in the stage-input string. */\n end: number;\n /** Replacement text. */\n text: string;\n}\n\nexport interface Insertion {\n /** Offset in the stage-input string to insert at. */\n offset: number;\n text: string;\n}\n\n/**\n * Apply non-overlapping edits (any order) and an optional insertion to a\n * string. Mirrors MagicString.overwrite + appendLeft semantics: an insertion\n * at an edit boundary lands before the edit's replacement.\n */\nexport function applyEdits(code: string, edits: readonly Edit[], insert?: Insertion): string {\n const ops: Array<Edit | (Insertion & { insertion: true })> = [...edits];\n if (insert) ops.push({ ...insert, insertion: true });\n ops.sort((a, b) => {\n const aStart = \"insertion\" in a ? a.offset : a.start;\n const bStart = \"insertion\" in b ? b.offset : b.start;\n if (aStart !== bStart) return bStart - aStart;\n // Same position: apply the insertion last so it ends up BEFORE the\n // replacement text in the output (appendLeft semantics).\n return \"insertion\" in a ? -1 : 1;\n });\n let result = code;\n for (const op of ops) {\n if (\"insertion\" in op) {\n result = result.slice(0, op.offset) + op.text + result.slice(op.offset);\n } else {\n result = result.slice(0, op.start) + op.text + result.slice(op.end);\n }\n }\n return result;\n}\n"],"mappings":";;;;;;AA+BA,SAAgB,WAAW,MAAc,OAAwB,QAA4B;CAC3F,MAAM,MAAuD,CAAC,GAAG,KAAK;CACtE,IAAI,QAAQ,IAAI,KAAK;EAAE,GAAG;EAAQ,WAAW;CAAK,CAAC;CACnD,IAAI,MAAM,GAAG,MAAM;EACjB,MAAM,SAAS,eAAe,IAAI,EAAE,SAAS,EAAE;EAC/C,MAAM,SAAS,eAAe,IAAI,EAAE,SAAS,EAAE;EAC/C,IAAI,WAAW,QAAQ,OAAO,SAAS;EAGvC,OAAO,eAAe,IAAI,KAAK;CACjC,CAAC;CACD,IAAI,SAAS;CACb,KAAK,MAAM,MAAM,KACf,IAAI,eAAe,IACjB,SAAS,OAAO,MAAM,GAAG,GAAG,MAAM,IAAI,GAAG,OAAO,OAAO,MAAM,GAAG,MAAM;MAEtE,SAAS,OAAO,MAAM,GAAG,GAAG,KAAK,IAAI,GAAG,OAAO,OAAO,MAAM,GAAG,GAAG;CAGtE,OAAO;AACT"}
1
+ {"version":3,"file":"edits.js","names":[],"sources":["../../src/unplugin/edits.ts"],"sourcesContent":["/**\n * Edit primitives shared by the transform pipeline stages.\n *\n * Every stage (hoist → hoisted-schema compile → rewrites → runtime inject)\n * describes its changes as non-overlapping `Edit` splices (plus at most one\n * insertion) against the STAGE INPUT. The same edit list is applied two\n * ways: `applyEdits()` produces the stage output string, and the pipeline\n * applies it to a MagicString to produce the stage's sourcemap — deriving\n * both from one list makes code/map divergence impossible.\n */\n\nexport interface Edit {\n /** Start offset in the stage-input string. */\n start: number;\n /** End offset (exclusive) in the stage-input string. */\n end: number;\n /** Replacement text. */\n text: string;\n}\n\nexport interface Insertion {\n /** Offset in the stage-input string to insert at. */\n offset: number;\n text: string;\n}\n\n/**\n * Offset after the shebang and the directive prologue (\"use strict\",\n * \"use client\", \"use server\", ...) — where module-head insertions must go.\n *\n * A directive is only a directive while it is still part of the prologue, so\n * anything prepended at offset 0 demotes it to a plain string expression.\n * Bundlers that give directives meaning reject that outright: React Server\n * Components builds fail with `The \"use client\" directive must be placed\n * before other expressions`, which is how the generated runtime prologue used\n * to break every Next.js file that exported a schema.\n */\nexport function moduleHeadOffset(code: string): number {\n let i = 0;\n if (code.startsWith(\"#!\")) {\n const nl = code.indexOf(\"\\n\");\n i = nl === -1 ? code.length : nl + 1;\n }\n while (true) {\n // Skip whitespace and comments between directives\n while (i < code.length) {\n const ch = code[i] as string;\n if (/\\s/.test(ch)) {\n i++;\n } else if (ch === \"/\" && code[i + 1] === \"/\") {\n const nl = code.indexOf(\"\\n\", i);\n i = nl === -1 ? code.length : nl + 1;\n } else if (ch === \"/\" && code[i + 1] === \"*\") {\n const end = code.indexOf(\"*/\", i + 2);\n i = end === -1 ? code.length : end + 2;\n } else {\n break;\n }\n }\n // The body excludes backslashes and the lookahead demands a statement\n // boundary, so the two shapes that merely START like a directive are left\n // alone: `\"use \\\"strict\\\"\";` (matching through the escape would land the\n // insertion mid-literal) and `\"use \" + mode;` (an expression statement,\n // where it would land on the `+`). Both would emit a syntax error.\n const directive = code\n .slice(i)\n .match(/^([\"'])use [^\"'\\n\\\\]*\\1(?=[^\\S\\n]*([;\\n\"']|$))\\s*;?[^\\S\\n]*\\n?/);\n if (!directive) return i;\n i += directive[0].length;\n }\n}\n\n/**\n * Apply non-overlapping edits (any order) and an optional insertion to a\n * string. Mirrors MagicString.overwrite + appendLeft semantics: an insertion\n * at an edit boundary lands before the edit's replacement.\n */\nexport function applyEdits(code: string, edits: readonly Edit[], insert?: Insertion): string {\n const ops: Array<Edit | (Insertion & { insertion: true })> = [...edits];\n if (insert) ops.push({ ...insert, insertion: true });\n ops.sort((a, b) => {\n const aStart = \"insertion\" in a ? a.offset : a.start;\n const bStart = \"insertion\" in b ? b.offset : b.start;\n if (aStart !== bStart) return bStart - aStart;\n // Same position: apply the insertion last so it ends up BEFORE the\n // replacement text in the output (appendLeft semantics).\n return \"insertion\" in a ? -1 : 1;\n });\n let result = code;\n for (const op of ops) {\n if (\"insertion\" in op) {\n result = result.slice(0, op.offset) + op.text + result.slice(op.offset);\n } else {\n result = result.slice(0, op.start) + op.text + result.slice(op.end);\n }\n }\n return result;\n}\n"],"mappings":";;;;;;;;;;;;AAqCA,SAAgB,iBAAiB,MAAsB;CACrD,IAAI,IAAI;CACR,IAAI,KAAK,WAAW,IAAI,GAAG;EACzB,MAAM,KAAK,KAAK,QAAQ,IAAI;EAC5B,IAAI,OAAO,KAAK,KAAK,SAAS,KAAK;CACrC;CACA,OAAO,MAAM;EAEX,OAAO,IAAI,KAAK,QAAQ;GACtB,MAAM,KAAK,KAAK;GAChB,IAAI,KAAK,KAAK,EAAE,GACd;QACK,IAAI,OAAO,OAAO,KAAK,IAAI,OAAO,KAAK;IAC5C,MAAM,KAAK,KAAK,QAAQ,MAAM,CAAC;IAC/B,IAAI,OAAO,KAAK,KAAK,SAAS,KAAK;GACrC,OAAO,IAAI,OAAO,OAAO,KAAK,IAAI,OAAO,KAAK;IAC5C,MAAM,MAAM,KAAK,QAAQ,MAAM,IAAI,CAAC;IACpC,IAAI,QAAQ,KAAK,KAAK,SAAS,MAAM;GACvC,OACE;EAEJ;EAMA,MAAM,YAAY,KACf,MAAM,CAAC,CAAC,CACR,MAAM,gEAAgE;EACzE,IAAI,CAAC,WAAW,OAAO;EACvB,KAAK,UAAU,EAAE,CAAC;CACpB;AACF;;;;;;AAOA,SAAgB,WAAW,MAAc,OAAwB,QAA4B;CAC3F,MAAM,MAAuD,CAAC,GAAG,KAAK;CACtE,IAAI,QAAQ,IAAI,KAAK;EAAE,GAAG;EAAQ,WAAW;CAAK,CAAC;CACnD,IAAI,MAAM,GAAG,MAAM;EACjB,MAAM,SAAS,eAAe,IAAI,EAAE,SAAS,EAAE;EAC/C,MAAM,SAAS,eAAe,IAAI,EAAE,SAAS,EAAE;EAC/C,IAAI,WAAW,QAAQ,OAAO,SAAS;EAGvC,OAAO,eAAe,IAAI,KAAK;CACjC,CAAC;CACD,IAAI,SAAS;CACb,KAAK,MAAM,MAAM,KACf,IAAI,eAAe,IACjB,SAAS,OAAO,MAAM,GAAG,GAAG,MAAM,IAAI,GAAG,OAAO,OAAO,MAAM,GAAG,MAAM;MAEtE,SAAS,OAAO,MAAM,GAAG,GAAG,KAAK,IAAI,GAAG,OAAO,OAAO,MAAM,GAAG,GAAG;CAGtE,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"hoist.d.ts","names":[],"sources":["../../src/unplugin/hoist.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA+Da,qBAAmB;;;;;;cAOnB,aAAW;;UAGP;;EAEf;;EAEA;;UAGQ;;EAER,KAAK;;EAEL,KAAK;;EAEL,SAAS,YAAY;;;;;;;iBAQP,sBAAsB,eAAe;UAq9BpC;;;;;;;EAOf,oBAAoB;;;;;;;;EAQpB;;;;;;;iBAQc,yBACd;EACG,WAAW;EAAa,cAAc;;;UAY1B;;EAEf;;EAEA;;UAGe;;EAEf;;EAEA,SAAS;;;;;;EAMT,OAAO;EACP,QAAQ;;;;;;iBAOM,gBAAgB,cAAc,UAAU;;;;;iBAQxC,oBAAoB,cAAc,UAAU,eAAe"}
1
+ {"version":3,"file":"hoist.d.ts","names":[],"sources":["../../src/unplugin/hoist.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA+Da,qBAAmB;;;;;;cAOnB,aAAW;;UAGP;;EAEf;;EAEA;;UAGQ;;EAER,KAAK;;EAEL,KAAK;;EAEL,SAAS,YAAY;;;;;;;iBAQP,sBAAsB,eAAe;UAw7BpC;;;;;;;EAOf,oBAAoB;;;;;;;;EAQpB;;;;;;;iBAQc,yBACd;EACG,WAAW;EAAa,cAAc;;;UAY1B;;EAEf;;EAEA;;UAGe;;EAEf;;EAEA,SAAS;;;;;;EAMT,OAAO;EACP,QAAQ;;;;;;iBAOM,gBAAgB,cAAc,UAAU;;;;;iBAQxC,oBAAoB,cAAc,UAAU,eAAe"}
@@ -1,4 +1,4 @@
1
- import { applyEdits } from "./edits.js";
1
+ import { applyEdits, moduleHeadOffset } from "./edits.js";
2
2
  import { Parser } from "acorn";
3
3
  //#region src/unplugin/hoist.ts
4
4
  /**
@@ -723,30 +723,6 @@ function analyzeCaptures(expr) {
723
723
  impure
724
724
  };
725
725
  }
726
- /** Offset after the shebang and directive prologue ("use client", ...). */
727
- function insertionOffset(code) {
728
- let i = 0;
729
- if (code.startsWith("#!")) {
730
- const nl = code.indexOf("\n");
731
- i = nl === -1 ? code.length : nl + 1;
732
- }
733
- while (true) {
734
- while (i < code.length) {
735
- const ch = code[i];
736
- if (/\s/.test(ch)) i++;
737
- else if (ch === "/" && code[i + 1] === "/") {
738
- const nl = code.indexOf("\n", i);
739
- i = nl === -1 ? code.length : nl + 1;
740
- } else if (ch === "/" && code[i + 1] === "*") {
741
- const end = code.indexOf("*/", i + 2);
742
- i = end === -1 ? code.length : end + 2;
743
- } else break;
744
- }
745
- const directive = code.slice(i).match(/^(["'])use [^"'\n]*\1\s*;?[^\S\n]*\n?/);
746
- if (!directive) return i;
747
- i += directive[0].length;
748
- }
749
- }
750
726
  /** Keywords that precede `( ... ) {` without binding anything. */
751
727
  const NON_BINDING_KEYWORDS = "if|for|while|switch|return|typeof|await|yield|new|do|else|case|in|of|delete|void";
752
728
  /**
@@ -998,7 +974,7 @@ function hoistZodSchemasMeta(code, options) {
998
974
  text: h.name
999
975
  }));
1000
976
  const insert = {
1001
- offset: insertionOffset(code),
977
+ offset: moduleHeadOffset(code),
1002
978
  text: `${decls}\n`
1003
979
  };
1004
980
  return {