@warlock.js/web 5.2.0 → 5.2.1

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 (201) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/esm/build/contribution.d.mts +1 -1
  3. package/esm/build/contribution.mjs +1 -1
  4. package/esm/build/contribution.mjs.map +1 -1
  5. package/esm/build/discover-pages.mjs +1 -1
  6. package/esm/build/discover-pages.mjs.map +1 -1
  7. package/esm/build/generate-client-registry.mjs +1 -1
  8. package/esm/build/generate-client-registry.mjs.map +1 -1
  9. package/esm/build/generate-pages-barrel.mjs +1 -1
  10. package/esm/build/generate-pages-barrel.mjs.map +1 -1
  11. package/esm/build/page-default-export.mjs +1 -1
  12. package/esm/build/page-default-export.mjs.map +1 -1
  13. package/esm/build/page-routes-manifest.mjs +1 -1
  14. package/esm/build/page-routes-manifest.mjs.map +1 -1
  15. package/esm/build/public-files.mjs +1 -1
  16. package/esm/build/public-files.mjs.map +1 -1
  17. package/esm/build/read-route-exports.mjs +1 -1
  18. package/esm/build/read-route-exports.mjs.map +1 -1
  19. package/esm/client/build-hydrated-tree.mjs +1 -1
  20. package/esm/client/build-hydrated-tree.mjs.map +1 -1
  21. package/esm/client/hydrate-page.mjs +1 -1
  22. package/esm/client/hydrate-page.mjs.map +1 -1
  23. package/esm/client/navigation/current-route.d.mts +1 -1
  24. package/esm/client/navigation/current-route.mjs +1 -1
  25. package/esm/client/navigation/current-route.mjs.map +1 -1
  26. package/esm/client/navigation/fetch-page-data.mjs +1 -1
  27. package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
  28. package/esm/client/navigation/navigation-root.mjs +1 -1
  29. package/esm/client/navigation/navigation-root.mjs.map +1 -1
  30. package/esm/client/navigation/prefetch.mjs +1 -1
  31. package/esm/client/navigation/prefetch.mjs.map +1 -1
  32. package/esm/client/navigation/refresh.d.mts +1 -1
  33. package/esm/client/navigation/refresh.mjs +1 -1
  34. package/esm/client/navigation/refresh.mjs.map +1 -1
  35. package/esm/client/navigation/scroll-to-fragment.mjs +1 -1
  36. package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -1
  37. package/esm/client/navigation/verbs.d.mts +1 -1
  38. package/esm/client/navigation/verbs.mjs +1 -1
  39. package/esm/client/navigation/verbs.mjs.map +1 -1
  40. package/esm/client/runtime/manifest.d.mts +1 -1
  41. package/esm/client/runtime/manifest.mjs +1 -1
  42. package/esm/client/runtime/manifest.mjs.map +1 -1
  43. package/esm/client/runtime/matcher.d.mts +1 -1
  44. package/esm/client/runtime/matcher.mjs +1 -1
  45. package/esm/client/runtime/matcher.mjs.map +1 -1
  46. package/esm/client/runtime/types.d.mts +1 -1
  47. package/esm/components/default-app.mjs +1 -1
  48. package/esm/components/default-app.mjs.map +1 -1
  49. package/esm/components/document-context.d.mts +1 -1
  50. package/esm/components/document-context.mjs +1 -1
  51. package/esm/components/document-context.mjs.map +1 -1
  52. package/esm/components/head.d.mts +1 -1
  53. package/esm/components/head.mjs +1 -1
  54. package/esm/components/head.mjs.map +1 -1
  55. package/esm/components/link.d.mts +1 -1
  56. package/esm/components/link.mjs +1 -1
  57. package/esm/components/link.mjs.map +1 -1
  58. package/esm/components/scripts.d.mts +1 -1
  59. package/esm/components/scripts.mjs +1 -1
  60. package/esm/components/scripts.mjs.map +1 -1
  61. package/esm/context.d.mts +1 -1
  62. package/esm/hydration/index.mjs +1 -1
  63. package/esm/hydration/index.mjs.map +1 -1
  64. package/esm/hydration-payload.mjs +1 -1
  65. package/esm/hydration-payload.mjs.map +1 -1
  66. package/esm/index.d.mts +1 -1
  67. package/esm/loaders.d.mts +1 -1
  68. package/esm/metadata.d.mts +1 -1
  69. package/esm/metadata.mjs +1 -1
  70. package/esm/metadata.mjs.map +1 -1
  71. package/esm/props.d.mts +1 -1
  72. package/esm/route.d.mts +1 -1
  73. package/esm/routing/compose-route-path.d.mts +1 -1
  74. package/esm/routing/compose-route-path.mjs +1 -1
  75. package/esm/routing/compose-route-path.mjs.map +1 -1
  76. package/esm/routing/data-request.mjs +1 -1
  77. package/esm/routing/data-request.mjs.map +1 -1
  78. package/esm/routing/filesystem-route.mjs +1 -1
  79. package/esm/routing/filesystem-route.mjs.map +1 -1
  80. package/esm/routing/layout-policy.mjs +1 -1
  81. package/esm/routing/layout-policy.mjs.map +1 -1
  82. package/esm/routing/navigator.mjs +1 -1
  83. package/esm/routing/navigator.mjs.map +1 -1
  84. package/esm/routing/query-string.d.mts +1 -1
  85. package/esm/routing/query-string.mjs +1 -1
  86. package/esm/routing/query-string.mjs.map +1 -1
  87. package/esm/routing/route-identity.mjs +1 -1
  88. package/esm/routing/route-identity.mjs.map +1 -1
  89. package/esm/routing/route-table.d.mts +1 -1
  90. package/esm/routing/route-table.mjs +1 -1
  91. package/esm/routing/route-table.mjs.map +1 -1
  92. package/esm/routing/router-events.d.mts +1 -1
  93. package/esm/routing/router-events.mjs +1 -1
  94. package/esm/routing/router-events.mjs.map +1 -1
  95. package/esm/routing/url-fragment.mjs +1 -1
  96. package/esm/routing/url-fragment.mjs.map +1 -1
  97. package/esm/runtime/register-modules.d.mts +1 -1
  98. package/esm/runtime/register-modules.mjs +1 -1
  99. package/esm/runtime/register-modules.mjs.map +1 -1
  100. package/esm/server/build-hydration-payload.mjs +1 -1
  101. package/esm/server/build-hydration-payload.mjs.map +1 -1
  102. package/esm/server/client-asset-url-prefix.d.mts +1 -1
  103. package/esm/server/client-asset-url-prefix.mjs +1 -1
  104. package/esm/server/client-asset-url-prefix.mjs.map +1 -1
  105. package/esm/server/create-page-module-loader.d.mts +1 -1
  106. package/esm/server/create-page-module-loader.mjs +1 -1
  107. package/esm/server/create-page-module-loader.mjs.map +1 -1
  108. package/esm/server/create-page-route-handler.d.mts +1 -1
  109. package/esm/server/create-page-route-handler.mjs +1 -1
  110. package/esm/server/create-page-route-handler.mjs.map +1 -1
  111. package/esm/server/dev-server.mjs +1 -1
  112. package/esm/server/dev-server.mjs.map +1 -1
  113. package/esm/server/error-page.d.mts +1 -1
  114. package/esm/server/error-page.mjs +1 -1
  115. package/esm/server/error-page.mjs.map +1 -1
  116. package/esm/server/execute-page-request.d.mts +1 -1
  117. package/esm/server/execute-page-request.mjs +1 -1
  118. package/esm/server/execute-page-request.mjs.map +1 -1
  119. package/esm/server/execute-page-request.types.d.mts +1 -1
  120. package/esm/server/hydration-client-url.d.mts +1 -1
  121. package/esm/server/hydration-client-url.mjs +1 -1
  122. package/esm/server/hydration-client-url.mjs.map +1 -1
  123. package/esm/server/install-page-routes-from-manifest.d.mts +1 -1
  124. package/esm/server/install-page-routes-from-manifest.mjs +1 -1
  125. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  126. package/esm/server/install-page-routes.d.mts +1 -1
  127. package/esm/server/install-page-routes.mjs +1 -1
  128. package/esm/server/install-page-routes.mjs.map +1 -1
  129. package/esm/server/install-production-page-routes.mjs +1 -1
  130. package/esm/server/install-production-page-routes.mjs.map +1 -1
  131. package/esm/server/match-page-route.mjs +1 -1
  132. package/esm/server/match-page-route.mjs.map +1 -1
  133. package/esm/server/not-found-page.d.mts +1 -1
  134. package/esm/server/not-found-page.mjs +1 -1
  135. package/esm/server/not-found-page.mjs.map +1 -1
  136. package/esm/server/page-context.d.mts +1 -1
  137. package/esm/server/page-context.mjs +1 -1
  138. package/esm/server/page-context.mjs.map +1 -1
  139. package/esm/server/page-file-change.mjs +1 -1
  140. package/esm/server/page-file-change.mjs.map +1 -1
  141. package/esm/server/page-manifest.d.mts +1 -1
  142. package/esm/server/page-manifest.mjs +1 -1
  143. package/esm/server/page-manifest.mjs.map +1 -1
  144. package/esm/server/page-render-bundle.mjs +1 -1
  145. package/esm/server/page-render-bundle.mjs.map +1 -1
  146. package/esm/server/page-route-reload.mjs +1 -1
  147. package/esm/server/page-route-reload.mjs.map +1 -1
  148. package/esm/server/register-production-public-files.mjs +1 -1
  149. package/esm/server/register-production-public-files.mjs.map +1 -1
  150. package/esm/server/render-page.d.mts +1 -1
  151. package/esm/server/render-page.mjs +1 -1
  152. package/esm/server/render-page.mjs.map +1 -1
  153. package/esm/server/resolve-page-metadata.mjs +1 -1
  154. package/esm/server/resolve-page-metadata.mjs.map +1 -1
  155. package/esm/server/resolve-validation-data.mjs +1 -1
  156. package/esm/server/resolve-validation-data.mjs.map +1 -1
  157. package/esm/server/settle-page-response.d.mts +1 -1
  158. package/esm/server/settle-page-response.mjs +1 -1
  159. package/esm/server/settle-page-response.mjs.map +1 -1
  160. package/esm/server/stylesheet-urls.d.mts +1 -1
  161. package/esm/server/stylesheet-urls.mjs +1 -1
  162. package/esm/server/stylesheet-urls.mjs.map +1 -1
  163. package/esm/server/unregistered-pages.mjs +1 -1
  164. package/esm/server/unregistered-pages.mjs.map +1 -1
  165. package/esm/server/web-connector-factory.d.mts +1 -1
  166. package/esm/server/web-connector-factory.mjs +1 -1
  167. package/esm/server/web-connector-factory.mjs.map +1 -1
  168. package/esm/server/web-connector.d.mts +1 -1
  169. package/esm/server/web-connector.mjs +1 -1
  170. package/esm/server/web-connector.mjs.map +1 -1
  171. package/esm/shared.d.mts +1 -1
  172. package/esm/shared.mjs +1 -1
  173. package/esm/shared.mjs.map +1 -1
  174. package/esm/validation.d.mts +1 -1
  175. package/esm/vite/app-convention-aliases.mjs +1 -1
  176. package/esm/vite/app-convention-aliases.mjs.map +1 -1
  177. package/esm/vite/build-client.d.mts +1 -1
  178. package/esm/vite/build-client.mjs +1 -1
  179. package/esm/vite/build-client.mjs.map +1 -1
  180. package/esm/vite/gate-a-resolve.d.mts +1 -1
  181. package/esm/vite/gate-a-resolve.mjs +1 -1
  182. package/esm/vite/gate-a-resolve.mjs.map +1 -1
  183. package/esm/vite/gate-b-secrets.d.mts +1 -1
  184. package/esm/vite/gate-b-secrets.mjs +1 -1
  185. package/esm/vite/gate-b-secrets.mjs.map +1 -1
  186. package/esm/vite/gate-c-verify.d.mts +1 -1
  187. package/esm/vite/gate-c-verify.mjs +1 -1
  188. package/esm/vite/gate-c-verify.mjs.map +1 -1
  189. package/esm/vite/hydration-entries.d.mts +1 -1
  190. package/esm/vite/hydration-entries.mjs +1 -1
  191. package/esm/vite/hydration-entries.mjs.map +1 -1
  192. package/esm/vite/index.d.mts +1 -1
  193. package/esm/vite/index.mjs +1 -1
  194. package/esm/vite/index.mjs.map +1 -1
  195. package/esm/vite/page-registry-plugin.d.mts +1 -1
  196. package/esm/vite/page-registry-plugin.mjs +1 -1
  197. package/esm/vite/page-registry-plugin.mjs.map +1 -1
  198. package/esm/vite/projection.d.mts +1 -1
  199. package/esm/vite/projection.mjs +1 -1
  200. package/esm/vite/projection.mjs.map +1 -1
  201. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"gate-c-verify.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/vite/gate-c-verify.ts"],"sourcesContent":["/**\r\n * Gate C — emitted-output verification.\r\n *\r\n * Projection removes the six server exports before the client graph forms.\r\n * Gate A refuses forbidden import PATHS. Gate B refuses inline secret reads.\r\n * All three act BEFORE or DURING the build, on source or the module graph —\r\n * none of them ever looks at what actually came out the other end. Gate C\r\n * is that check: a build-time assertion on the EMITTED client bundle for\r\n * every page, verifying:\r\n *\r\n * 1. The emitted code contains none of the six server export names\r\n * (`route`, `middleware`, `validation`, `loader`, `metadata`, `prefix`) as a\r\n * top-level binding, exported or not — `findLeakedServerExports`.\r\n * 2. The emitted module graph (Rollup's `OutputBundle`, as seen in\r\n * `generateBundle`) contains no import edge into a\r\n * `warlock.environment: \"server\"` (or absent-defaulting-to-server)\r\n * package — re-derived from the exact classifier Gate A uses\r\n * (`gate-a-resolve.ts`'s `createEnvironmentClassifier`), never a second,\r\n * independently-drifting classification scheme — `findLeakedServerImportEdges`.\r\n *\r\n * This is defense in depth: if projection or Gate A has a bug that lets\r\n * something through, Gate C is what turns \"shipped a leak\" into \"build\r\n * fails.\" Both check functions are exported as PURE functions over a bundle\r\n * object precisely so a test can construct a bundle where projection/Gate A\r\n * are bypassed entirely (a hand-built leaked chunk, never produced by a real\r\n * pipeline run) and still prove Gate C catches it independently — re-running\r\n * the D.1-D.5 happy-path fixtures through the composed pipeline only proves\r\n * the happy path, never that Gate C itself has teeth.\r\n *\r\n * Gate C also emits the inlined-`PUBLIC_*`-value manifest — see\r\n * `buildPublicEnvManifest` below — and is the only\r\n * one of the three gates that runs exclusively at `generateBundle` time: it\r\n * has nothing to say about source, only about output.\r\n */\r\nimport { parse } from \"@babel/parser\";\r\nimport type { Plugin } from \"vite\";\r\nimport {\r\n createEnvironmentClassifier,\r\n type EnvironmentClassifier,\r\n type EnvironmentClassifierOptions,\r\n} from \"./gate-a-resolve\";\r\nimport { createPublicEnvTracker, type PublicEnvTracker } from \"./gate-b-secrets\";\r\nimport { SERVER_EXPORT_NAMES } from \"./projection\";\r\n\r\nexport interface ServerExportLeak {\r\n fileName: string;\r\n exportName: string;\r\n line?: number;\r\n}\r\n\r\nexport interface ServerImportEdgeLeak {\r\n fileName: string;\r\n moduleId: string;\r\n packageName: string;\r\n}\r\n\r\nexport interface PublicEnvManifestEntry {\r\n key: string;\r\n /** `null` when `redacted` is true — see `isSafeToShowValue`. */\r\n value: string | null;\r\n redacted: boolean;\r\n}\r\n\r\n/**\r\n * A structurally-minimal view of Rollup's `OutputBundle` — only the fields\r\n * Gate C actually reads. Deliberately NOT the real `OutputBundle`/`OutputChunk`\r\n * types: Part 1's own discipline requires proving Gate C catches a bundle\r\n * that a real pipeline could never produce (a hand-built leaked chunk), and\r\n * forcing every test fixture to satisfy Rollup's full chunk shape would\r\n * fight that requirement for no safety benefit — Vite's real `generateBundle`\r\n * hook still hands Gate C a real `OutputBundle`, which is a structural\r\n * superset of this.\r\n */\r\ntype BundleLike = Record<\r\n string,\r\n {\r\n type?: string;\r\n fileName?: string;\r\n code?: string;\r\n moduleIds?: readonly string[];\r\n }\r\n>;\r\n\r\n/**\r\n * Recursively collects every top-level statement's bound name(s), matching:\r\n * - `const route = ...` / `function loader() {}` (exported or not — the\r\n * check is on top-level BINDINGS, not top-level exports).\r\n * - `export const route = ...` / `export function loader() {}` (the\r\n * `ExportNamedDeclaration` wrapper form).\r\n * - `export { route }` (the bare re-export-specifier form Rollup sometimes\r\n * emits instead of inlining the declaration itself).\r\n */\r\nfunction collectServerExportNames(stmt: any): Array<{ name: string; line: number }> {\r\n const matches: Array<{ name: string; line: number }> = [];\r\n const line = stmt.loc?.start?.line as number | undefined;\r\n\r\n function record(name: string | undefined) {\r\n if (name && SERVER_EXPORT_NAMES.has(name)) matches.push({ name, line: line ?? 0 });\r\n }\r\n\r\n if (stmt.type === \"VariableDeclaration\") {\r\n for (const decl of stmt.declarations) {\r\n if (decl.id?.type === \"Identifier\") record(decl.id.name);\r\n }\r\n } else if (stmt.type === \"FunctionDeclaration\") {\r\n record(stmt.id?.name);\r\n } else if (stmt.type === \"ExportNamedDeclaration\") {\r\n if (stmt.declaration) matches.push(...collectServerExportNames(stmt.declaration));\r\n for (const specifier of stmt.specifiers ?? []) {\r\n record(specifier.exported?.name ?? specifier.exported?.value);\r\n }\r\n }\r\n\r\n return matches;\r\n}\r\n\r\n/**\r\n * Thrown when an emitted chunk cannot be parsed, which means Gate C could not\r\n * inspect it. A gate that skips what it cannot read reports \"no violation\r\n * found\" on precisely the input it failed to look at — the one input where\r\n * that answer is worthless. So an unreadable chunk fails the build instead.\r\n */\r\nexport class UnverifiableChunkError extends Error {\r\n readonly fileName: string;\r\n\r\n constructor(fileName: string, cause?: unknown) {\r\n super(\r\n [\r\n `Warlock stopped this build: a file in your client bundle could not be checked for server-only code.`,\r\n ``,\r\n `File: ${fileName}`,\r\n `Cause: this file could not be parsed as JavaScript, so the client/server boundary check could not be performed on it. Warlock cannot confirm that the server-only exports (route, middleware, validation, loader, metadata, prefix) were kept out of it.`,\r\n `Fix: the build is being stopped rather than passed, because a file that was never checked is not a file known to be safe. This emitted file is outside the JavaScript syntax Warlock can currently verify. Two things commonly put it there, and this error cannot tell which: the output uses syntax newer than the parser Warlock ships with, or a plugin or loader emitted non-standard syntax into the client bundle. Check the compatibility of whatever produced this file, then build again.`,\r\n ].join(\"\\n\"),\r\n );\r\n this.name = \"UnverifiableChunkError\";\r\n this.fileName = fileName;\r\n if (cause !== undefined) (this as { cause?: unknown }).cause = cause;\r\n }\r\n}\r\n\r\n/**\r\n * Part 1, item 1: parses each emitted chunk's ACTUAL code (never pre-transform\r\n * source) and looks for a top-level binding named one of the five server\r\n * exports. A parse failure on an emitted chunk FAILS the gate\r\n * (`UnverifiableChunkError`) — it is never skipped. Skipping would report the\r\n * bundle clean on the one chunk the gate did not actually inspect, which is a\r\n * safety check failing open; a build stopped on an unreadable chunk is\r\n * recoverable, a boundary silently unenforced is not.\r\n *\r\n * This SURVIVES real minification (pinned by the `gate-c-verify.spec.ts`\r\n * \"D.8\" describe block). A minifier (esbuild,\r\n * Vite's default) is only free to rename LOCAL bindings; the exported name\r\n * itself is part of the module's public interface and is never mangled — a\r\n * minified `export const route = ...` still emits `export { e as route }`,\r\n * with the local identifier renamed (`e`) but `route` intact as the specifier's\r\n * `exported` name, which is exactly the field `collectServerExportNames`\r\n * reads for the `ExportNamedDeclaration` specifier form. No name-independent\r\n * redesign needed here — verified against a real `build.minify: true` output,\r\n * not assumed.\r\n */\r\nexport function findLeakedServerExports(bundle: BundleLike): ServerExportLeak[] {\r\n const leaks: ServerExportLeak[] = [];\r\n\r\n for (const file of Object.values(bundle)) {\r\n if (!file || file.type !== \"chunk\" || typeof file.code !== \"string\") continue;\r\n\r\n let ast: any;\r\n try {\r\n ast = parse(file.code, { sourceType: \"module\", plugins: [\"typescript\", \"jsx\"] });\r\n } catch (error) {\r\n throw new UnverifiableChunkError(file.fileName ?? \"(unnamed chunk)\", error);\r\n }\r\n\r\n for (const stmt of ast.program.body as any[]) {\r\n for (const match of collectServerExportNames(stmt)) {\r\n leaks.push({ fileName: file.fileName as string, exportName: match.name, line: match.line });\r\n }\r\n }\r\n }\r\n\r\n return leaks;\r\n}\r\n\r\n/**\r\n * Part 1, item 2: walks every emitted chunk's `moduleIds` (the resolved,\r\n * absolute file paths of every source module Rollup actually bundled into\r\n * that chunk) and classifies each one via the SAME `EnvironmentClassifier`\r\n * Gate A's `resolveId` uses, re-derived from `gate-a-resolve.ts` rather than\r\n * hand-rolled a second time. A moduleId that maps to a governed-scope\r\n * package classified `\"server\"` is an import edge Gate A should already\r\n * have refused — if it's here anyway, the earlier gate has a bug.\r\n *\r\n * D.8 verdict: this SURVIVES real minification unconditionally, and for a\r\n * stronger reason than item 1 above — it never reads emitted code text or\r\n * identifier names at all. `moduleIds` is Rollup's own bundling metadata\r\n * (which resolved source files ended up in this chunk), untouched by\r\n * minification, which only rewrites code, not that metadata. Verified\r\n * against a real `build.minify: true` output in `gate-c-verify.spec.ts`.\r\n */\r\nexport function findLeakedServerImportEdges(\r\n bundle: BundleLike,\r\n classifier: Pick<EnvironmentClassifier, \"environmentOf\" | \"packageNameForFilePath\">,\r\n): ServerImportEdgeLeak[] {\r\n const leaks: ServerImportEdgeLeak[] = [];\r\n\r\n for (const file of Object.values(bundle)) {\r\n if (!file || file.type !== \"chunk\") continue;\r\n\r\n for (const moduleId of file.moduleIds ?? []) {\r\n const packageName = classifier.packageNameForFilePath(moduleId);\r\n if (!packageName) continue;\r\n if (classifier.environmentOf(packageName) === \"server\") {\r\n leaks.push({ fileName: file.fileName as string, moduleId, packageName });\r\n }\r\n }\r\n }\r\n\r\n return leaks;\r\n}\r\n\r\n/**\r\n * Conservative \"does this PUBLIC_* value look safe to print in the reviewable\r\n * manifest\" heuristic: if genuinely unsure, show the key only — never guess\r\n * that a value is safe. The whole point of this manifest is that the `PUBLIC_`\r\n * prefix rule alone is not trustworthy — someone can and will name an actual\r\n * secret `PUBLIC_STRIPE_KEY` — so a value that LOOKS like a credential is\r\n * redacted regardless of what its key is named. Non-string values (booleans,\r\n * numbers) are never secret-shaped and always shown.\r\n */\r\nconst CREDENTIALED_URL_RE = /:\\/\\/[^/\\s]+:[^/\\s@]+@/; // scheme://user:pass@host\r\nconst KNOWN_SECRET_PREFIX_RES = [\r\n /^sk_(live|test)_/i, // Stripe secret key (pk_ publishable is a different prefix)\r\n /^rk_(live|test)_/i, // Stripe restricted key\r\n /^AKIA[0-9A-Z]{12,}/, // AWS access key id\r\n /^gh[pousr]_[A-Za-z0-9]{20,}/, // GitHub personal/OAuth/user/server tokens\r\n /^glpat-[A-Za-z0-9_-]{20,}/, // GitLab personal access token\r\n /^xox[baprs]-[A-Za-z0-9-]{10,}/, // Slack tokens\r\n /^eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$/, // JWT (3 base64url segments)\r\n];\r\n// A long opaque token made up only of base64url/hex-ish characters, with both\r\n// letters and digits, and no separators a human-authored public value (a\r\n// URL, a short id) would normally contain — conservatively treated as a\r\n// secret-shaped random token even though it might genuinely be safe.\r\nconst HIGH_ENTROPY_TOKEN_RE = /^[A-Za-z0-9+/_=-]{32,}$/;\r\n\r\nfunction isSafeToShowValue(value: unknown): boolean {\r\n if (typeof value !== \"string\" || value.length === 0) return true;\r\n if (CREDENTIALED_URL_RE.test(value)) return false;\r\n if (KNOWN_SECRET_PREFIX_RES.some((re) => re.test(value))) return false;\r\n if (HIGH_ENTROPY_TOKEN_RE.test(value) && /[0-9]/.test(value) && /[a-zA-Z]/.test(value)) return false;\r\n return true;\r\n}\r\n\r\nfunction stringifyEnvValue(value: unknown): string {\r\n if (typeof value === \"string\") return value;\r\n return JSON.stringify(value) ?? \"undefined\";\r\n}\r\n\r\n/**\r\n * The enumerated, human-reviewable list of\r\n * every `PUBLIC_*` key actually inlined into the client bundle. Built\r\n * directly from `tracker.referencedKeys` — the exact same `Set` Gate B's own\r\n * `generateBundle` unread-key check (`gate-b-secrets.ts`) reads from — so\r\n * this manifest and that exclusion logic agree BY CONSTRUCTION: they are two\r\n * readers of one Set, not two independent recomputations of \"which keys were\r\n * read\" that could drift apart. A key only reaches `referencedKeys` once\r\n * `findViolation` in `gate-b-secrets.ts` has seen a statically-resolved\r\n * `import.meta.env.PUBLIC_X` read for it, which is also precisely the\r\n * condition under which Gate B allows its value to be inlined at all — an\r\n * unread key that somehow leaked in anyway is a Gate B `generateBundle`\r\n * BUILD FAILURE (see `gate-b-secrets.ts`), never silently listed here.\r\n */\r\nexport function buildPublicEnvManifest(tracker: PublicEnvTracker): PublicEnvManifestEntry[] {\r\n return [...tracker.referencedKeys]\r\n .sort()\r\n .map((key) => {\r\n const value = tracker.declaredEnv[key];\r\n const safe = isSafeToShowValue(value);\r\n return { key, value: safe ? stringifyEnvValue(value) : null, redacted: !safe };\r\n });\r\n}\r\n\r\nexport interface GateCOptions extends EnvironmentClassifierOptions {\r\n /**\r\n * Shared with `gateBSecrets({ tracker })` — required for the manifest to\r\n * reflect what THIS build's Gate B pass actually saw. Defaults to a\r\n * private, unshared tracker (always empty) if omitted, which is only ever\r\n * correct when Gate C runs standalone in a test.\r\n */\r\n tracker?: PublicEnvTracker;\r\n /** Defaults to `\"warlock-env-manifest.json\"` (Suki's suggested name). */\r\n manifestFileName?: string;\r\n}\r\n\r\n/**\r\n * The client-build Vite plugin. Runs only at `generateBundle` — Gate C has\r\n * nothing to say about source, only about the bundle Rollup actually wrote.\r\n * Skipped for the SSR/server build, same as Gate B (`gate-b-secrets.ts`):\r\n * a page's server exports are meant to survive in that build; only the\r\n * client build is judged.\r\n */\r\nexport function gateCVerify(options: GateCOptions = {}): Plugin {\r\n const classifier = createEnvironmentClassifier(options);\r\n const tracker = options.tracker ?? createPublicEnvTracker();\r\n const manifestFileName = options.manifestFileName ?? \"warlock-env-manifest.json\";\r\n\r\n return {\r\n name: \"warlock:gate-c-verify\",\r\n generateBundle(_outputOptions, bundle) {\r\n if (this.environment?.config?.consumer === \"server\") return;\r\n\r\n const exportLeak = findLeakedServerExports(bundle)[0];\r\n if (exportLeak) {\r\n this.error(\r\n [\r\n `Gate C refused a build: a server export survived into the emitted client bundle.`,\r\n ``,\r\n `File: ${exportLeak.fileName}${exportLeak.line ? `:${exportLeak.line}` : \"\"}`,\r\n `Export: ${exportLeak.exportName}`,\r\n `Cause: \"${exportLeak.exportName}\" is one of the six server exports (route, middleware, validation, loader, metadata, prefix) and is still present as a top-level binding in the EMITTED client chunk — projection and/or Gate A should have removed or refused it before the bundle was written.`,\r\n `Fix: this should already be impossible if projection and Gate A ran correctly — investigate why \"${exportLeak.exportName}\" reached the emitted output (a projection bug, a build config that bypasses these plugins, or a plugin ordering change) rather than assuming this build is a one-off; Gate C is defense in depth, not the primary fence.`,\r\n ].join(\"\\n\"),\r\n );\r\n }\r\n\r\n const importEdgeLeak = findLeakedServerImportEdges(bundle, classifier)[0];\r\n if (importEdgeLeak) {\r\n this.error(\r\n [\r\n `Gate C refused a build: a server-only import edge survived into the emitted client bundle's module graph.`,\r\n ``,\r\n `File: ${importEdgeLeak.fileName}`,\r\n `Module: ${importEdgeLeak.moduleId}`,\r\n `Cause: \"${importEdgeLeak.moduleId}\" belongs to ${importEdgeLeak.packageName}, a server-only package (it declares \"warlock\": { \"environment\": \"server\" } in its package.json), and is present among the bundled modules of the emitted client chunk \"${importEdgeLeak.fileName}\" — Gate A's resolveId should have refused this import before it ever reached the bundle.`,\r\n `Fix: this should already be impossible if Gate A ran on this build — investigate why ${importEdgeLeak.packageName} reached the emitted output (a Gate A bypass, a custom resolveId/external override, or a plugin ordering change) rather than assuming this build is a one-off; Gate C is defense in depth, not the primary fence.`,\r\n ].join(\"\\n\"),\r\n );\r\n }\r\n\r\n const manifest = buildPublicEnvManifest(tracker);\r\n\r\n // EMITTED through `this.emitFile`, not written into `bundle` by hand.\r\n //\r\n // This used to assign a hand-built record directly:\r\n //\r\n // bundle[manifestFileName] = { type: \"asset\", fileName, name, source } as any;\r\n //\r\n // and the `as any` was load-bearing, which was the warning sign. A Rollup\r\n // asset record carries `names` and `originalFileNames` ARRAYS; that object\r\n // had neither, so it was not the shape Rollup produces — it merely\r\n // type-asserted its way into the bundle.\r\n //\r\n // Nothing noticed until the production build got far enough to render\r\n // chunks, at which point Vite's own `vite:manifest` plugin read\r\n // `chunk.names.length` on every asset and died on `undefined`:\r\n //\r\n // [vite:manifest] Cannot read properties of undefined (reading 'length')\r\n //\r\n // `emitFile` makes Rollup construct the record, so the shape is correct by\r\n // construction and stays correct when Rollup adds fields. Hand-building a\r\n // bundle entry is signing up to track someone else's internal type forever.\r\n this.emitFile({\r\n type: \"asset\",\r\n fileName: manifestFileName,\r\n source: JSON.stringify(manifest, null, 2),\r\n });\r\n },\r\n };\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"gate-c-verify.mjs","names":[],"sources":["../../../../../../../web/src/vite/gate-c-verify.ts"],"sourcesContent":["/**\r\n * Gate C — emitted-output verification.\r\n *\r\n * Projection removes the six server exports before the client graph forms.\r\n * Gate A refuses forbidden import PATHS. Gate B refuses inline secret reads.\r\n * All three act BEFORE or DURING the build, on source or the module graph —\r\n * none of them ever looks at what actually came out the other end. Gate C\r\n * is that check: a build-time assertion on the EMITTED client bundle for\r\n * every page, verifying:\r\n *\r\n * 1. The emitted code contains none of the six server export names\r\n * (`route`, `middleware`, `validation`, `loader`, `metadata`, `prefix`) as a\r\n * top-level binding, exported or not — `findLeakedServerExports`.\r\n * 2. The emitted module graph (Rollup's `OutputBundle`, as seen in\r\n * `generateBundle`) contains no import edge into a\r\n * `warlock.environment: \"server\"` (or absent-defaulting-to-server)\r\n * package — re-derived from the exact classifier Gate A uses\r\n * (`gate-a-resolve.ts`'s `createEnvironmentClassifier`), never a second,\r\n * independently-drifting classification scheme — `findLeakedServerImportEdges`.\r\n *\r\n * This is defense in depth: if projection or Gate A has a bug that lets\r\n * something through, Gate C is what turns \"shipped a leak\" into \"build\r\n * fails.\" Both check functions are exported as PURE functions over a bundle\r\n * object precisely so a test can construct a bundle where projection/Gate A\r\n * are bypassed entirely (a hand-built leaked chunk, never produced by a real\r\n * pipeline run) and still prove Gate C catches it independently — re-running\r\n * the D.1-D.5 happy-path fixtures through the composed pipeline only proves\r\n * the happy path, never that Gate C itself has teeth.\r\n *\r\n * Gate C also emits the inlined-`PUBLIC_*`-value manifest — see\r\n * `buildPublicEnvManifest` below — and is the only\r\n * one of the three gates that runs exclusively at `generateBundle` time: it\r\n * has nothing to say about source, only about output.\r\n */\r\nimport { parse } from \"@babel/parser\";\r\nimport type { Plugin } from \"vite\";\r\nimport {\r\n createEnvironmentClassifier,\r\n type EnvironmentClassifier,\r\n type EnvironmentClassifierOptions,\r\n} from \"./gate-a-resolve\";\r\nimport { createPublicEnvTracker, type PublicEnvTracker } from \"./gate-b-secrets\";\r\nimport { SERVER_EXPORT_NAMES } from \"./projection\";\r\n\r\nexport interface ServerExportLeak {\r\n fileName: string;\r\n exportName: string;\r\n line?: number;\r\n}\r\n\r\nexport interface ServerImportEdgeLeak {\r\n fileName: string;\r\n moduleId: string;\r\n packageName: string;\r\n}\r\n\r\nexport interface PublicEnvManifestEntry {\r\n key: string;\r\n /** `null` when `redacted` is true — see `isSafeToShowValue`. */\r\n value: string | null;\r\n redacted: boolean;\r\n}\r\n\r\n/**\r\n * A structurally-minimal view of Rollup's `OutputBundle` — only the fields\r\n * Gate C actually reads. Deliberately NOT the real `OutputBundle`/`OutputChunk`\r\n * types: Part 1's own discipline requires proving Gate C catches a bundle\r\n * that a real pipeline could never produce (a hand-built leaked chunk), and\r\n * forcing every test fixture to satisfy Rollup's full chunk shape would\r\n * fight that requirement for no safety benefit — Vite's real `generateBundle`\r\n * hook still hands Gate C a real `OutputBundle`, which is a structural\r\n * superset of this.\r\n */\r\ntype BundleLike = Record<\r\n string,\r\n {\r\n type?: string;\r\n fileName?: string;\r\n code?: string;\r\n moduleIds?: readonly string[];\r\n }\r\n>;\r\n\r\n/**\r\n * Recursively collects every top-level statement's bound name(s), matching:\r\n * - `const route = ...` / `function loader() {}` (exported or not — the\r\n * check is on top-level BINDINGS, not top-level exports).\r\n * - `export const route = ...` / `export function loader() {}` (the\r\n * `ExportNamedDeclaration` wrapper form).\r\n * - `export { route }` (the bare re-export-specifier form Rollup sometimes\r\n * emits instead of inlining the declaration itself).\r\n */\r\nfunction collectServerExportNames(stmt: any): Array<{ name: string; line: number }> {\r\n const matches: Array<{ name: string; line: number }> = [];\r\n const line = stmt.loc?.start?.line as number | undefined;\r\n\r\n function record(name: string | undefined) {\r\n if (name && SERVER_EXPORT_NAMES.has(name)) matches.push({ name, line: line ?? 0 });\r\n }\r\n\r\n if (stmt.type === \"VariableDeclaration\") {\r\n for (const decl of stmt.declarations) {\r\n if (decl.id?.type === \"Identifier\") record(decl.id.name);\r\n }\r\n } else if (stmt.type === \"FunctionDeclaration\") {\r\n record(stmt.id?.name);\r\n } else if (stmt.type === \"ExportNamedDeclaration\") {\r\n if (stmt.declaration) matches.push(...collectServerExportNames(stmt.declaration));\r\n for (const specifier of stmt.specifiers ?? []) {\r\n record(specifier.exported?.name ?? specifier.exported?.value);\r\n }\r\n }\r\n\r\n return matches;\r\n}\r\n\r\n/**\r\n * Thrown when an emitted chunk cannot be parsed, which means Gate C could not\r\n * inspect it. A gate that skips what it cannot read reports \"no violation\r\n * found\" on precisely the input it failed to look at — the one input where\r\n * that answer is worthless. So an unreadable chunk fails the build instead.\r\n */\r\nexport class UnverifiableChunkError extends Error {\r\n readonly fileName: string;\r\n\r\n constructor(fileName: string, cause?: unknown) {\r\n super(\r\n [\r\n `Warlock stopped this build: a file in your client bundle could not be checked for server-only code.`,\r\n ``,\r\n `File: ${fileName}`,\r\n `Cause: this file could not be parsed as JavaScript, so the client/server boundary check could not be performed on it. Warlock cannot confirm that the server-only exports (route, middleware, validation, loader, metadata, prefix) were kept out of it.`,\r\n `Fix: the build is being stopped rather than passed, because a file that was never checked is not a file known to be safe. This emitted file is outside the JavaScript syntax Warlock can currently verify. Two things commonly put it there, and this error cannot tell which: the output uses syntax newer than the parser Warlock ships with, or a plugin or loader emitted non-standard syntax into the client bundle. Check the compatibility of whatever produced this file, then build again.`,\r\n ].join(\"\\n\"),\r\n );\r\n this.name = \"UnverifiableChunkError\";\r\n this.fileName = fileName;\r\n if (cause !== undefined) (this as { cause?: unknown }).cause = cause;\r\n }\r\n}\r\n\r\n/**\r\n * Part 1, item 1: parses each emitted chunk's ACTUAL code (never pre-transform\r\n * source) and looks for a top-level binding named one of the five server\r\n * exports. A parse failure on an emitted chunk FAILS the gate\r\n * (`UnverifiableChunkError`) — it is never skipped. Skipping would report the\r\n * bundle clean on the one chunk the gate did not actually inspect, which is a\r\n * safety check failing open; a build stopped on an unreadable chunk is\r\n * recoverable, a boundary silently unenforced is not.\r\n *\r\n * This SURVIVES real minification (pinned by the `gate-c-verify.spec.ts`\r\n * \"D.8\" describe block). A minifier (esbuild,\r\n * Vite's default) is only free to rename LOCAL bindings; the exported name\r\n * itself is part of the module's public interface and is never mangled — a\r\n * minified `export const route = ...` still emits `export { e as route }`,\r\n * with the local identifier renamed (`e`) but `route` intact as the specifier's\r\n * `exported` name, which is exactly the field `collectServerExportNames`\r\n * reads for the `ExportNamedDeclaration` specifier form. No name-independent\r\n * redesign needed here — verified against a real `build.minify: true` output,\r\n * not assumed.\r\n */\r\nexport function findLeakedServerExports(bundle: BundleLike): ServerExportLeak[] {\r\n const leaks: ServerExportLeak[] = [];\r\n\r\n for (const file of Object.values(bundle)) {\r\n if (!file || file.type !== \"chunk\" || typeof file.code !== \"string\") continue;\r\n\r\n let ast: any;\r\n try {\r\n ast = parse(file.code, { sourceType: \"module\", plugins: [\"typescript\", \"jsx\"] });\r\n } catch (error) {\r\n throw new UnverifiableChunkError(file.fileName ?? \"(unnamed chunk)\", error);\r\n }\r\n\r\n for (const stmt of ast.program.body as any[]) {\r\n for (const match of collectServerExportNames(stmt)) {\r\n leaks.push({ fileName: file.fileName as string, exportName: match.name, line: match.line });\r\n }\r\n }\r\n }\r\n\r\n return leaks;\r\n}\r\n\r\n/**\r\n * Part 1, item 2: walks every emitted chunk's `moduleIds` (the resolved,\r\n * absolute file paths of every source module Rollup actually bundled into\r\n * that chunk) and classifies each one via the SAME `EnvironmentClassifier`\r\n * Gate A's `resolveId` uses, re-derived from `gate-a-resolve.ts` rather than\r\n * hand-rolled a second time. A moduleId that maps to a governed-scope\r\n * package classified `\"server\"` is an import edge Gate A should already\r\n * have refused — if it's here anyway, the earlier gate has a bug.\r\n *\r\n * D.8 verdict: this SURVIVES real minification unconditionally, and for a\r\n * stronger reason than item 1 above — it never reads emitted code text or\r\n * identifier names at all. `moduleIds` is Rollup's own bundling metadata\r\n * (which resolved source files ended up in this chunk), untouched by\r\n * minification, which only rewrites code, not that metadata. Verified\r\n * against a real `build.minify: true` output in `gate-c-verify.spec.ts`.\r\n */\r\nexport function findLeakedServerImportEdges(\r\n bundle: BundleLike,\r\n classifier: Pick<EnvironmentClassifier, \"environmentOf\" | \"packageNameForFilePath\">,\r\n): ServerImportEdgeLeak[] {\r\n const leaks: ServerImportEdgeLeak[] = [];\r\n\r\n for (const file of Object.values(bundle)) {\r\n if (!file || file.type !== \"chunk\") continue;\r\n\r\n for (const moduleId of file.moduleIds ?? []) {\r\n const packageName = classifier.packageNameForFilePath(moduleId);\r\n if (!packageName) continue;\r\n if (classifier.environmentOf(packageName) === \"server\") {\r\n leaks.push({ fileName: file.fileName as string, moduleId, packageName });\r\n }\r\n }\r\n }\r\n\r\n return leaks;\r\n}\r\n\r\n/**\r\n * Conservative \"does this PUBLIC_* value look safe to print in the reviewable\r\n * manifest\" heuristic: if genuinely unsure, show the key only — never guess\r\n * that a value is safe. The whole point of this manifest is that the `PUBLIC_`\r\n * prefix rule alone is not trustworthy — someone can and will name an actual\r\n * secret `PUBLIC_STRIPE_KEY` — so a value that LOOKS like a credential is\r\n * redacted regardless of what its key is named. Non-string values (booleans,\r\n * numbers) are never secret-shaped and always shown.\r\n */\r\nconst CREDENTIALED_URL_RE = /:\\/\\/[^/\\s]+:[^/\\s@]+@/; // scheme://user:pass@host\r\nconst KNOWN_SECRET_PREFIX_RES = [\r\n /^sk_(live|test)_/i, // Stripe secret key (pk_ publishable is a different prefix)\r\n /^rk_(live|test)_/i, // Stripe restricted key\r\n /^AKIA[0-9A-Z]{12,}/, // AWS access key id\r\n /^gh[pousr]_[A-Za-z0-9]{20,}/, // GitHub personal/OAuth/user/server tokens\r\n /^glpat-[A-Za-z0-9_-]{20,}/, // GitLab personal access token\r\n /^xox[baprs]-[A-Za-z0-9-]{10,}/, // Slack tokens\r\n /^eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$/, // JWT (3 base64url segments)\r\n];\r\n// A long opaque token made up only of base64url/hex-ish characters, with both\r\n// letters and digits, and no separators a human-authored public value (a\r\n// URL, a short id) would normally contain — conservatively treated as a\r\n// secret-shaped random token even though it might genuinely be safe.\r\nconst HIGH_ENTROPY_TOKEN_RE = /^[A-Za-z0-9+/_=-]{32,}$/;\r\n\r\nfunction isSafeToShowValue(value: unknown): boolean {\r\n if (typeof value !== \"string\" || value.length === 0) return true;\r\n if (CREDENTIALED_URL_RE.test(value)) return false;\r\n if (KNOWN_SECRET_PREFIX_RES.some((re) => re.test(value))) return false;\r\n if (HIGH_ENTROPY_TOKEN_RE.test(value) && /[0-9]/.test(value) && /[a-zA-Z]/.test(value)) return false;\r\n return true;\r\n}\r\n\r\nfunction stringifyEnvValue(value: unknown): string {\r\n if (typeof value === \"string\") return value;\r\n return JSON.stringify(value) ?? \"undefined\";\r\n}\r\n\r\n/**\r\n * The enumerated, human-reviewable list of\r\n * every `PUBLIC_*` key actually inlined into the client bundle. Built\r\n * directly from `tracker.referencedKeys` — the exact same `Set` Gate B's own\r\n * `generateBundle` unread-key check (`gate-b-secrets.ts`) reads from — so\r\n * this manifest and that exclusion logic agree BY CONSTRUCTION: they are two\r\n * readers of one Set, not two independent recomputations of \"which keys were\r\n * read\" that could drift apart. A key only reaches `referencedKeys` once\r\n * `findViolation` in `gate-b-secrets.ts` has seen a statically-resolved\r\n * `import.meta.env.PUBLIC_X` read for it, which is also precisely the\r\n * condition under which Gate B allows its value to be inlined at all — an\r\n * unread key that somehow leaked in anyway is a Gate B `generateBundle`\r\n * BUILD FAILURE (see `gate-b-secrets.ts`), never silently listed here.\r\n */\r\nexport function buildPublicEnvManifest(tracker: PublicEnvTracker): PublicEnvManifestEntry[] {\r\n return [...tracker.referencedKeys]\r\n .sort()\r\n .map((key) => {\r\n const value = tracker.declaredEnv[key];\r\n const safe = isSafeToShowValue(value);\r\n return { key, value: safe ? stringifyEnvValue(value) : null, redacted: !safe };\r\n });\r\n}\r\n\r\nexport interface GateCOptions extends EnvironmentClassifierOptions {\r\n /**\r\n * Shared with `gateBSecrets({ tracker })` — required for the manifest to\r\n * reflect what THIS build's Gate B pass actually saw. Defaults to a\r\n * private, unshared tracker (always empty) if omitted, which is only ever\r\n * correct when Gate C runs standalone in a test.\r\n */\r\n tracker?: PublicEnvTracker;\r\n /** Defaults to `\"warlock-env-manifest.json\"` (Suki's suggested name). */\r\n manifestFileName?: string;\r\n}\r\n\r\n/**\r\n * The client-build Vite plugin. Runs only at `generateBundle` — Gate C has\r\n * nothing to say about source, only about the bundle Rollup actually wrote.\r\n * Skipped for the SSR/server build, same as Gate B (`gate-b-secrets.ts`):\r\n * a page's server exports are meant to survive in that build; only the\r\n * client build is judged.\r\n */\r\nexport function gateCVerify(options: GateCOptions = {}): Plugin {\r\n const classifier = createEnvironmentClassifier(options);\r\n const tracker = options.tracker ?? createPublicEnvTracker();\r\n const manifestFileName = options.manifestFileName ?? \"warlock-env-manifest.json\";\r\n\r\n return {\r\n name: \"warlock:gate-c-verify\",\r\n generateBundle(_outputOptions, bundle) {\r\n if (this.environment?.config?.consumer === \"server\") return;\r\n\r\n const exportLeak = findLeakedServerExports(bundle)[0];\r\n if (exportLeak) {\r\n this.error(\r\n [\r\n `Gate C refused a build: a server export survived into the emitted client bundle.`,\r\n ``,\r\n `File: ${exportLeak.fileName}${exportLeak.line ? `:${exportLeak.line}` : \"\"}`,\r\n `Export: ${exportLeak.exportName}`,\r\n `Cause: \"${exportLeak.exportName}\" is one of the six server exports (route, middleware, validation, loader, metadata, prefix) and is still present as a top-level binding in the EMITTED client chunk — projection and/or Gate A should have removed or refused it before the bundle was written.`,\r\n `Fix: this should already be impossible if projection and Gate A ran correctly — investigate why \"${exportLeak.exportName}\" reached the emitted output (a projection bug, a build config that bypasses these plugins, or a plugin ordering change) rather than assuming this build is a one-off; Gate C is defense in depth, not the primary fence.`,\r\n ].join(\"\\n\"),\r\n );\r\n }\r\n\r\n const importEdgeLeak = findLeakedServerImportEdges(bundle, classifier)[0];\r\n if (importEdgeLeak) {\r\n this.error(\r\n [\r\n `Gate C refused a build: a server-only import edge survived into the emitted client bundle's module graph.`,\r\n ``,\r\n `File: ${importEdgeLeak.fileName}`,\r\n `Module: ${importEdgeLeak.moduleId}`,\r\n `Cause: \"${importEdgeLeak.moduleId}\" belongs to ${importEdgeLeak.packageName}, a server-only package (it declares \"warlock\": { \"environment\": \"server\" } in its package.json), and is present among the bundled modules of the emitted client chunk \"${importEdgeLeak.fileName}\" — Gate A's resolveId should have refused this import before it ever reached the bundle.`,\r\n `Fix: this should already be impossible if Gate A ran on this build — investigate why ${importEdgeLeak.packageName} reached the emitted output (a Gate A bypass, a custom resolveId/external override, or a plugin ordering change) rather than assuming this build is a one-off; Gate C is defense in depth, not the primary fence.`,\r\n ].join(\"\\n\"),\r\n );\r\n }\r\n\r\n const manifest = buildPublicEnvManifest(tracker);\r\n\r\n // EMITTED through `this.emitFile`, not written into `bundle` by hand.\r\n //\r\n // This used to assign a hand-built record directly:\r\n //\r\n // bundle[manifestFileName] = { type: \"asset\", fileName, name, source } as any;\r\n //\r\n // and the `as any` was load-bearing, which was the warning sign. A Rollup\r\n // asset record carries `names` and `originalFileNames` ARRAYS; that object\r\n // had neither, so it was not the shape Rollup produces — it merely\r\n // type-asserted its way into the bundle.\r\n //\r\n // Nothing noticed until the production build got far enough to render\r\n // chunks, at which point Vite's own `vite:manifest` plugin read\r\n // `chunk.names.length` on every asset and died on `undefined`:\r\n //\r\n // [vite:manifest] Cannot read properties of undefined (reading 'length')\r\n //\r\n // `emitFile` makes Rollup construct the record, so the shape is correct by\r\n // construction and stays correct when Rollup adds fields. Hand-building a\r\n // bundle entry is signing up to track someone else's internal type forever.\r\n this.emitFile({\r\n type: \"asset\",\r\n fileName: manifestFileName,\r\n source: JSON.stringify(manifest, null, 2),\r\n });\r\n },\r\n };\r\n}\r\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- //#region web/src/vite/hydration-entries.d.ts
1
+ //#region ../web/src/vite/hydration-entries.d.ts
2
2
  /** Stable Rollup/Vite entry name shared by development and production wiring. */
3
3
  declare const HYDRATION_CLIENT_ENTRY_NAME = "hydration";
4
4
  type HydrationClientEntry = Readonly<{
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
- //#region web/src/vite/hydration-entries.ts
4
+ //#region ../web/src/vite/hydration-entries.ts
5
5
  /** Stable Rollup/Vite entry name shared by development and production wiring. */
6
6
  const HYDRATION_CLIENT_ENTRY_NAME = "hydration";
7
7
  function normalizeFileSystemPath(filePath) {
@@ -1 +1 @@
1
- {"version":3,"file":"hydration-entries.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/vite/hydration-entries.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport path from \"node:path\";\n\n/** Stable Rollup/Vite entry name shared by development and production wiring. */\nexport const HYDRATION_CLIENT_ENTRY_NAME = \"hydration\";\n\nexport type HydrationClientEntry = Readonly<{\n name: typeof HYDRATION_CLIENT_ENTRY_NAME;\n sourcePath: string;\n devUrl: string;\n}>;\n\nfunction normalizeFileSystemPath(filePath: string): string {\n return filePath.replace(/\\\\/g, \"/\");\n}\n\n/**\n * Where the hydration entry lives inside an INSTALLED `@warlock.js/web`, and\n * where it lives inside this checkout — in that order of preference.\n *\n * The published tarball ships `esm/`, `skills/` and the docs files — no `src/`\n * — so `src/` is absent from every real install. (Verified against\n * `@warlock.js/web@5.0.2`: 191 files, none under `src/`. Note the published\n * `package.json` carries no `files` key at all; the release tooling rewrites\n * the manifest, so do not treat this repo's `files` field as the mechanism.)\n * Resolving the entry to `<webRoot>/src/hydration/index.ts` unconditionally\n * therefore worked in this monorepo and failed for every consumer, with\n * `warlock build` unable to emit a client bundle at all.\n *\n * The built artifact is preferred rather than the source being published,\n * because shipping `src/` beside `esm/` would put TWO instances of\n * `routing/route-table` in one client bundle — the app's own imports resolve\n * through `esm/`, the hydration entry's relative imports through `src/`.\n * `publishRouteTable()` would write to one and `<Link>` would read the other.\n */\nconst PACKAGED_ENTRY = \"esm/hydration/index.mjs\";\nconst CHECKOUT_ENTRY = \"src/hydration/index.ts\";\n\n/**\n * Describes the single framework hydration entry without importing Vite.\n * Vite's `/@fs/` prefix accepts an absolute normalized file-system path;\n * keeping the drive colon produces `/@fs/D:/...` consistently on Windows.\n */\nexport function createHydrationClientEntry(webRoot: string): HydrationClientEntry {\n if (typeof webRoot !== \"string\" || webRoot.trim().length === 0) {\n throw new TypeError(\"Cannot create the hydration client entry: webRoot must be a non-empty path.\");\n }\n\n const packagedPath = path.resolve(webRoot, PACKAGED_ENTRY);\n const sourcePath = normalizeFileSystemPath(\n existsSync(packagedPath) ? packagedPath : path.resolve(webRoot, CHECKOUT_ENTRY),\n );\n\n return {\n name: HYDRATION_CLIENT_ENTRY_NAME,\n sourcePath,\n devUrl: `/@fs/${sourcePath}`,\n };\n}\n"],"mappings":";;;;;AAIA,MAAa,8BAA8B;AAQ3C,SAAS,wBAAwB,UAA0B;CACzD,OAAO,SAAS,QAAQ,OAAO,GAAG;AACpC;;;;;;;;;;;;;;;;;;;;AAqBA,MAAM,iBAAiB;AACvB,MAAM,iBAAiB;;;;;;AAOvB,SAAgB,2BAA2B,SAAuC;CAChF,IAAI,OAAO,YAAY,YAAY,QAAQ,KAAK,EAAE,WAAW,GAC3D,MAAM,IAAI,UAAU,6EAA6E;CAGnG,MAAM,eAAe,KAAK,QAAQ,SAAS,cAAc;CACzD,MAAM,aAAa,wBACjB,WAAW,YAAY,IAAI,eAAe,KAAK,QAAQ,SAAS,cAAc,CAChF;CAEA,OAAO;EACL,MAAM;EACN;EACA,QAAQ,QAAQ;CAClB;AACF"}
1
+ {"version":3,"file":"hydration-entries.mjs","names":[],"sources":["../../../../../../../web/src/vite/hydration-entries.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport path from \"node:path\";\n\n/** Stable Rollup/Vite entry name shared by development and production wiring. */\nexport const HYDRATION_CLIENT_ENTRY_NAME = \"hydration\";\n\nexport type HydrationClientEntry = Readonly<{\n name: typeof HYDRATION_CLIENT_ENTRY_NAME;\n sourcePath: string;\n devUrl: string;\n}>;\n\nfunction normalizeFileSystemPath(filePath: string): string {\n return filePath.replace(/\\\\/g, \"/\");\n}\n\n/**\n * Where the hydration entry lives inside an INSTALLED `@warlock.js/web`, and\n * where it lives inside this checkout — in that order of preference.\n *\n * The published tarball ships `esm/`, `skills/` and the docs files — no `src/`\n * — so `src/` is absent from every real install. (Verified against\n * `@warlock.js/web@5.0.2`: 191 files, none under `src/`. Note the published\n * `package.json` carries no `files` key at all; the release tooling rewrites\n * the manifest, so do not treat this repo's `files` field as the mechanism.)\n * Resolving the entry to `<webRoot>/src/hydration/index.ts` unconditionally\n * therefore worked in this monorepo and failed for every consumer, with\n * `warlock build` unable to emit a client bundle at all.\n *\n * The built artifact is preferred rather than the source being published,\n * because shipping `src/` beside `esm/` would put TWO instances of\n * `routing/route-table` in one client bundle — the app's own imports resolve\n * through `esm/`, the hydration entry's relative imports through `src/`.\n * `publishRouteTable()` would write to one and `<Link>` would read the other.\n */\nconst PACKAGED_ENTRY = \"esm/hydration/index.mjs\";\nconst CHECKOUT_ENTRY = \"src/hydration/index.ts\";\n\n/**\n * Describes the single framework hydration entry without importing Vite.\n * Vite's `/@fs/` prefix accepts an absolute normalized file-system path;\n * keeping the drive colon produces `/@fs/D:/...` consistently on Windows.\n */\nexport function createHydrationClientEntry(webRoot: string): HydrationClientEntry {\n if (typeof webRoot !== \"string\" || webRoot.trim().length === 0) {\n throw new TypeError(\"Cannot create the hydration client entry: webRoot must be a non-empty path.\");\n }\n\n const packagedPath = path.resolve(webRoot, PACKAGED_ENTRY);\n const sourcePath = normalizeFileSystemPath(\n existsSync(packagedPath) ? packagedPath : path.resolve(webRoot, CHECKOUT_ENTRY),\n );\n\n return {\n name: HYDRATION_CLIENT_ENTRY_NAME,\n sourcePath,\n devUrl: `/@fs/${sourcePath}`,\n };\n}\n"],"mappings":";;;;;AAIA,MAAa,8BAA8B;AAQ3C,SAAS,wBAAwB,UAA0B;CACzD,OAAO,SAAS,QAAQ,OAAO,GAAG;AACpC;;;;;;;;;;;;;;;;;;;;AAqBA,MAAM,iBAAiB;AACvB,MAAM,iBAAiB;;;;;;AAOvB,SAAgB,2BAA2B,SAAuC;CAChF,IAAI,OAAO,YAAY,YAAY,QAAQ,KAAK,EAAE,WAAW,GAC3D,MAAM,IAAI,UAAU,6EAA6E;CAGnG,MAAM,eAAe,KAAK,QAAQ,SAAS,cAAc;CACzD,MAAM,aAAa,wBACjB,WAAW,YAAY,IAAI,eAAe,KAAK,QAAQ,SAAS,cAAc,CAChF;CAEA,OAAO;EACL,MAAM;EACN;EACA,QAAQ,QAAQ;CAClB;AACF"}
@@ -7,7 +7,7 @@ import { GateCOptions, PublicEnvManifestEntry, ServerExportLeak, ServerImportEdg
7
7
  import { ProjectionAmbiguityError, ProjectionResult, projection } from "./projection.mjs";
8
8
  import { Plugin } from "vite";
9
9
 
10
- //#region web/src/vite/index.d.ts
10
+ //#region ../web/src/vite/index.d.ts
11
11
  type WarlockClientBoundaryOptions = Parameters<typeof gateAResolve>[0] & {
12
12
  beforePageHotUpdate?: ClientPageRegistryPluginOptions["beforePageHotUpdate"];
13
13
  };
@@ -8,7 +8,7 @@ import { CLIENT_PAGE_REGISTRY_ID, RESOLVED_CLIENT_PAGE_REGISTRY_ID, clientPageRe
8
8
  import path from "node:path";
9
9
  import { parse } from "@babel/parser";
10
10
 
11
- //#region web/src/vite/index.ts
11
+ //#region ../web/src/vite/index.ts
12
12
  /**
13
13
  * `@warlock.js/web/vite` — build-tooling subpath, kept separate from the
14
14
  * runtime barrel (`@warlock.js/web`) so importing it never pulls `vite` into
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/vite/index.ts"],"sourcesContent":["/**\n * `@warlock.js/web/vite` — build-tooling subpath, kept separate from the\n * runtime barrel (`@warlock.js/web`) so importing it never pulls `vite` into\n * a project that doesn't build with Vite.\n */\nimport { parse } from \"@babel/parser\";\nimport path from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport {\n buildHydrationClient,\n type BuildHydrationClientOptions,\n type BuildHydrationClientResult,\n} from \"./build-client\";\nimport {\n gateAResolve,\n isAppSourcePath,\n isRecognizedUniversalSurface,\n isServerFile,\n isWithinModuleWebFolder,\n} from \"./gate-a-resolve\";\nimport { createPublicEnvTracker, gateBSecrets } from \"./gate-b-secrets\";\nimport { gateCVerify } from \"./gate-c-verify\";\nimport {\n clientPageRegistry,\n type ClientPageRegistryPluginOptions,\n} from \"./page-registry-plugin\";\nimport { isProjectableFile, projectModule, projection } from \"./projection\";\n\nexport { buildHydrationClient } from \"./build-client\";\nexport type {\n BuildHydrationClientOptions,\n BuildHydrationClientResult,\n HydrationClientBuildOutput,\n} from \"./build-client\";\nexport { gateAResolve } from \"./gate-a-resolve\";\nexport type {\n EnvironmentClassifier,\n EnvironmentClassifierOptions,\n WarlockEnvironment,\n} from \"./gate-a-resolve\";\nexport { createPublicEnvTracker, gateBSecrets } from \"./gate-b-secrets\";\nexport type { PublicEnvTracker } from \"./gate-b-secrets\";\nexport {\n buildPublicEnvManifest,\n findLeakedServerExports,\n findLeakedServerImportEdges,\n gateCVerify,\n} from \"./gate-c-verify\";\nexport type {\n GateCOptions,\n PublicEnvManifestEntry,\n ServerExportLeak,\n ServerImportEdgeLeak,\n} from \"./gate-c-verify\";\nexport {\n HYDRATION_CLIENT_ENTRY_NAME,\n createHydrationClientEntry,\n} from \"./hydration-entries\";\nexport type { HydrationClientEntry } from \"./hydration-entries\";\nexport {\n CLIENT_PAGE_REGISTRY_ID,\n clientPageRegistry,\n invalidateClientPageRegistry,\n RESOLVED_CLIENT_PAGE_REGISTRY_ID,\n} from \"./page-registry-plugin\";\nexport type { ClientPageRegistryPluginOptions } from \"./page-registry-plugin\";\nexport { projection, ProjectionAmbiguityError } from \"./projection\";\nexport type { ProjectionResult } from \"./projection\";\n\nexport type WarlockClientBoundaryOptions = Parameters<\n typeof gateAResolve\n>[0] & {\n beforePageHotUpdate?: ClientPageRegistryPluginOptions[\"beforePageHotUpdate\"];\n};\n\nexport type BuildWarlockHydrationClientOptions = Readonly<{\n appRoot: string;\n webRoot: string;\n /** Absolute client output dir — threaded to `buildHydrationClient` (`<outdir>/client`). */\n outDir: string;\n resolveAliases: BuildHydrationClientOptions[\"resolveAliases\"];\n external?: BuildHydrationClientOptions[\"external\"];\n /** App-configured plugins, appended after Warlock's client-boundary pipeline. */\n plugins?: BuildHydrationClientOptions[\"plugins\"];\n}>;\n\ntype SsrBoundaryState = {\n readonly appRoot: string;\n readonly clientBoundModules: Set<string>;\n readonly clientImportsByModule: Map<string, Set<string>>;\n};\n\nfunction moduleKey(id: string): string {\n return id.split(\"?\")[0].replace(/\\\\/g, \"/\");\n}\n\nconst CODE_MODULE_EXTENSION = /\\.([cm]?[jt]sx?)$/;\n\nfunction isStatelessClientSurface(id: string, appRoot: string): boolean {\n const bare = moduleKey(id);\n if (!isAppSourcePath(bare, appRoot)) return false;\n if (isServerFile(bare, appRoot)) return false;\n\n return (\n isProjectableFile(bare) ||\n isRecognizedUniversalSurface(bare) ||\n isWithinModuleWebFolder(bare, appRoot)\n );\n}\n\nfunction collectImportSpecifiers(code: string): Set<string> {\n const ast = parse(code, {\n sourceType: \"module\",\n plugins: [\"typescript\", \"jsx\"],\n });\n const imports = new Set<string>();\n\n function walk(node: unknown): void {\n if (!node || typeof node !== \"object\") return;\n if (Array.isArray(node)) {\n for (const child of node) walk(child);\n return;\n }\n\n const record = node as Record<string, any>;\n if (\n (record.type === \"ImportDeclaration\" ||\n record.type === \"ExportNamedDeclaration\" ||\n record.type === \"ExportAllDeclaration\") &&\n record.source?.type === \"StringLiteral\"\n ) {\n imports.add(record.source.value);\n }\n if (\n record.type === \"CallExpression\" &&\n record.callee?.type === \"Import\" &&\n record.arguments?.[0]?.type === \"StringLiteral\"\n ) {\n imports.add(record.arguments[0].value);\n }\n if (\n record.type === \"ImportExpression\" &&\n record.source?.type === \"StringLiteral\"\n ) {\n imports.add(record.source.value);\n }\n\n for (const [key, child] of Object.entries(record)) {\n if (\n key === \"type\" ||\n key === \"start\" ||\n key === \"end\" ||\n key === \"loc\" ||\n key === \"range\" ||\n key.endsWith(\"Comments\") ||\n key === \"extra\"\n ) {\n continue;\n }\n walk(child);\n }\n }\n\n walk(ast.program);\n return imports;\n}\n\nfunction clientViewOf(\n state: SsrBoundaryState,\n code: string,\n id: string,\n): string | undefined {\n const key = moduleKey(id);\n if (\n !state.clientBoundModules.has(key) &&\n !isStatelessClientSurface(key, state.appRoot)\n ) {\n return undefined;\n }\n\n state.clientBoundModules.add(key);\n if (!CODE_MODULE_EXTENSION.test(key)) {\n state.clientImportsByModule.set(key, new Set());\n return code;\n }\n\n const clientCode = isProjectableFile(key)\n ? projectModule(code, key).code\n : code;\n state.clientImportsByModule.set(key, collectImportSpecifiers(clientCode));\n return clientCode;\n}\n\nfunction markResolvedClientModule(\n state: SsrBoundaryState,\n resolved: { id: string } | string | null | false | void,\n): void {\n if (!resolved) return;\n const id = typeof resolved === \"string\" ? resolved : resolved.id;\n if (!id.includes(\"\\0\")) state.clientBoundModules.add(moduleKey(id));\n}\n\nfunction isServerEnvironment(context: {\n environment?: { config: { consumer?: string } };\n}): boolean {\n return context.environment?.config.consumer === \"server\";\n}\n\n/**\n * Keeps the production/client pipeline byte-identical, while giving Gate A\n * and Gate B a validation-only view in Vite's development SSR environment.\n * SSR still evaluates the original source: only the gates receive the\n * projected client view, so loader/server exports retain their legitimate\n * server access while component-visible code is refused before evaluation.\n */\nfunction clientEnvironmentOnly(\n plugin: Plugin,\n ssrState: SsrBoundaryState,\n): Plugin {\n const validatesDevSsr =\n plugin.name === \"warlock:gate-a-resolve\" ||\n plugin.name === \"warlock:gate-b-secrets\";\n const originalTransform =\n typeof plugin.transform === \"function\"\n ? plugin.transform\n : plugin.transform?.handler;\n const originalResolveId =\n typeof plugin.resolveId === \"function\"\n ? plugin.resolveId\n : plugin.resolveId?.handler;\n const originalBuildStart =\n typeof plugin.buildStart === \"function\"\n ? plugin.buildStart\n : plugin.buildStart?.handler;\n\n return {\n ...plugin,\n applyToEnvironment(environment) {\n return (\n environment.config.consumer === \"client\" ||\n (validatesDevSsr && environment.config.consumer === \"server\")\n );\n },\n buildStart: originalBuildStart\n ? function (...args) {\n if (isServerEnvironment(this)) return;\n return originalBuildStart.apply(this, args);\n }\n : undefined,\n transform: originalTransform\n ? async function (code, id, options) {\n if (!isServerEnvironment(this)) {\n return originalTransform.call(this, code, id, options);\n }\n\n const clientCode = clientViewOf(ssrState, code, id);\n if (clientCode === undefined) return null;\n\n const transformed = await originalTransform.call(\n this,\n clientCode,\n id,\n {\n ...options,\n ssr: false,\n },\n );\n\n // Vite can externalize a package in the SSR environment before its\n // normal resolver walk offers that edge to a plugin. Gate A cannot\n // wait for that walk: validate every import that survived projection\n // now, while the original TypeScript source and importer are known.\n if (plugin.name === \"warlock:gate-a-resolve\" && originalResolveId) {\n for (const source of ssrState.clientImportsByModule.get(\n moduleKey(id),\n ) ?? []) {\n const resolved = await originalResolveId.call(this, source, id, {\n attributes: {},\n isEntry: false,\n ssr: false,\n });\n markResolvedClientModule(ssrState, resolved);\n }\n }\n\n return transformed;\n }\n : undefined,\n resolveId: originalResolveId\n ? async function (source, importer, options) {\n if (!isServerEnvironment(this)) {\n return originalResolveId.call(this, source, importer, options);\n }\n\n if (!importer) return null;\n const importerKey = moduleKey(importer);\n const isClientBound =\n ssrState.clientBoundModules.has(importerKey) ||\n isStatelessClientSurface(importerKey, ssrState.appRoot);\n if (!isClientBound) return null;\n\n const survivingImports =\n ssrState.clientImportsByModule.get(importerKey);\n if (survivingImports && !survivingImports.has(source)) return null;\n\n const resolved = await originalResolveId.call(\n this,\n source,\n importer,\n {\n ...options,\n ssr: false,\n },\n );\n markResolvedClientModule(ssrState, resolved);\n return resolved;\n }\n : undefined,\n };\n}\n\n/**\n * The composed client-build pipeline: projection\n * strips the 5 server exports first, THEN Gate B's `transform` checks\n * whatever source remains for inline secret reads, THEN Gate A's\n * `resolveId` judges whatever imports remain. Array order here is\n * `[projection(), gateBSecrets(), gateAResolve()]` to match Vite's own\n * pipeline shape (`transform` before `resolveId`), but array order alone\n * does not guarantee this — see the hook-ordering fact below, which is what\n * actually makes the composition correct.\n *\n * Empirically observed fact (via an instrumented real `vite.build()`, not\n * assumed from plugin array order): for a given module M, Vite/Rollup calls\n * `transform(M)` BEFORE it calls `resolveId` for any of M's own import\n * specifiers — because Rollup must parse M's post-transform source to even\n * discover which specifiers to resolve next. Concretely: `transform` ran on\n * `entry.page.tsx` first, and only after that did `resolveId(\"./dep\", ...)`\n * fire for the import statement still present in the transformed code. A\n * consequence follows directly: if projection's `transform` removes an\n * import statement from a page module entirely (e.g. `loader`'s\n * `@warlock.js/core` import, stripped because `loader` itself is removed),\n * `resolveId` is never invoked for that specifier at all — Gate A doesn't\n * \"let it pass\", it never sees it. Gate A's `resolveId` only fires for\n * imports that survive projection's `transform`, which is exactly why a\n * component-level `@warlock.js/core` import (never touched by projection)\n * still reaches and is refused by Gate A.\n *\n * This is OBSERVED Rollup behavior, not a documented contract — a future\n * Vite/Rollup upgrade could invert it. Pinned by a\n * regression test (`index.spec.ts`, \"D.3 hook ordering pin\") that fails\n * loudly if the ordering ever inverts, and by the `vite` peer floor in\n * `web/package.json` (`>=7.3.5`, the version this was verified against). If\n * that test ever fails after a Vite bump: the failure mode of the ordering\n * assumption breaking is SAFE — projection would stop removing an import\n * statement Gate A still sees, so Gate A would refuse an import it used to\n * silently let a stripped server export take with it. That is a loud build\n * failure (\"Gate A refused an import\"), never a silent client-bundle leak.\n * Do NOT \"fix\" an apparent Gate A false-positive after a Vite upgrade by\n * weakening Gate A (e.g. widening what it lets through) — investigate\n * whether this ordering assumption broke instead; loosening Gate A to work\n * around it would turn a loud failure into the exact silent leak this\n * pipeline exists to prevent.\n *\n * Gate B is placed between the two for the same reason, but for a `transform`\n * hook rather than `resolveId`: within a plugin array, Rollup runs each\n * module's registered `transform` hooks in array order, each one receiving\n * the PREVIOUS plugin's output. Running Gate B after projection means it\n * inspects the POST-projection source — a `process.env.SECRET` read inside\n * `loader` (a server export, legitimately reading a real secret server-side)\n * is invisible to Gate B once projection has already removed `loader`\n * entirely, exactly as it should be: Gate B's job is to fence client-bound\n * code, and projection is what decides what counts as client-bound. A\n * component-level secret read is untouched by projection and still reaches\n * Gate B, which refuses it. Gate B does not depend on Gate A's `resolveId`\n * output at all (orthogonal concern, raw source vs. import paths), so its\n * position relative to Gate A is not load-bearing — it is placed before Gate\n * A only to keep both `transform` hooks adjacent in the array.\n *\n * Gate C (`gate-c-verify.ts`) runs last and only at `generateBundle` — after\n * the entire `transform`/`resolveId` build phase has completed for every\n * plugin, regardless of array position (a Rollup lifecycle fact, not\n * something this array order enforces). It verifies the EMITTED output the\n * other three produced: no server export survived as a top-level binding, no\n * import edge into a server-only package survived into the module graph, and\n * it emits the reviewable `PUBLIC_*` inlined-value manifest. `gateBSecrets` and `gateCVerify` share\n * one `PublicEnvTracker` instance so the manifest and Gate B's own unread-key\n * exclusion check agree by construction, not by coincidence.\n *\n * `clientPageRegistry()` is FIRST, ahead of projection. It contributes no\n * `transform` at all — only a `resolveId`/`load` pair for one synthetic id —\n * so it cannot displace or pre-empt any gate's inspection of any real file.\n * Two reasons for the position, one of which is not load-bearing and is\n * labelled as such:\n *\n * 1. Load-bearing: it must own `virtual:warlock/pages` before Gate A's\n * `resolveId` (also `enforce: \"pre\"`) reaches its `this.resolve(...)` call\n * for that specifier. Gate A's nested resolve would find it anyway, but\n * routing the id through Gate A's importer-chain bookkeeping only to have\n * it come back means a synthetic id can surface in a user-facing \"Import\n * chain:\" message. Resolving it first keeps ownership of the id in one\n * place.\n * 2. NOT load-bearing: the position relative to `projection()`. Projection is\n * `enforce: \"pre\"` and selects by file BASENAME (`projection.ts:242-248`),\n * so it transforms every `*.page.tsx` / `layout.tsx` / `root.tsx` that\n * enters the graph regardless of who imported it or where this plugin sits.\n * The registry emits absolute POSIX specifiers that Rollup resolves and\n * loads as ORDINARY file modules — they are not inlined into the virtual\n * module — so each one is transformed exactly as a page imported from a\n * real file would be. Projection declines the virtual module itself\n * (`\\0virtual:warlock/pages` has no matching basename), which is correct:\n * generated code has no server exports to strip.\n *\n * Point 2 is asserted, not assumed, by a real `vite.build()` in\n * `page-registry-plugin.spec.ts`: a fixture page whose `loader` — and only its\n * `loader` — imports a marker module that Gate A independently PERMITS, built\n * through this exact array, with the marker proven absent from every emitted\n * chunk while the page's own component text is proven present. Inspecting this\n * array's order would prove nothing about what reaches the browser.\n */\nexport function warlockClientBoundary(\n options: WarlockClientBoundaryOptions = {},\n): Plugin[] {\n const tracker = createPublicEnvTracker();\n const ssrState: SsrBoundaryState = {\n appRoot: path.resolve(options.appRoot ?? process.cwd()),\n clientBoundModules: new Set(),\n clientImportsByModule: new Map(),\n };\n return [\n clientPageRegistry({\n appRoot: options.appRoot,\n beforePageHotUpdate: options.beforePageHotUpdate,\n }),\n projection(),\n gateBSecrets({ tracker }),\n gateAResolve(options),\n gateCVerify({ ...options, tracker }),\n ].map((plugin) => clientEnvironmentOnly(plugin, ssrState));\n}\n\n/**\n * Callable production seam: callers own their exact source aliases and the\n * app-root classification boundary, while this module owns the one canonical\n * projection/Gate B/Gate A/Gate C composition.\n */\nexport async function buildWarlockHydrationClient(\n options: BuildWarlockHydrationClientOptions,\n): Promise<BuildHydrationClientResult> {\n return buildHydrationClient({\n webRoot: options.webRoot,\n outDir: options.outDir,\n resolveAliases: options.resolveAliases,\n external: options.external,\n plugins: [\n ...warlockClientBoundary({ appRoot: options.appRoot }),\n ...(options.plugins ?? []),\n ],\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../web/src/vite/index.ts"],"sourcesContent":["/**\n * `@warlock.js/web/vite` — build-tooling subpath, kept separate from the\n * runtime barrel (`@warlock.js/web`) so importing it never pulls `vite` into\n * a project that doesn't build with Vite.\n */\nimport { parse } from \"@babel/parser\";\nimport path from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport {\n buildHydrationClient,\n type BuildHydrationClientOptions,\n type BuildHydrationClientResult,\n} from \"./build-client\";\nimport {\n gateAResolve,\n isAppSourcePath,\n isRecognizedUniversalSurface,\n isServerFile,\n isWithinModuleWebFolder,\n} from \"./gate-a-resolve\";\nimport { createPublicEnvTracker, gateBSecrets } from \"./gate-b-secrets\";\nimport { gateCVerify } from \"./gate-c-verify\";\nimport {\n clientPageRegistry,\n type ClientPageRegistryPluginOptions,\n} from \"./page-registry-plugin\";\nimport { isProjectableFile, projectModule, projection } from \"./projection\";\n\nexport { buildHydrationClient } from \"./build-client\";\nexport type {\n BuildHydrationClientOptions,\n BuildHydrationClientResult,\n HydrationClientBuildOutput,\n} from \"./build-client\";\nexport { gateAResolve } from \"./gate-a-resolve\";\nexport type {\n EnvironmentClassifier,\n EnvironmentClassifierOptions,\n WarlockEnvironment,\n} from \"./gate-a-resolve\";\nexport { createPublicEnvTracker, gateBSecrets } from \"./gate-b-secrets\";\nexport type { PublicEnvTracker } from \"./gate-b-secrets\";\nexport {\n buildPublicEnvManifest,\n findLeakedServerExports,\n findLeakedServerImportEdges,\n gateCVerify,\n} from \"./gate-c-verify\";\nexport type {\n GateCOptions,\n PublicEnvManifestEntry,\n ServerExportLeak,\n ServerImportEdgeLeak,\n} from \"./gate-c-verify\";\nexport {\n HYDRATION_CLIENT_ENTRY_NAME,\n createHydrationClientEntry,\n} from \"./hydration-entries\";\nexport type { HydrationClientEntry } from \"./hydration-entries\";\nexport {\n CLIENT_PAGE_REGISTRY_ID,\n clientPageRegistry,\n invalidateClientPageRegistry,\n RESOLVED_CLIENT_PAGE_REGISTRY_ID,\n} from \"./page-registry-plugin\";\nexport type { ClientPageRegistryPluginOptions } from \"./page-registry-plugin\";\nexport { projection, ProjectionAmbiguityError } from \"./projection\";\nexport type { ProjectionResult } from \"./projection\";\n\nexport type WarlockClientBoundaryOptions = Parameters<\n typeof gateAResolve\n>[0] & {\n beforePageHotUpdate?: ClientPageRegistryPluginOptions[\"beforePageHotUpdate\"];\n};\n\nexport type BuildWarlockHydrationClientOptions = Readonly<{\n appRoot: string;\n webRoot: string;\n /** Absolute client output dir — threaded to `buildHydrationClient` (`<outdir>/client`). */\n outDir: string;\n resolveAliases: BuildHydrationClientOptions[\"resolveAliases\"];\n external?: BuildHydrationClientOptions[\"external\"];\n /** App-configured plugins, appended after Warlock's client-boundary pipeline. */\n plugins?: BuildHydrationClientOptions[\"plugins\"];\n}>;\n\ntype SsrBoundaryState = {\n readonly appRoot: string;\n readonly clientBoundModules: Set<string>;\n readonly clientImportsByModule: Map<string, Set<string>>;\n};\n\nfunction moduleKey(id: string): string {\n return id.split(\"?\")[0].replace(/\\\\/g, \"/\");\n}\n\nconst CODE_MODULE_EXTENSION = /\\.([cm]?[jt]sx?)$/;\n\nfunction isStatelessClientSurface(id: string, appRoot: string): boolean {\n const bare = moduleKey(id);\n if (!isAppSourcePath(bare, appRoot)) return false;\n if (isServerFile(bare, appRoot)) return false;\n\n return (\n isProjectableFile(bare) ||\n isRecognizedUniversalSurface(bare) ||\n isWithinModuleWebFolder(bare, appRoot)\n );\n}\n\nfunction collectImportSpecifiers(code: string): Set<string> {\n const ast = parse(code, {\n sourceType: \"module\",\n plugins: [\"typescript\", \"jsx\"],\n });\n const imports = new Set<string>();\n\n function walk(node: unknown): void {\n if (!node || typeof node !== \"object\") return;\n if (Array.isArray(node)) {\n for (const child of node) walk(child);\n return;\n }\n\n const record = node as Record<string, any>;\n if (\n (record.type === \"ImportDeclaration\" ||\n record.type === \"ExportNamedDeclaration\" ||\n record.type === \"ExportAllDeclaration\") &&\n record.source?.type === \"StringLiteral\"\n ) {\n imports.add(record.source.value);\n }\n if (\n record.type === \"CallExpression\" &&\n record.callee?.type === \"Import\" &&\n record.arguments?.[0]?.type === \"StringLiteral\"\n ) {\n imports.add(record.arguments[0].value);\n }\n if (\n record.type === \"ImportExpression\" &&\n record.source?.type === \"StringLiteral\"\n ) {\n imports.add(record.source.value);\n }\n\n for (const [key, child] of Object.entries(record)) {\n if (\n key === \"type\" ||\n key === \"start\" ||\n key === \"end\" ||\n key === \"loc\" ||\n key === \"range\" ||\n key.endsWith(\"Comments\") ||\n key === \"extra\"\n ) {\n continue;\n }\n walk(child);\n }\n }\n\n walk(ast.program);\n return imports;\n}\n\nfunction clientViewOf(\n state: SsrBoundaryState,\n code: string,\n id: string,\n): string | undefined {\n const key = moduleKey(id);\n if (\n !state.clientBoundModules.has(key) &&\n !isStatelessClientSurface(key, state.appRoot)\n ) {\n return undefined;\n }\n\n state.clientBoundModules.add(key);\n if (!CODE_MODULE_EXTENSION.test(key)) {\n state.clientImportsByModule.set(key, new Set());\n return code;\n }\n\n const clientCode = isProjectableFile(key)\n ? projectModule(code, key).code\n : code;\n state.clientImportsByModule.set(key, collectImportSpecifiers(clientCode));\n return clientCode;\n}\n\nfunction markResolvedClientModule(\n state: SsrBoundaryState,\n resolved: { id: string } | string | null | false | void,\n): void {\n if (!resolved) return;\n const id = typeof resolved === \"string\" ? resolved : resolved.id;\n if (!id.includes(\"\\0\")) state.clientBoundModules.add(moduleKey(id));\n}\n\nfunction isServerEnvironment(context: {\n environment?: { config: { consumer?: string } };\n}): boolean {\n return context.environment?.config.consumer === \"server\";\n}\n\n/**\n * Keeps the production/client pipeline byte-identical, while giving Gate A\n * and Gate B a validation-only view in Vite's development SSR environment.\n * SSR still evaluates the original source: only the gates receive the\n * projected client view, so loader/server exports retain their legitimate\n * server access while component-visible code is refused before evaluation.\n */\nfunction clientEnvironmentOnly(\n plugin: Plugin,\n ssrState: SsrBoundaryState,\n): Plugin {\n const validatesDevSsr =\n plugin.name === \"warlock:gate-a-resolve\" ||\n plugin.name === \"warlock:gate-b-secrets\";\n const originalTransform =\n typeof plugin.transform === \"function\"\n ? plugin.transform\n : plugin.transform?.handler;\n const originalResolveId =\n typeof plugin.resolveId === \"function\"\n ? plugin.resolveId\n : plugin.resolveId?.handler;\n const originalBuildStart =\n typeof plugin.buildStart === \"function\"\n ? plugin.buildStart\n : plugin.buildStart?.handler;\n\n return {\n ...plugin,\n applyToEnvironment(environment) {\n return (\n environment.config.consumer === \"client\" ||\n (validatesDevSsr && environment.config.consumer === \"server\")\n );\n },\n buildStart: originalBuildStart\n ? function (...args) {\n if (isServerEnvironment(this)) return;\n return originalBuildStart.apply(this, args);\n }\n : undefined,\n transform: originalTransform\n ? async function (code, id, options) {\n if (!isServerEnvironment(this)) {\n return originalTransform.call(this, code, id, options);\n }\n\n const clientCode = clientViewOf(ssrState, code, id);\n if (clientCode === undefined) return null;\n\n const transformed = await originalTransform.call(\n this,\n clientCode,\n id,\n {\n ...options,\n ssr: false,\n },\n );\n\n // Vite can externalize a package in the SSR environment before its\n // normal resolver walk offers that edge to a plugin. Gate A cannot\n // wait for that walk: validate every import that survived projection\n // now, while the original TypeScript source and importer are known.\n if (plugin.name === \"warlock:gate-a-resolve\" && originalResolveId) {\n for (const source of ssrState.clientImportsByModule.get(\n moduleKey(id),\n ) ?? []) {\n const resolved = await originalResolveId.call(this, source, id, {\n attributes: {},\n isEntry: false,\n ssr: false,\n });\n markResolvedClientModule(ssrState, resolved);\n }\n }\n\n return transformed;\n }\n : undefined,\n resolveId: originalResolveId\n ? async function (source, importer, options) {\n if (!isServerEnvironment(this)) {\n return originalResolveId.call(this, source, importer, options);\n }\n\n if (!importer) return null;\n const importerKey = moduleKey(importer);\n const isClientBound =\n ssrState.clientBoundModules.has(importerKey) ||\n isStatelessClientSurface(importerKey, ssrState.appRoot);\n if (!isClientBound) return null;\n\n const survivingImports =\n ssrState.clientImportsByModule.get(importerKey);\n if (survivingImports && !survivingImports.has(source)) return null;\n\n const resolved = await originalResolveId.call(\n this,\n source,\n importer,\n {\n ...options,\n ssr: false,\n },\n );\n markResolvedClientModule(ssrState, resolved);\n return resolved;\n }\n : undefined,\n };\n}\n\n/**\n * The composed client-build pipeline: projection\n * strips the 5 server exports first, THEN Gate B's `transform` checks\n * whatever source remains for inline secret reads, THEN Gate A's\n * `resolveId` judges whatever imports remain. Array order here is\n * `[projection(), gateBSecrets(), gateAResolve()]` to match Vite's own\n * pipeline shape (`transform` before `resolveId`), but array order alone\n * does not guarantee this — see the hook-ordering fact below, which is what\n * actually makes the composition correct.\n *\n * Empirically observed fact (via an instrumented real `vite.build()`, not\n * assumed from plugin array order): for a given module M, Vite/Rollup calls\n * `transform(M)` BEFORE it calls `resolveId` for any of M's own import\n * specifiers — because Rollup must parse M's post-transform source to even\n * discover which specifiers to resolve next. Concretely: `transform` ran on\n * `entry.page.tsx` first, and only after that did `resolveId(\"./dep\", ...)`\n * fire for the import statement still present in the transformed code. A\n * consequence follows directly: if projection's `transform` removes an\n * import statement from a page module entirely (e.g. `loader`'s\n * `@warlock.js/core` import, stripped because `loader` itself is removed),\n * `resolveId` is never invoked for that specifier at all — Gate A doesn't\n * \"let it pass\", it never sees it. Gate A's `resolveId` only fires for\n * imports that survive projection's `transform`, which is exactly why a\n * component-level `@warlock.js/core` import (never touched by projection)\n * still reaches and is refused by Gate A.\n *\n * This is OBSERVED Rollup behavior, not a documented contract — a future\n * Vite/Rollup upgrade could invert it. Pinned by a\n * regression test (`index.spec.ts`, \"D.3 hook ordering pin\") that fails\n * loudly if the ordering ever inverts, and by the `vite` peer floor in\n * `web/package.json` (`>=7.3.5`, the version this was verified against). If\n * that test ever fails after a Vite bump: the failure mode of the ordering\n * assumption breaking is SAFE — projection would stop removing an import\n * statement Gate A still sees, so Gate A would refuse an import it used to\n * silently let a stripped server export take with it. That is a loud build\n * failure (\"Gate A refused an import\"), never a silent client-bundle leak.\n * Do NOT \"fix\" an apparent Gate A false-positive after a Vite upgrade by\n * weakening Gate A (e.g. widening what it lets through) — investigate\n * whether this ordering assumption broke instead; loosening Gate A to work\n * around it would turn a loud failure into the exact silent leak this\n * pipeline exists to prevent.\n *\n * Gate B is placed between the two for the same reason, but for a `transform`\n * hook rather than `resolveId`: within a plugin array, Rollup runs each\n * module's registered `transform` hooks in array order, each one receiving\n * the PREVIOUS plugin's output. Running Gate B after projection means it\n * inspects the POST-projection source — a `process.env.SECRET` read inside\n * `loader` (a server export, legitimately reading a real secret server-side)\n * is invisible to Gate B once projection has already removed `loader`\n * entirely, exactly as it should be: Gate B's job is to fence client-bound\n * code, and projection is what decides what counts as client-bound. A\n * component-level secret read is untouched by projection and still reaches\n * Gate B, which refuses it. Gate B does not depend on Gate A's `resolveId`\n * output at all (orthogonal concern, raw source vs. import paths), so its\n * position relative to Gate A is not load-bearing — it is placed before Gate\n * A only to keep both `transform` hooks adjacent in the array.\n *\n * Gate C (`gate-c-verify.ts`) runs last and only at `generateBundle` — after\n * the entire `transform`/`resolveId` build phase has completed for every\n * plugin, regardless of array position (a Rollup lifecycle fact, not\n * something this array order enforces). It verifies the EMITTED output the\n * other three produced: no server export survived as a top-level binding, no\n * import edge into a server-only package survived into the module graph, and\n * it emits the reviewable `PUBLIC_*` inlined-value manifest. `gateBSecrets` and `gateCVerify` share\n * one `PublicEnvTracker` instance so the manifest and Gate B's own unread-key\n * exclusion check agree by construction, not by coincidence.\n *\n * `clientPageRegistry()` is FIRST, ahead of projection. It contributes no\n * `transform` at all — only a `resolveId`/`load` pair for one synthetic id —\n * so it cannot displace or pre-empt any gate's inspection of any real file.\n * Two reasons for the position, one of which is not load-bearing and is\n * labelled as such:\n *\n * 1. Load-bearing: it must own `virtual:warlock/pages` before Gate A's\n * `resolveId` (also `enforce: \"pre\"`) reaches its `this.resolve(...)` call\n * for that specifier. Gate A's nested resolve would find it anyway, but\n * routing the id through Gate A's importer-chain bookkeeping only to have\n * it come back means a synthetic id can surface in a user-facing \"Import\n * chain:\" message. Resolving it first keeps ownership of the id in one\n * place.\n * 2. NOT load-bearing: the position relative to `projection()`. Projection is\n * `enforce: \"pre\"` and selects by file BASENAME (`projection.ts:242-248`),\n * so it transforms every `*.page.tsx` / `layout.tsx` / `root.tsx` that\n * enters the graph regardless of who imported it or where this plugin sits.\n * The registry emits absolute POSIX specifiers that Rollup resolves and\n * loads as ORDINARY file modules — they are not inlined into the virtual\n * module — so each one is transformed exactly as a page imported from a\n * real file would be. Projection declines the virtual module itself\n * (`\\0virtual:warlock/pages` has no matching basename), which is correct:\n * generated code has no server exports to strip.\n *\n * Point 2 is asserted, not assumed, by a real `vite.build()` in\n * `page-registry-plugin.spec.ts`: a fixture page whose `loader` — and only its\n * `loader` — imports a marker module that Gate A independently PERMITS, built\n * through this exact array, with the marker proven absent from every emitted\n * chunk while the page's own component text is proven present. Inspecting this\n * array's order would prove nothing about what reaches the browser.\n */\nexport function warlockClientBoundary(\n options: WarlockClientBoundaryOptions = {},\n): Plugin[] {\n const tracker = createPublicEnvTracker();\n const ssrState: SsrBoundaryState = {\n appRoot: path.resolve(options.appRoot ?? process.cwd()),\n clientBoundModules: new Set(),\n clientImportsByModule: new Map(),\n };\n return [\n clientPageRegistry({\n appRoot: options.appRoot,\n beforePageHotUpdate: options.beforePageHotUpdate,\n }),\n projection(),\n gateBSecrets({ tracker }),\n gateAResolve(options),\n gateCVerify({ ...options, tracker }),\n ].map((plugin) => clientEnvironmentOnly(plugin, ssrState));\n}\n\n/**\n * Callable production seam: callers own their exact source aliases and the\n * app-root classification boundary, while this module owns the one canonical\n * projection/Gate B/Gate A/Gate C composition.\n */\nexport async function buildWarlockHydrationClient(\n options: BuildWarlockHydrationClientOptions,\n): Promise<BuildHydrationClientResult> {\n return buildHydrationClient({\n webRoot: options.webRoot,\n outDir: options.outDir,\n resolveAliases: options.resolveAliases,\n external: options.external,\n plugins: [\n ...warlockClientBoundary({ appRoot: options.appRoot }),\n ...(options.plugins ?? []),\n ],\n });\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { Plugin, ViteDevServer } from "vite";
2
2
 
3
- //#region web/src/vite/page-registry-plugin.d.ts
3
+ //#region ../web/src/vite/page-registry-plugin.d.ts
4
4
  /**
5
5
  * The specifier application code writes.
6
6
  *
@@ -5,7 +5,7 @@ import path from "node:path";
5
5
  import { parse } from "@babel/parser";
6
6
  import MagicString from "magic-string";
7
7
 
8
- //#region web/src/vite/page-registry-plugin.ts
8
+ //#region ../web/src/vite/page-registry-plugin.ts
9
9
  /**
10
10
  * The wire between the two halves that already existed and never met:
11
11
  * `discoverPages` (the page graph, read off disk) and `generateClientRegistry`
@@ -1 +1 @@
1
- {"version":3,"file":"page-registry-plugin.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/vite/page-registry-plugin.ts"],"sourcesContent":["/**\n * The wire between the two halves that already existed and never met:\n * `discoverPages` (the page graph, read off disk) and `generateClientRegistry`\n * (the module SOURCE that carries that graph into the browser). Neither one\n * touches Vite; this plugin is the only place they are joined, and it joins\n * them as a VIRTUAL module so nothing is ever written to the user's tree.\n *\n * Identical in dev and build — no `apply`/`command` gating, matching the rest\n * of `warlockClientBoundary`'s composition (`index.ts`), which is also\n * mode-agnostic. A registry that differed between `vite dev` and `vite build`\n * would make every dev-only or prod-only page bug unreproducible in the other\n * mode.\n */\nimport { parse } from \"@babel/parser\";\nimport MagicString from \"magic-string\";\nimport path from \"node:path\";\nimport type { Plugin, ViteDevServer } from \"vite\";\nimport { discoverPages, toPosix } from \"../build/discover-pages\";\nimport { generateClientRegistry } from \"../build/generate-client-registry\";\nimport { SERVER_EXPORT_NAMES } from \"./projection\";\n\n/**\n * The specifier application code writes.\n *\n * Exported so the client runtime imports this constant instead of retyping the\n * string: a constant two sides must agree on is a guard, and a guard duplicated\n * at a second site fails open at the third — a typo'd re-spelling doesn't fail\n * loudly, it resolves to \"no such module\" or, worse, to a stale real file.\n */\nexport const CLIENT_PAGE_REGISTRY_ID = \"virtual:warlock/pages\";\n\n/**\n * The resolved id, `\\0`-prefixed per Vite/Rollup convention so no other plugin\n * (and no filesystem watcher) mistakes it for a real path.\n */\nexport const RESOLVED_CLIENT_PAGE_REGISTRY_ID = `\\0${CLIENT_PAGE_REGISTRY_ID}`;\n\n/**\n * Evicts the client registry so its next request re-runs page discovery, then\n * reloads the document so hydration consumes that fresh registry.\n *\n * Vite 7 keeps separate module graphs per environment. Pages are imported by\n * the browser, so only the resolved virtual module in the client graph is the\n * cache entry this operation owns.\n */\nexport function invalidateClientPageRegistry(vite: ViteDevServer): void {\n const moduleGraph = vite.environments.client.moduleGraph;\n const registryModule = moduleGraph.getModuleById(RESOLVED_CLIENT_PAGE_REGISTRY_ID);\n\n if (registryModule) moduleGraph.invalidateModule(registryModule);\n\n vite.hot.send({ type: \"full-reload\", path: \"*\" });\n}\n\nexport type ClientPageRegistryPluginOptions = {\n /** Absolute path to the application root. Defaults to `process.cwd()`, matching Vite's own default `root` and Gate A's `appRoot` default. */\n appRoot?: string;\n /** Source directory name under `appRoot`; forwarded verbatim to `discoverPages`, which defaults it to `\"src\"`. */\n srcDir?: string;\n /**\n * Optional server-side barrier run before this plugin decides how the browser\n * receives a page update. `true` means the callback already published the\n * new route graph and sent the required reload, so this hook emits no second\n * update for the same filesystem event.\n */\n beforePageHotUpdate?: (context: {\n file: string;\n type: \"create\" | \"update\" | \"delete\";\n }) => boolean | Promise<boolean>;\n};\n\n/**\n * The import specifiers the emitted registry names must be ABSOLUTE POSIX file\n * paths, never relative ones.\n *\n * A relative specifier resolves against its IMPORTER, and the importer here is\n * `\\0virtual:warlock/pages` — a synthetic id whose `dirname` is not a real\n * directory. `./blog.page.tsx` from that importer resolves to nonsense that\n * fails at bundle time with a path no user authored and no user can act on.\n *\n * Separator normalization is `hydration-entries.ts`'s\n * (`hydration-entries.ts:12-14`) and `discover-pages.ts`'s single\n * `.replace(/\\\\/g, \"/\")` rule, reused via the already-exported `toPosix` rather\n * than spelled a third time — keeping the drive colon (`D:/...`) is exactly\n * what Vite's resolver wants on Windows.\n */\nfunction toImportSpecifier(absoluteFilePath: string): string {\n return toPosix(path.resolve(absoluteFilePath));\n}\n\n/**\n * Erases the generated module's TypeScript down to plain JavaScript.\n *\n * NOT optional, and not a style choice. Vite's `vite:esbuild` transform is\n * gated behind `createFilter`, which refuses ANY id containing a NUL byte\n * (`node_modules/vite/dist/node/chunks/config.js:1512` — `if\n * (id.includes(\"\\0\")) return false`). So the one module in this build that is\n * `\\0`-prefixed by convention is precisely the one module esbuild will never\n * transform, while `generateClientRegistry` always emits TypeScript (a\n * type-only `ClientPageEntry` import plus the array's type annotation). Handed\n * to Rollup verbatim, `import type { ClientPageEntry } from ...` is a\n * JavaScript syntax error.\n *\n * Done with the AST rather than a regex, using the same `@babel/parser` +\n * `MagicString` pair `projection.ts` already uses in this directory — a regex\n * over generated source is a second grammar that drifts from the generator's\n * silently. If the generator ever emits a TS construct outside these two\n * shapes, the result is a Rollup parse error naming the virtual module: loud,\n * not silent. `page-registry-plugin.spec.ts` pins that the erased output\n * re-parses as plain JavaScript with the TypeScript plugin switched OFF.\n */\nfunction eraseTypes(source: string): string {\n const ast = parse(source, { sourceType: \"module\", plugins: [\"typescript\"] });\n const magic = new MagicString(source);\n\n for (const statement of ast.program.body as any[]) {\n if (statement.type === \"ImportDeclaration\" && statement.importKind === \"type\") {\n let end = statement.end as number;\n if (source[end] === \"\\r\" && source[end + 1] === \"\\n\") end += 2;\n else if (source[end] === \"\\n\") end += 1;\n magic.remove(statement.start as number, end);\n continue;\n }\n\n const declaration =\n statement.type === \"ExportNamedDeclaration\" ? statement.declaration : statement;\n\n if (declaration?.type !== \"VariableDeclaration\") continue;\n\n for (const declarator of declaration.declarations as any[]) {\n const annotation = declarator.id?.typeAnnotation;\n if (annotation) magic.remove(annotation.start as number, annotation.end as number);\n }\n }\n\n return magic.toString();\n}\n\n/**\n * The four file shapes that carry SERVER data (`metadata` chief among them)\n * and are therefore projected before the client graph forms — the exact set\n * `projection.ts`'s `isProjectableFile` matches, spelled here by BASENAME so\n * the two agree by construction on what \"a server-side page module\" is. A\n * change to one of these is the only kind of change whose SERVER half\n * (`metadata`, `loader`, …) can move without the client half moving at all.\n */\nfunction isServerPageModule(file: string): boolean {\n const base = path.basename(file.split(\"?\")[0]);\n if (/\\.page\\.tsx?$/.test(base)) return true;\n if (base === \"layout.tsx\" || base === \"layout.ts\") return true;\n if (/\\.layout\\.tsx?$/.test(base)) return true;\n if (base === \"root.tsx\") return true;\n return false;\n}\n\n/**\n * The server-vs-client reload seam.\n *\n * A page module carries TWO halves. The CLIENT half is the projected code the\n * browser actually runs; Fast Refresh can hot-swap it with zero reloads. The\n * SERVER half — `metadata`, `loader`, `route`, `middleware`, `validation`, `prefix`, plus\n * the imports/locals orphaned with them — is stripped by projection\n * (`projection.ts:49`) and set to `undefined` on hydration\n * (`client/hydrate-page.tsx`), so the browser never holds it and there is\n * nothing on the client to hot-swap. Its effect is felt only when SSR re-runs\n * and re-renders `<head>`; the honest way to apply a change to it is a full\n * document reload.\n *\n * THE RULING (canon `6b240682`), stated as the invariant it is:\n *\n * FAST REFRESH ONLY WHEN THE ONLY CHANGES ARE INSIDE COMPONENT BODIES.\n * EVERYTHING ELSE RELOADS.\n *\n * Concretely: any change to an import statement, to a module-level\n * declaration, or to a server export forces a full document reload — whether\n * or not the JSX moved in the same save.\n *\n * WHY AN OVER-APPROXIMATION, AND WHY NOBODY SHOULD \"IMPROVE\" IT BACK\n *\n * Two earlier cuts tried to name the server half EXACTLY and both shipped a\n * stale `<head>`:\n *\n * 1. Comparing only the projected CLIENT code. A save that changed the JSX\n * *and* `metadata` moved the client half, which was read as proof that\n * only the JSX moved. Mixed saves took the Fast Refresh branch.\n * 2. Adding the complement — the stripped server half, recovered by\n * subsequence diff. `projection.ts:447-448` KEEPS an import when the\n * client reads it, so an import read by BOTH `metadata` and the JSX\n * lives in the projection and appears in NEITHER half exclusively.\n * Change its specifier and the complement is byte-identical → Fast\n * Refresh, stale `<title>`. Shared module-level LOCALS have the same\n * shape, so extending the complement a third time is a third bug.\n *\n * Both failures were UNDER-approximations, and under-approximating is the\n * unsafe direction. A precise reachability analysis over shared imports and\n * locals is the correct answer and is a later refinement; getting it subtly\n * wrong reproduces this bug again. Over-approximating can only err toward\n * RELOADING. A needless reload costs component state; a missed one ships a\n * stale `<head>` and calls it a hot update.\n *\n * THE ACCEPTED COST, which is not a bug to be optimised away: editing a\n * module-level helper read only by the JSX now reloads.\n *\n * The skeleton has to be captured BEFORE the edit, because by the time\n * `hotUpdate` runs Vite has already hard-invalidated the module and cleared its\n * `transformResult` (`onFileChange` → `invalidateModule`, which runs before any\n * `hotUpdate` hook). The `transform` spy below is that capture: it runs first in\n * the client environment, records the skeleton, and returns nothing so\n * projection still performs the real transform.\n */\ntype SkeletonCache = Map<string, string>;\n\n/**\n * What replaces a refresh-safe body in the skeleton. Its content is irrelevant —\n * only that it is CONSTANT, so two sources that differ solely inside a masked\n * body serialise identically.\n */\nconst MASKED_REFRESH_BODY = \"/*warlock:refresh-body*/\";\n\n/** React's own convention, and the one `react-refresh` itself uses: components are PascalCase. */\nfunction isComponentName(name: string | undefined): boolean {\n return typeof name === \"string\" && /^[A-Z]/.test(name);\n}\n\n/** The `body` node of a function-shaped expression/declaration, or `undefined` for anything else. */\nfunction functionBody(node: any): any | undefined {\n if (!node) return undefined;\n if (\n node.type === \"FunctionDeclaration\" ||\n node.type === \"FunctionExpression\" ||\n node.type === \"ArrowFunctionExpression\"\n ) {\n return node.body;\n }\n return undefined;\n}\n\n/**\n * Generic duck-typed identifier walk, the same shape `projection.ts`'s\n * `collectIdentifierNames` uses (it is not exported, and re-deriving one\n * OVER-collecting walk is safe here for the same reason it is safe there).\n *\n * Over-collecting — counting an object property key or a shadowing parameter\n * as a \"read\" — can only make the reachable set BIGGER, which can only UNMASK\n * more component bodies, which can only produce more reloads. The safe\n * direction.\n */\nfunction collectIdentifierNames(node: unknown, names: Set<string>): void {\n if (!node || typeof node !== \"object\") return;\n if (Array.isArray(node)) {\n for (const item of node) collectIdentifierNames(item, names);\n return;\n }\n const record = node as Record<string, unknown>;\n if (typeof record.type !== \"string\") return;\n if (record.type === \"Identifier\" || record.type === \"JSXIdentifier\") {\n names.add((record as any).name);\n }\n for (const key of Object.keys(record)) {\n if (key === \"type\" || key === \"start\" || key === \"end\" || key === \"loc\" || key === \"range\") continue;\n if (key === \"leadingComments\" || key === \"trailingComments\" || key === \"innerComments\" || key === \"extra\") {\n continue;\n }\n collectIdentifierNames(record[key], names);\n }\n}\n\n/** The module-scope names a top-level statement binds (the `export` wrapper looked through). */\nfunction topLevelBoundNames(stmt: any): Set<string> {\n const names = new Set<string>();\n const declaration = stmt.type === \"ExportNamedDeclaration\" ? stmt.declaration : stmt;\n if (!declaration) return names;\n if (declaration.type === \"VariableDeclaration\") {\n for (const declarator of declaration.declarations) {\n if (declarator.id?.type === \"Identifier\") names.add(declarator.id.name);\n }\n } else if (declaration.id?.type === \"Identifier\") {\n names.add(declaration.id.name);\n }\n return names;\n}\n\n/**\n * Every module-scope name reachable from one of the six server exports.\n *\n * Used ONLY to UNMASK: a PascalCase function that `metadata` or `loader` can\n * reach is not a component for this purpose, it is a server-side helper that\n * merely looks like one, and a change inside its body must reload. Seeded from\n * any top-level statement binding a `SERVER_EXPORT_NAMES` name — deliberately\n * looser than `projection.ts`'s own `isServerExportDeclaration` (no export\n * requirement, no single-declarator requirement), because seeding from MORE\n * statements can only unmask more, i.e. reload more.\n *\n * Fixpoint, not one pass, for the same reason projection's is: a server-only\n * helper can be reached only through another server-only helper.\n */\nfunction serverReachableNames(body: any[]): Set<string> {\n const reached = new Set<string>();\n const declarations = body\n .filter((stmt) => stmt.type !== \"ImportDeclaration\")\n .map((stmt) => ({ stmt, names: topLevelBoundNames(stmt) }));\n\n for (const { stmt, names } of declarations) {\n let isServerExport = false;\n for (const name of names) {\n if (SERVER_EXPORT_NAMES.has(name)) isServerExport = true;\n }\n if (isServerExport) collectIdentifierNames(stmt, reached);\n }\n\n for (let changed = true; changed; ) {\n changed = false;\n for (const { stmt, names } of declarations) {\n let isReached = false;\n for (const name of names) {\n if (reached.has(name)) isReached = true;\n }\n if (!isReached) continue;\n const before = reached.size;\n collectIdentifierNames(stmt, reached);\n if (reached.size !== before) changed = true;\n }\n }\n\n return reached;\n}\n\n/**\n * The body node to mask for a top-level statement, or `undefined` if this\n * statement is neither a component declaration nor an exported `register`\n * declaration.\n *\n * Recognised shapes, and only these:\n * - `export default function () {…}` / `export default () => …` — the page\n * component, whatever it is called.\n * - `function Name() {…}` / `const Name = () => …` (PascalCase, optionally\n * `export`ed) — a component declared alongside it.\n * - `export function register() {…}` / `export const register = () => …` —\n * the lifecycle hook whose replacement namespace is invoked by projection.\n *\n * Everything else — `memo(...)`/`forwardRef(...)` wrappers, classes,\n * lowercase helpers, every server export — is left UNMASKED and therefore\n * compared byte-for-byte. That costs Fast Refresh on those shapes and buys the\n * guarantee; see this seam's header.\n */\nfunction componentBodyToMask(stmt: any, serverReachable: Set<string>): any | undefined {\n if (stmt.type === \"ExportDefaultDeclaration\") return functionBody(stmt.declaration);\n\n if (stmt.type === \"ExportNamedDeclaration\") {\n const exported = stmt.declaration;\n if (exported?.type === \"FunctionDeclaration\" && exported.id?.name === \"register\") {\n return functionBody(exported);\n }\n if (exported?.type === \"VariableDeclaration\" && exported.declarations.length === 1) {\n const declarator = exported.declarations[0];\n if (declarator.id?.type === \"Identifier\" && declarator.id.name === \"register\") {\n return functionBody(declarator.init);\n }\n }\n }\n\n const declaration = stmt.type === \"ExportNamedDeclaration\" ? stmt.declaration : stmt;\n if (!declaration) return undefined;\n\n const named = (name: string | undefined, node: any) =>\n isComponentName(name) && !serverReachable.has(name as string) ? functionBody(node) : undefined;\n\n if (declaration.type === \"FunctionDeclaration\") {\n return named(declaration.id?.name, declaration);\n }\n if (declaration.type === \"VariableDeclaration\" && declaration.declarations.length === 1) {\n const declarator = declaration.declarations[0];\n if (declarator.id?.type !== \"Identifier\") return undefined;\n return named(declarator.id.name, declarator.init);\n }\n return undefined;\n}\n\n/**\n * The module source with every component and exported `register` body replaced\n * by a constant — the ONE value the reload decision compares across an edit.\n *\n * Everything outside those bodies survives verbatim: imports, signatures,\n * module-level declarations, all six server exports, and the comments and\n * whitespace between them. So the skeleton is unchanged iff the save touched\n * nothing but refresh-safe bodies.\n *\n * Returns `undefined` when the source does not parse — a half-typed file whose\n * error Vite is already reporting from projection's real `transform`. The\n * caller leaves the cache holding the last GOOD skeleton, so the next\n * successful save is still compared against the right baseline.\n */\nfunction captureSkeleton(code: string): string | undefined {\n let ast: ReturnType<typeof parse>;\n try {\n ast = parse(code, { sourceType: \"module\", plugins: [\"typescript\", \"jsx\"] });\n } catch {\n return undefined;\n }\n\n const body = ast.program.body as any[];\n const serverReachable = serverReachableNames(body);\n const magic = new MagicString(code);\n\n for (const stmt of body) {\n const bodyNode = componentBodyToMask(stmt, serverReachable);\n if (!bodyNode) continue;\n const start = bodyNode.start as number;\n const end = bodyNode.end as number;\n if (end > start) magic.overwrite(start, end, MASKED_REFRESH_BODY);\n }\n\n return magic.toString();\n}\n\n/**\n * Serves the client page registry at {@link CLIENT_PAGE_REGISTRY_ID}.\n *\n * Discovery runs INSIDE `load`, once per `load` call, and its result is NOT\n * cached across builds — the plugin holds no state at all. A registry cached\n * past the moment a page file appears is a page that silently 404s until\n * someone restarts the dev server, which is a far more expensive bug than\n * re-walking a source tree. Rollup calls `load` once per module per build, and\n * in dev Vite's module graph caches the transformed result until the module is\n * invalidated, so the walk is not per-request either way. (Invalidating that\n * dev-server cache when a page file is ADDED needs a `handleHotUpdate`/watcher\n * hook that belongs with the dev provider slice — see the followup.)\n *\n * `enforce: \"pre\"` and placed FIRST in `warlockClientBoundary`'s array — see\n * that function's comment in `index.ts` for why position is what it is, and\n * `page-registry-plugin.spec.ts` for the real-build proof that the pages this\n * module names still reach `projection()`.\n */\nexport function clientPageRegistry(options: ClientPageRegistryPluginOptions = {}): Plugin {\n const appRoot = path.resolve(options.appRoot ?? process.cwd());\n\n // Per-plugin-instance, so two composed pipelines never cross-contaminate.\n // Holds the last captured SKELETON (source with refresh-safe bodies masked) of\n // each server page module the client environment transformed — the \"before\"\n // side of the comparison in `hotUpdate`. See `captureSkeleton` above.\n const skeletonCache: SkeletonCache = new Map();\n\n return {\n name: \"warlock:client-page-registry\",\n enforce: \"pre\",\n resolveId(source) {\n if (source === CLIENT_PAGE_REGISTRY_ID) return RESOLVED_CLIENT_PAGE_REGISTRY_ID;\n return undefined;\n },\n load(id) {\n if (id !== RESOLVED_CLIENT_PAGE_REGISTRY_ID) return undefined;\n\n const pages = discoverPages({ appRoot, srcDir: options.srcDir });\n\n return eraseTypes(generateClientRegistry({ pages, toImportSpecifier }));\n },\n /**\n * Capture-only spy. Records the SKELETON of every server page module the\n * CLIENT environment transforms, and returns nothing so projection's own\n * `transform` still does the real work. SERVE-ONLY:\n * `this.environment.mode !== \"dev\"` skips it during `vite build`, where\n * there is no `hotUpdate` to feed and the extra parse would be pure cost.\n */\n transform(code, id) {\n if (this.environment?.mode !== \"dev\") return undefined;\n if (!isServerPageModule(id)) return undefined;\n\n const skeleton = captureSkeleton(code);\n if (skeleton !== undefined) skeletonCache.set(id, skeleton);\n\n return undefined;\n },\n /**\n * Applies the ruling (canon `6b240682`): Fast Refresh ONLY when the only\n * changes are inside component or exported `register` bodies.\n *\n * - Skeleton moved (an import, a module-level declaration, ANY server\n * export — with or without a simultaneous JSX change) → full reload.\n * - Skeleton unchanged → defer to Fast Refresh, zero reloads.\n *\n * Note what is NOT here: no attempt to name which half a shared import or\n * local belongs to. That question is what produced the two previous stale\n * `<head>` bugs; this seam refuses to answer it and reloads instead.\n * `hotUpdate` exists only on the dev server, so this is serve-only by\n * construction.\n */\n async hotUpdate(context) {\n if (isServerPageModule(context.file)) {\n const routeGraphHandled = await options.beforePageHotUpdate?.({\n file: context.file,\n type: context.type,\n });\n\n if (routeGraphHandled) return [];\n }\n\n // `create`/`delete` are page graph churn, not in-place edits — leave them\n // to Vite's normal handling (a new/removed module reloads on its own).\n if (context.type !== \"update\") return undefined;\n if (!isServerPageModule(context.file)) return undefined;\n\n const nextSource = await context.read();\n const next = captureSkeleton(nextSource);\n const prev = skeletonCache.get(context.file);\n\n // Refresh the cache for the next edit regardless of the decision below.\n if (next !== undefined) skeletonCache.set(context.file, next);\n\n // Could not parse the new source (Vite is already reporting that error),\n // or the client environment never transformed this module — which means\n // the browser is not holding this page, so there is no stale `<head>` to\n // ship and nothing a reload of some OTHER page would fix.\n if (next === undefined || prev === undefined) return undefined;\n\n // Anything outside a component body moved: the browser cannot hot-swap\n // it, so reload the document to re-run SSR and re-render `<head>`.\n // `path: \"*\"` matches Vite's own middleware-mode reload.\n if (prev !== next) {\n this.environment.hot.send({ type: \"full-reload\", path: \"*\" });\n\n // Empty module list: we've issued the update ourselves, so Vite should\n // not additionally push a Fast Refresh for the client module.\n return [];\n }\n\n // Only component bodies moved: defer to Vite's Fast Refresh with zero\n // reloads. A no-op re-save falls through the same harmless path.\n return undefined;\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"page-registry-plugin.mjs","names":[],"sources":["../../../../../../../web/src/vite/page-registry-plugin.ts"],"sourcesContent":["/**\n * The wire between the two halves that already existed and never met:\n * `discoverPages` (the page graph, read off disk) and `generateClientRegistry`\n * (the module SOURCE that carries that graph into the browser). Neither one\n * touches Vite; this plugin is the only place they are joined, and it joins\n * them as a VIRTUAL module so nothing is ever written to the user's tree.\n *\n * Identical in dev and build — no `apply`/`command` gating, matching the rest\n * of `warlockClientBoundary`'s composition (`index.ts`), which is also\n * mode-agnostic. A registry that differed between `vite dev` and `vite build`\n * would make every dev-only or prod-only page bug unreproducible in the other\n * mode.\n */\nimport { parse } from \"@babel/parser\";\nimport MagicString from \"magic-string\";\nimport path from \"node:path\";\nimport type { Plugin, ViteDevServer } from \"vite\";\nimport { discoverPages, toPosix } from \"../build/discover-pages\";\nimport { generateClientRegistry } from \"../build/generate-client-registry\";\nimport { SERVER_EXPORT_NAMES } from \"./projection\";\n\n/**\n * The specifier application code writes.\n *\n * Exported so the client runtime imports this constant instead of retyping the\n * string: a constant two sides must agree on is a guard, and a guard duplicated\n * at a second site fails open at the third — a typo'd re-spelling doesn't fail\n * loudly, it resolves to \"no such module\" or, worse, to a stale real file.\n */\nexport const CLIENT_PAGE_REGISTRY_ID = \"virtual:warlock/pages\";\n\n/**\n * The resolved id, `\\0`-prefixed per Vite/Rollup convention so no other plugin\n * (and no filesystem watcher) mistakes it for a real path.\n */\nexport const RESOLVED_CLIENT_PAGE_REGISTRY_ID = `\\0${CLIENT_PAGE_REGISTRY_ID}`;\n\n/**\n * Evicts the client registry so its next request re-runs page discovery, then\n * reloads the document so hydration consumes that fresh registry.\n *\n * Vite 7 keeps separate module graphs per environment. Pages are imported by\n * the browser, so only the resolved virtual module in the client graph is the\n * cache entry this operation owns.\n */\nexport function invalidateClientPageRegistry(vite: ViteDevServer): void {\n const moduleGraph = vite.environments.client.moduleGraph;\n const registryModule = moduleGraph.getModuleById(RESOLVED_CLIENT_PAGE_REGISTRY_ID);\n\n if (registryModule) moduleGraph.invalidateModule(registryModule);\n\n vite.hot.send({ type: \"full-reload\", path: \"*\" });\n}\n\nexport type ClientPageRegistryPluginOptions = {\n /** Absolute path to the application root. Defaults to `process.cwd()`, matching Vite's own default `root` and Gate A's `appRoot` default. */\n appRoot?: string;\n /** Source directory name under `appRoot`; forwarded verbatim to `discoverPages`, which defaults it to `\"src\"`. */\n srcDir?: string;\n /**\n * Optional server-side barrier run before this plugin decides how the browser\n * receives a page update. `true` means the callback already published the\n * new route graph and sent the required reload, so this hook emits no second\n * update for the same filesystem event.\n */\n beforePageHotUpdate?: (context: {\n file: string;\n type: \"create\" | \"update\" | \"delete\";\n }) => boolean | Promise<boolean>;\n};\n\n/**\n * The import specifiers the emitted registry names must be ABSOLUTE POSIX file\n * paths, never relative ones.\n *\n * A relative specifier resolves against its IMPORTER, and the importer here is\n * `\\0virtual:warlock/pages` — a synthetic id whose `dirname` is not a real\n * directory. `./blog.page.tsx` from that importer resolves to nonsense that\n * fails at bundle time with a path no user authored and no user can act on.\n *\n * Separator normalization is `hydration-entries.ts`'s\n * (`hydration-entries.ts:12-14`) and `discover-pages.ts`'s single\n * `.replace(/\\\\/g, \"/\")` rule, reused via the already-exported `toPosix` rather\n * than spelled a third time — keeping the drive colon (`D:/...`) is exactly\n * what Vite's resolver wants on Windows.\n */\nfunction toImportSpecifier(absoluteFilePath: string): string {\n return toPosix(path.resolve(absoluteFilePath));\n}\n\n/**\n * Erases the generated module's TypeScript down to plain JavaScript.\n *\n * NOT optional, and not a style choice. Vite's `vite:esbuild` transform is\n * gated behind `createFilter`, which refuses ANY id containing a NUL byte\n * (`node_modules/vite/dist/node/chunks/config.js:1512` — `if\n * (id.includes(\"\\0\")) return false`). So the one module in this build that is\n * `\\0`-prefixed by convention is precisely the one module esbuild will never\n * transform, while `generateClientRegistry` always emits TypeScript (a\n * type-only `ClientPageEntry` import plus the array's type annotation). Handed\n * to Rollup verbatim, `import type { ClientPageEntry } from ...` is a\n * JavaScript syntax error.\n *\n * Done with the AST rather than a regex, using the same `@babel/parser` +\n * `MagicString` pair `projection.ts` already uses in this directory — a regex\n * over generated source is a second grammar that drifts from the generator's\n * silently. If the generator ever emits a TS construct outside these two\n * shapes, the result is a Rollup parse error naming the virtual module: loud,\n * not silent. `page-registry-plugin.spec.ts` pins that the erased output\n * re-parses as plain JavaScript with the TypeScript plugin switched OFF.\n */\nfunction eraseTypes(source: string): string {\n const ast = parse(source, { sourceType: \"module\", plugins: [\"typescript\"] });\n const magic = new MagicString(source);\n\n for (const statement of ast.program.body as any[]) {\n if (statement.type === \"ImportDeclaration\" && statement.importKind === \"type\") {\n let end = statement.end as number;\n if (source[end] === \"\\r\" && source[end + 1] === \"\\n\") end += 2;\n else if (source[end] === \"\\n\") end += 1;\n magic.remove(statement.start as number, end);\n continue;\n }\n\n const declaration =\n statement.type === \"ExportNamedDeclaration\" ? statement.declaration : statement;\n\n if (declaration?.type !== \"VariableDeclaration\") continue;\n\n for (const declarator of declaration.declarations as any[]) {\n const annotation = declarator.id?.typeAnnotation;\n if (annotation) magic.remove(annotation.start as number, annotation.end as number);\n }\n }\n\n return magic.toString();\n}\n\n/**\n * The four file shapes that carry SERVER data (`metadata` chief among them)\n * and are therefore projected before the client graph forms — the exact set\n * `projection.ts`'s `isProjectableFile` matches, spelled here by BASENAME so\n * the two agree by construction on what \"a server-side page module\" is. A\n * change to one of these is the only kind of change whose SERVER half\n * (`metadata`, `loader`, …) can move without the client half moving at all.\n */\nfunction isServerPageModule(file: string): boolean {\n const base = path.basename(file.split(\"?\")[0]);\n if (/\\.page\\.tsx?$/.test(base)) return true;\n if (base === \"layout.tsx\" || base === \"layout.ts\") return true;\n if (/\\.layout\\.tsx?$/.test(base)) return true;\n if (base === \"root.tsx\") return true;\n return false;\n}\n\n/**\n * The server-vs-client reload seam.\n *\n * A page module carries TWO halves. The CLIENT half is the projected code the\n * browser actually runs; Fast Refresh can hot-swap it with zero reloads. The\n * SERVER half — `metadata`, `loader`, `route`, `middleware`, `validation`, `prefix`, plus\n * the imports/locals orphaned with them — is stripped by projection\n * (`projection.ts:49`) and set to `undefined` on hydration\n * (`client/hydrate-page.tsx`), so the browser never holds it and there is\n * nothing on the client to hot-swap. Its effect is felt only when SSR re-runs\n * and re-renders `<head>`; the honest way to apply a change to it is a full\n * document reload.\n *\n * THE RULING (canon `6b240682`), stated as the invariant it is:\n *\n * FAST REFRESH ONLY WHEN THE ONLY CHANGES ARE INSIDE COMPONENT BODIES.\n * EVERYTHING ELSE RELOADS.\n *\n * Concretely: any change to an import statement, to a module-level\n * declaration, or to a server export forces a full document reload — whether\n * or not the JSX moved in the same save.\n *\n * WHY AN OVER-APPROXIMATION, AND WHY NOBODY SHOULD \"IMPROVE\" IT BACK\n *\n * Two earlier cuts tried to name the server half EXACTLY and both shipped a\n * stale `<head>`:\n *\n * 1. Comparing only the projected CLIENT code. A save that changed the JSX\n * *and* `metadata` moved the client half, which was read as proof that\n * only the JSX moved. Mixed saves took the Fast Refresh branch.\n * 2. Adding the complement — the stripped server half, recovered by\n * subsequence diff. `projection.ts:447-448` KEEPS an import when the\n * client reads it, so an import read by BOTH `metadata` and the JSX\n * lives in the projection and appears in NEITHER half exclusively.\n * Change its specifier and the complement is byte-identical → Fast\n * Refresh, stale `<title>`. Shared module-level LOCALS have the same\n * shape, so extending the complement a third time is a third bug.\n *\n * Both failures were UNDER-approximations, and under-approximating is the\n * unsafe direction. A precise reachability analysis over shared imports and\n * locals is the correct answer and is a later refinement; getting it subtly\n * wrong reproduces this bug again. Over-approximating can only err toward\n * RELOADING. A needless reload costs component state; a missed one ships a\n * stale `<head>` and calls it a hot update.\n *\n * THE ACCEPTED COST, which is not a bug to be optimised away: editing a\n * module-level helper read only by the JSX now reloads.\n *\n * The skeleton has to be captured BEFORE the edit, because by the time\n * `hotUpdate` runs Vite has already hard-invalidated the module and cleared its\n * `transformResult` (`onFileChange` → `invalidateModule`, which runs before any\n * `hotUpdate` hook). The `transform` spy below is that capture: it runs first in\n * the client environment, records the skeleton, and returns nothing so\n * projection still performs the real transform.\n */\ntype SkeletonCache = Map<string, string>;\n\n/**\n * What replaces a refresh-safe body in the skeleton. Its content is irrelevant —\n * only that it is CONSTANT, so two sources that differ solely inside a masked\n * body serialise identically.\n */\nconst MASKED_REFRESH_BODY = \"/*warlock:refresh-body*/\";\n\n/** React's own convention, and the one `react-refresh` itself uses: components are PascalCase. */\nfunction isComponentName(name: string | undefined): boolean {\n return typeof name === \"string\" && /^[A-Z]/.test(name);\n}\n\n/** The `body` node of a function-shaped expression/declaration, or `undefined` for anything else. */\nfunction functionBody(node: any): any | undefined {\n if (!node) return undefined;\n if (\n node.type === \"FunctionDeclaration\" ||\n node.type === \"FunctionExpression\" ||\n node.type === \"ArrowFunctionExpression\"\n ) {\n return node.body;\n }\n return undefined;\n}\n\n/**\n * Generic duck-typed identifier walk, the same shape `projection.ts`'s\n * `collectIdentifierNames` uses (it is not exported, and re-deriving one\n * OVER-collecting walk is safe here for the same reason it is safe there).\n *\n * Over-collecting — counting an object property key or a shadowing parameter\n * as a \"read\" — can only make the reachable set BIGGER, which can only UNMASK\n * more component bodies, which can only produce more reloads. The safe\n * direction.\n */\nfunction collectIdentifierNames(node: unknown, names: Set<string>): void {\n if (!node || typeof node !== \"object\") return;\n if (Array.isArray(node)) {\n for (const item of node) collectIdentifierNames(item, names);\n return;\n }\n const record = node as Record<string, unknown>;\n if (typeof record.type !== \"string\") return;\n if (record.type === \"Identifier\" || record.type === \"JSXIdentifier\") {\n names.add((record as any).name);\n }\n for (const key of Object.keys(record)) {\n if (key === \"type\" || key === \"start\" || key === \"end\" || key === \"loc\" || key === \"range\") continue;\n if (key === \"leadingComments\" || key === \"trailingComments\" || key === \"innerComments\" || key === \"extra\") {\n continue;\n }\n collectIdentifierNames(record[key], names);\n }\n}\n\n/** The module-scope names a top-level statement binds (the `export` wrapper looked through). */\nfunction topLevelBoundNames(stmt: any): Set<string> {\n const names = new Set<string>();\n const declaration = stmt.type === \"ExportNamedDeclaration\" ? stmt.declaration : stmt;\n if (!declaration) return names;\n if (declaration.type === \"VariableDeclaration\") {\n for (const declarator of declaration.declarations) {\n if (declarator.id?.type === \"Identifier\") names.add(declarator.id.name);\n }\n } else if (declaration.id?.type === \"Identifier\") {\n names.add(declaration.id.name);\n }\n return names;\n}\n\n/**\n * Every module-scope name reachable from one of the six server exports.\n *\n * Used ONLY to UNMASK: a PascalCase function that `metadata` or `loader` can\n * reach is not a component for this purpose, it is a server-side helper that\n * merely looks like one, and a change inside its body must reload. Seeded from\n * any top-level statement binding a `SERVER_EXPORT_NAMES` name — deliberately\n * looser than `projection.ts`'s own `isServerExportDeclaration` (no export\n * requirement, no single-declarator requirement), because seeding from MORE\n * statements can only unmask more, i.e. reload more.\n *\n * Fixpoint, not one pass, for the same reason projection's is: a server-only\n * helper can be reached only through another server-only helper.\n */\nfunction serverReachableNames(body: any[]): Set<string> {\n const reached = new Set<string>();\n const declarations = body\n .filter((stmt) => stmt.type !== \"ImportDeclaration\")\n .map((stmt) => ({ stmt, names: topLevelBoundNames(stmt) }));\n\n for (const { stmt, names } of declarations) {\n let isServerExport = false;\n for (const name of names) {\n if (SERVER_EXPORT_NAMES.has(name)) isServerExport = true;\n }\n if (isServerExport) collectIdentifierNames(stmt, reached);\n }\n\n for (let changed = true; changed; ) {\n changed = false;\n for (const { stmt, names } of declarations) {\n let isReached = false;\n for (const name of names) {\n if (reached.has(name)) isReached = true;\n }\n if (!isReached) continue;\n const before = reached.size;\n collectIdentifierNames(stmt, reached);\n if (reached.size !== before) changed = true;\n }\n }\n\n return reached;\n}\n\n/**\n * The body node to mask for a top-level statement, or `undefined` if this\n * statement is neither a component declaration nor an exported `register`\n * declaration.\n *\n * Recognised shapes, and only these:\n * - `export default function () {…}` / `export default () => …` — the page\n * component, whatever it is called.\n * - `function Name() {…}` / `const Name = () => …` (PascalCase, optionally\n * `export`ed) — a component declared alongside it.\n * - `export function register() {…}` / `export const register = () => …` —\n * the lifecycle hook whose replacement namespace is invoked by projection.\n *\n * Everything else — `memo(...)`/`forwardRef(...)` wrappers, classes,\n * lowercase helpers, every server export — is left UNMASKED and therefore\n * compared byte-for-byte. That costs Fast Refresh on those shapes and buys the\n * guarantee; see this seam's header.\n */\nfunction componentBodyToMask(stmt: any, serverReachable: Set<string>): any | undefined {\n if (stmt.type === \"ExportDefaultDeclaration\") return functionBody(stmt.declaration);\n\n if (stmt.type === \"ExportNamedDeclaration\") {\n const exported = stmt.declaration;\n if (exported?.type === \"FunctionDeclaration\" && exported.id?.name === \"register\") {\n return functionBody(exported);\n }\n if (exported?.type === \"VariableDeclaration\" && exported.declarations.length === 1) {\n const declarator = exported.declarations[0];\n if (declarator.id?.type === \"Identifier\" && declarator.id.name === \"register\") {\n return functionBody(declarator.init);\n }\n }\n }\n\n const declaration = stmt.type === \"ExportNamedDeclaration\" ? stmt.declaration : stmt;\n if (!declaration) return undefined;\n\n const named = (name: string | undefined, node: any) =>\n isComponentName(name) && !serverReachable.has(name as string) ? functionBody(node) : undefined;\n\n if (declaration.type === \"FunctionDeclaration\") {\n return named(declaration.id?.name, declaration);\n }\n if (declaration.type === \"VariableDeclaration\" && declaration.declarations.length === 1) {\n const declarator = declaration.declarations[0];\n if (declarator.id?.type !== \"Identifier\") return undefined;\n return named(declarator.id.name, declarator.init);\n }\n return undefined;\n}\n\n/**\n * The module source with every component and exported `register` body replaced\n * by a constant — the ONE value the reload decision compares across an edit.\n *\n * Everything outside those bodies survives verbatim: imports, signatures,\n * module-level declarations, all six server exports, and the comments and\n * whitespace between them. So the skeleton is unchanged iff the save touched\n * nothing but refresh-safe bodies.\n *\n * Returns `undefined` when the source does not parse — a half-typed file whose\n * error Vite is already reporting from projection's real `transform`. The\n * caller leaves the cache holding the last GOOD skeleton, so the next\n * successful save is still compared against the right baseline.\n */\nfunction captureSkeleton(code: string): string | undefined {\n let ast: ReturnType<typeof parse>;\n try {\n ast = parse(code, { sourceType: \"module\", plugins: [\"typescript\", \"jsx\"] });\n } catch {\n return undefined;\n }\n\n const body = ast.program.body as any[];\n const serverReachable = serverReachableNames(body);\n const magic = new MagicString(code);\n\n for (const stmt of body) {\n const bodyNode = componentBodyToMask(stmt, serverReachable);\n if (!bodyNode) continue;\n const start = bodyNode.start as number;\n const end = bodyNode.end as number;\n if (end > start) magic.overwrite(start, end, MASKED_REFRESH_BODY);\n }\n\n return magic.toString();\n}\n\n/**\n * Serves the client page registry at {@link CLIENT_PAGE_REGISTRY_ID}.\n *\n * Discovery runs INSIDE `load`, once per `load` call, and its result is NOT\n * cached across builds — the plugin holds no state at all. A registry cached\n * past the moment a page file appears is a page that silently 404s until\n * someone restarts the dev server, which is a far more expensive bug than\n * re-walking a source tree. Rollup calls `load` once per module per build, and\n * in dev Vite's module graph caches the transformed result until the module is\n * invalidated, so the walk is not per-request either way. (Invalidating that\n * dev-server cache when a page file is ADDED needs a `handleHotUpdate`/watcher\n * hook that belongs with the dev provider slice — see the followup.)\n *\n * `enforce: \"pre\"` and placed FIRST in `warlockClientBoundary`'s array — see\n * that function's comment in `index.ts` for why position is what it is, and\n * `page-registry-plugin.spec.ts` for the real-build proof that the pages this\n * module names still reach `projection()`.\n */\nexport function clientPageRegistry(options: ClientPageRegistryPluginOptions = {}): Plugin {\n const appRoot = path.resolve(options.appRoot ?? process.cwd());\n\n // Per-plugin-instance, so two composed pipelines never cross-contaminate.\n // Holds the last captured SKELETON (source with refresh-safe bodies masked) of\n // each server page module the client environment transformed — the \"before\"\n // side of the comparison in `hotUpdate`. See `captureSkeleton` above.\n const skeletonCache: SkeletonCache = new Map();\n\n return {\n name: \"warlock:client-page-registry\",\n enforce: \"pre\",\n resolveId(source) {\n if (source === CLIENT_PAGE_REGISTRY_ID) return RESOLVED_CLIENT_PAGE_REGISTRY_ID;\n return undefined;\n },\n load(id) {\n if (id !== RESOLVED_CLIENT_PAGE_REGISTRY_ID) return undefined;\n\n const pages = discoverPages({ appRoot, srcDir: options.srcDir });\n\n return eraseTypes(generateClientRegistry({ pages, toImportSpecifier }));\n },\n /**\n * Capture-only spy. Records the SKELETON of every server page module the\n * CLIENT environment transforms, and returns nothing so projection's own\n * `transform` still does the real work. SERVE-ONLY:\n * `this.environment.mode !== \"dev\"` skips it during `vite build`, where\n * there is no `hotUpdate` to feed and the extra parse would be pure cost.\n */\n transform(code, id) {\n if (this.environment?.mode !== \"dev\") return undefined;\n if (!isServerPageModule(id)) return undefined;\n\n const skeleton = captureSkeleton(code);\n if (skeleton !== undefined) skeletonCache.set(id, skeleton);\n\n return undefined;\n },\n /**\n * Applies the ruling (canon `6b240682`): Fast Refresh ONLY when the only\n * changes are inside component or exported `register` bodies.\n *\n * - Skeleton moved (an import, a module-level declaration, ANY server\n * export — with or without a simultaneous JSX change) → full reload.\n * - Skeleton unchanged → defer to Fast Refresh, zero reloads.\n *\n * Note what is NOT here: no attempt to name which half a shared import or\n * local belongs to. That question is what produced the two previous stale\n * `<head>` bugs; this seam refuses to answer it and reloads instead.\n * `hotUpdate` exists only on the dev server, so this is serve-only by\n * construction.\n */\n async hotUpdate(context) {\n if (isServerPageModule(context.file)) {\n const routeGraphHandled = await options.beforePageHotUpdate?.({\n file: context.file,\n type: context.type,\n });\n\n if (routeGraphHandled) return [];\n }\n\n // `create`/`delete` are page graph churn, not in-place edits — leave them\n // to Vite's normal handling (a new/removed module reloads on its own).\n if (context.type !== \"update\") return undefined;\n if (!isServerPageModule(context.file)) return undefined;\n\n const nextSource = await context.read();\n const next = captureSkeleton(nextSource);\n const prev = skeletonCache.get(context.file);\n\n // Refresh the cache for the next edit regardless of the decision below.\n if (next !== undefined) skeletonCache.set(context.file, next);\n\n // Could not parse the new source (Vite is already reporting that error),\n // or the client environment never transformed this module — which means\n // the browser is not holding this page, so there is no stale `<head>` to\n // ship and nothing a reload of some OTHER page would fix.\n if (next === undefined || prev === undefined) return undefined;\n\n // Anything outside a component body moved: the browser cannot hot-swap\n // it, so reload the document to re-run SSR and re-render `<head>`.\n // `path: \"*\"` matches Vite's own middleware-mode reload.\n if (prev !== next) {\n this.environment.hot.send({ type: \"full-reload\", path: \"*\" });\n\n // Empty module list: we've issued the update ourselves, so Vite should\n // not additionally push a Fast Refresh for the client module.\n return [];\n }\n\n // Only component bodies moved: defer to Vite's Fast Refresh with zero\n // reloads. A no-op re-save falls through the same harmless path.\n return undefined;\n },\n };\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import MagicString from "magic-string";
2
2
  import { Plugin } from "vite";
3
3
 
4
- //#region web/src/vite/projection.d.ts
4
+ //#region ../web/src/vite/projection.d.ts
5
5
  /**
6
6
  * Thrown when projection encounters an attribution-ambiguous top-level
7
7
  * statement (`c604f0bc` §3: "the compiler must not guess"). Carries the
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
  import { parse } from "@babel/parser";
3
3
  import MagicString from "magic-string";
4
4
 
5
- //#region web/src/vite/projection.ts
5
+ //#region ../web/src/vite/projection.ts
6
6
  /**
7
7
  * Projection — the compile-time AST transform that strips a page module's
8
8
  * six server exports before the CLIENT graph forms.