@warlock.js/web 5.5.0 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/CHANGELOG.md +53 -5
  2. package/esm/build/contribution.d.mts +2 -2
  3. package/esm/build/contribution.mjs +1 -1
  4. package/esm/build/contribution.mjs.map +1 -1
  5. package/esm/build/discover-pages.mjs +130 -208
  6. package/esm/build/discover-pages.mjs.map +1 -1
  7. package/esm/build/generate-pages-barrel.mjs +7 -5
  8. package/esm/build/generate-pages-barrel.mjs.map +1 -1
  9. package/esm/build/page-routes-manifest.mjs +1 -1
  10. package/esm/build/public-files.mjs +30 -2
  11. package/esm/build/public-files.mjs.map +1 -1
  12. package/esm/build/read-metadata-keys.mjs +225 -0
  13. package/esm/build/read-metadata-keys.mjs.map +1 -0
  14. package/esm/build/read-route-exports.mjs.map +1 -1
  15. package/esm/client/build-hydrated-tree.mjs +1 -1
  16. package/esm/client/build-hydrated-tree.mjs.map +1 -1
  17. package/esm/client/navigation/document-metadata.mjs +108 -0
  18. package/esm/client/navigation/document-metadata.mjs.map +1 -0
  19. package/esm/client/navigation/fetch-page-data.mjs +3 -10
  20. package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
  21. package/esm/client/navigation/navigation-root.mjs +24 -104
  22. package/esm/client/navigation/navigation-root.mjs.map +1 -1
  23. package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -1
  24. package/esm/client/navigation/use-query-string.d.mts +35 -0
  25. package/esm/client/navigation/use-query-string.mjs +68 -0
  26. package/esm/client/navigation/use-query-string.mjs.map +1 -0
  27. package/esm/client/runtime/index.d.mts +1 -1
  28. package/esm/client/runtime/index.mjs +1 -1
  29. package/esm/client/runtime/manifest.mjs.map +1 -1
  30. package/esm/client/runtime/matcher.mjs.map +1 -1
  31. package/esm/client/runtime/types.d.mts +1 -1
  32. package/esm/components/link.mjs.map +1 -1
  33. package/esm/core/src/router/normalize-route-path.mjs +33 -0
  34. package/esm/core/src/router/normalize-route-path.mjs.map +1 -0
  35. package/esm/{hydration → entry}/index.mjs +1 -1
  36. package/esm/entry/index.mjs.map +1 -0
  37. package/esm/hydration-payload.mjs +19 -10
  38. package/esm/hydration-payload.mjs.map +1 -1
  39. package/esm/index.d.mts +3 -2
  40. package/esm/index.mjs +3 -2
  41. package/esm/loaders.d.mts +13 -1
  42. package/esm/localization.mjs.map +1 -1
  43. package/esm/metadata.d.mts +4 -4
  44. package/esm/metadata.mjs.map +1 -1
  45. package/esm/node_modules/.pnpm/@mongez_concat-route@1.2.0/node_modules/@mongez/concat-route/esm/index.mjs +33 -0
  46. package/esm/node_modules/.pnpm/@mongez_concat-route@1.2.0/node_modules/@mongez/concat-route/esm/index.mjs.map +1 -0
  47. package/esm/{runtime/register-modules.d.mts → register-modules.d.mts} +1 -1
  48. package/esm/{runtime/register-modules.mjs → register-modules.mjs} +1 -1
  49. package/esm/register-modules.mjs.map +1 -0
  50. package/esm/route.d.mts +9 -0
  51. package/esm/routing/duplicate-route-path.mjs +22 -0
  52. package/esm/routing/duplicate-route-path.mjs.map +1 -0
  53. package/esm/routing/layout-level.mjs +50 -0
  54. package/esm/routing/layout-level.mjs.map +1 -0
  55. package/esm/routing/layout-policy.mjs.map +1 -1
  56. package/esm/routing/query-string.d.mts +23 -1
  57. package/esm/routing/query-string.mjs +23 -1
  58. package/esm/routing/query-string.mjs.map +1 -1
  59. package/esm/routing/route-identity.mjs +27 -2
  60. package/esm/routing/route-identity.mjs.map +1 -1
  61. package/esm/routing/route-table.mjs.map +1 -1
  62. package/esm/routing/router-events.d.mts +5 -4
  63. package/esm/routing/router-events.mjs.map +1 -1
  64. package/esm/server/create-page-route-handler.d.mts +2 -2
  65. package/esm/server/create-page-route-handler.mjs +18 -12
  66. package/esm/server/create-page-route-handler.mjs.map +1 -1
  67. package/esm/server/{dev-server.mjs → dev-error-transport.mjs} +2 -2
  68. package/esm/server/dev-error-transport.mjs.map +1 -0
  69. package/esm/server/execute-page-request.mjs +91 -35
  70. package/esm/server/execute-page-request.mjs.map +1 -1
  71. package/esm/server/execute-page-request.types.d.mts +21 -1
  72. package/esm/server/fold-layout-loaders.mjs +36 -0
  73. package/esm/server/fold-layout-loaders.mjs.map +1 -0
  74. package/esm/server/hydration-client-url.mjs +3 -3
  75. package/esm/server/hydration-client-url.mjs.map +1 -1
  76. package/esm/server/index.d.mts +4 -3
  77. package/esm/server/index.mjs +6 -6
  78. package/esm/server/install-page-routes-from-manifest.d.mts +10 -1
  79. package/esm/server/install-page-routes-from-manifest.mjs +61 -58
  80. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  81. package/esm/server/install-page-routes.d.mts +24 -26
  82. package/esm/server/install-page-routes.mjs +149 -60
  83. package/esm/server/install-page-routes.mjs.map +1 -1
  84. package/esm/server/install-production-page-routes.mjs.map +1 -1
  85. package/esm/server/match-page-route.mjs +6 -10
  86. package/esm/server/match-page-route.mjs.map +1 -1
  87. package/esm/server/not-found-page.d.mts +1 -0
  88. package/esm/server/not-found-page.mjs +8 -1
  89. package/esm/server/not-found-page.mjs.map +1 -1
  90. package/esm/server/page-file-change.mjs +1 -1
  91. package/esm/server/page-file-change.mjs.map +1 -1
  92. package/esm/server/page-module-shapes.d.mts +24 -0
  93. package/esm/server/page-route-reload.mjs +4 -3
  94. package/esm/server/page-route-reload.mjs.map +1 -1
  95. package/esm/server/register-production-public-files.mjs +26 -2
  96. package/esm/server/register-production-public-files.mjs.map +1 -1
  97. package/esm/server/render-page.d.mts +13 -49
  98. package/esm/server/render-page.mjs +14 -55
  99. package/esm/server/render-page.mjs.map +1 -1
  100. package/esm/server/resolve-page-metadata.mjs.map +1 -1
  101. package/esm/server/resolve-route-validation-input.mjs +11 -0
  102. package/esm/server/resolve-route-validation-input.mjs.map +1 -0
  103. package/esm/server/resolve-validation-data.mjs +1 -1
  104. package/esm/server/resolve-validation-data.mjs.map +1 -1
  105. package/esm/server/settle-page-response.mjs +5 -3
  106. package/esm/server/settle-page-response.mjs.map +1 -1
  107. package/esm/server/stylesheet-urls.mjs +78 -19
  108. package/esm/server/stylesheet-urls.mjs.map +1 -1
  109. package/esm/server/unregistered-pages.mjs +2 -1
  110. package/esm/server/unregistered-pages.mjs.map +1 -1
  111. package/esm/server/web-connector-factory.mjs +1 -1
  112. package/esm/server/web-connector-factory.mjs.map +1 -1
  113. package/esm/server/web-connector.mjs +25 -322
  114. package/esm/server/web-connector.mjs.map +1 -1
  115. package/esm/shared/module-key.mjs +15 -0
  116. package/esm/shared/module-key.mjs.map +1 -0
  117. package/esm/shared/to-posix.mjs +13 -0
  118. package/esm/shared/to-posix.mjs.map +1 -0
  119. package/esm/shared.mjs +24 -0
  120. package/esm/shared.mjs.map +1 -1
  121. package/esm/validation.d.mts +12 -1
  122. package/esm/vite/build-client.mjs +1 -1
  123. package/esm/vite/build-client.mjs.map +1 -1
  124. package/esm/vite/dev-server-config.mjs +240 -0
  125. package/esm/vite/dev-server-config.mjs.map +1 -0
  126. package/esm/vite/gate-a-resolve.d.mts +5 -4
  127. package/esm/vite/gate-a-resolve.mjs +32 -71
  128. package/esm/vite/gate-a-resolve.mjs.map +1 -1
  129. package/esm/vite/gate-b-secrets.mjs.map +1 -1
  130. package/esm/vite/gate-c-verify.d.mts +1 -1
  131. package/esm/vite/gate-c-verify.mjs +1 -1
  132. package/esm/vite/gate-c-verify.mjs.map +1 -1
  133. package/esm/vite/hydration-entries.mjs +6 -8
  134. package/esm/vite/hydration-entries.mjs.map +1 -1
  135. package/esm/vite/index.d.mts +1 -1
  136. package/esm/vite/index.mjs +4 -110
  137. package/esm/vite/index.mjs.map +1 -1
  138. package/esm/vite/page-registry-plugin.mjs +9 -26
  139. package/esm/vite/page-registry-plugin.mjs.map +1 -1
  140. package/esm/vite/projection.mjs.map +1 -1
  141. package/esm/vite/react-refresh-preamble.mjs +90 -0
  142. package/esm/vite/react-refresh-preamble.mjs.map +1 -0
  143. package/esm/vite/ssr-client-view.mjs +135 -0
  144. package/esm/vite/ssr-client-view.mjs.map +1 -0
  145. package/llms-full.txt +564 -482
  146. package/llms.txt +2 -2
  147. package/package.json +6 -6
  148. package/skills/create-a-page/SKILL.md +354 -295
  149. package/skills/navigate-on-the-client/SKILL.md +212 -189
  150. package/esm/hydration/index.mjs.map +0 -1
  151. package/esm/runtime/register-modules.mjs.map +0 -1
  152. package/esm/server/dev-server.mjs.map +0 -1
  153. /package/esm/{hydration → entry}/index.d.mts +0 -0
@@ -1 +1 @@
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
+ {"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 } from \"../build/discover-pages\";\nimport { generateClientRegistry } from \"../build/generate-client-registry\";\nimport { toPosix } from \"../shared/to-posix\";\nimport { isProjectableFile, 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 goes through the one shared `toPosix`\n * (`../shared/to-posix`) — keeping the drive colon (`D:/...`) is exactly what\n * 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 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\")\n continue;\n if (\n key === \"leadingComments\" ||\n key === \"trailingComments\" ||\n key === \"innerComments\" ||\n key === \"extra\"\n ) {\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 (!isProjectableFile(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 (isProjectableFile(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 (!isProjectableFile(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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAa,0BAA0B;;;;;AAMvC,MAAa,mCAAmC,KAAK;;;;;;;;;AAUrD,SAAgB,6BAA6B,MAA2B;CACtE,MAAM,cAAc,KAAK,aAAa,OAAO;CAC7C,MAAM,iBAAiB,YAAY,cAAc,gCAAgC;CAEjF,IAAI,gBAAgB,YAAY,iBAAiB,cAAc;CAE/D,KAAK,IAAI,KAAK;EAAE,MAAM;EAAe,MAAM;CAAI,CAAC;AAClD;;;;;;;;;;;;;;AAgCA,SAAS,kBAAkB,kBAAkC;CAC3D,OAAO,QAAQ,KAAK,QAAQ,gBAAgB,CAAC;AAC/C;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAS,WAAW,QAAwB;CAC1C,MAAM,MAAM,MAAM,QAAQ;EAAE,YAAY;EAAU,SAAS,CAAC,YAAY;CAAE,CAAC;CAC3E,MAAM,QAAQ,IAAI,YAAY,MAAM;CAEpC,KAAK,MAAM,aAAa,IAAI,QAAQ,MAAe;EACjD,IAAI,UAAU,SAAS,uBAAuB,UAAU,eAAe,QAAQ;GAC7E,IAAI,MAAM,UAAU;GACpB,IAAI,OAAO,SAAS,QAAQ,OAAO,MAAM,OAAO,MAAM,OAAO;QACxD,IAAI,OAAO,SAAS,MAAM,OAAO;GACtC,MAAM,OAAO,UAAU,OAAiB,GAAG;GAC3C;EACF;EAEA,MAAM,cACJ,UAAU,SAAS,2BAA2B,UAAU,cAAc;EAExE,IAAI,aAAa,SAAS,uBAAuB;EAEjD,KAAK,MAAM,cAAc,YAAY,cAAuB;GAC1D,MAAM,aAAa,WAAW,IAAI;GAClC,IAAI,YAAY,MAAM,OAAO,WAAW,OAAiB,WAAW,GAAa;EACnF;CACF;CAEA,OAAO,MAAM,SAAS;AACxB;;;;;;AAgEA,MAAM,sBAAsB;;AAG5B,SAAS,gBAAgB,MAAmC;CAC1D,OAAO,OAAO,SAAS,YAAY,SAAS,KAAK,IAAI;AACvD;;AAGA,SAAS,aAAa,MAA4B;CAChD,IAAI,CAAC,MAAM,OAAO;CAClB,IACE,KAAK,SAAS,yBACd,KAAK,SAAS,wBACd,KAAK,SAAS,2BAEd,OAAO,KAAK;AAGhB;;;;;;;;;;;AAYA,SAAS,uBAAuB,MAAe,OAA0B;CACvE,IAAI,CAAC,QAAQ,OAAO,SAAS,UAAU;CACvC,IAAI,MAAM,QAAQ,IAAI,GAAG;EACvB,KAAK,MAAM,QAAQ,MAAM,uBAAuB,MAAM,KAAK;EAC3D;CACF;CACA,MAAM,SAAS;CACf,IAAI,OAAO,OAAO,SAAS,UAAU;CACrC,IAAI,OAAO,SAAS,gBAAgB,OAAO,SAAS,iBAClD,MAAM,IAAK,OAAe,IAAI;CAEhC,KAAK,MAAM,OAAO,OAAO,KAAK,MAAM,GAAG;EACrC,IAAI,QAAQ,UAAU,QAAQ,WAAW,QAAQ,SAAS,QAAQ,SAAS,QAAQ,SACjF;EACF,IACE,QAAQ,qBACR,QAAQ,sBACR,QAAQ,mBACR,QAAQ,SAER;EAEF,uBAAuB,OAAO,MAAM,KAAK;CAC3C;AACF;;AAGA,SAAS,mBAAmB,MAAwB;CAClD,MAAM,wBAAQ,IAAI,IAAY;CAC9B,MAAM,cAAc,KAAK,SAAS,2BAA2B,KAAK,cAAc;CAChF,IAAI,CAAC,aAAa,OAAO;CACzB,IAAI,YAAY,SAAS,uBACvB;OAAK,MAAM,cAAc,YAAY,cACnC,IAAI,WAAW,IAAI,SAAS,cAAc,MAAM,IAAI,WAAW,GAAG,IAAI;CACxE,OACK,IAAI,YAAY,IAAI,SAAS,cAClC,MAAM,IAAI,YAAY,GAAG,IAAI;CAE/B,OAAO;AACT;;;;;;;;;;;;;;;AAgBA,SAAS,qBAAqB,MAA0B;CACtD,MAAM,0BAAU,IAAI,IAAY;CAChC,MAAM,eAAe,KAClB,QAAQ,SAAS,KAAK,SAAS,mBAAmB,CAAC,CACnD,KAAK,UAAU;EAAE;EAAM,OAAO,mBAAmB,IAAI;CAAE,EAAE;CAE5D,KAAK,MAAM,EAAE,MAAM,WAAW,cAAc;EAC1C,IAAI,iBAAiB;EACrB,KAAK,MAAM,QAAQ,OACjB,IAAI,oBAAoB,IAAI,IAAI,GAAG,iBAAiB;EAEtD,IAAI,gBAAgB,uBAAuB,MAAM,OAAO;CAC1D;CAEA,KAAK,IAAI,UAAU,MAAM,UAAU;EACjC,UAAU;EACV,KAAK,MAAM,EAAE,MAAM,WAAW,cAAc;GAC1C,IAAI,YAAY;GAChB,KAAK,MAAM,QAAQ,OACjB,IAAI,QAAQ,IAAI,IAAI,GAAG,YAAY;GAErC,IAAI,CAAC,WAAW;GAChB,MAAM,SAAS,QAAQ;GACvB,uBAAuB,MAAM,OAAO;GACpC,IAAI,QAAQ,SAAS,QAAQ,UAAU;EACzC;CACF;CAEA,OAAO;AACT;;;;;;;;;;;;;;;;;;;AAoBA,SAAS,oBAAoB,MAAW,iBAA+C;CACrF,IAAI,KAAK,SAAS,4BAA4B,OAAO,aAAa,KAAK,WAAW;CAElF,IAAI,KAAK,SAAS,0BAA0B;EAC1C,MAAM,WAAW,KAAK;EACtB,IAAI,UAAU,SAAS,yBAAyB,SAAS,IAAI,SAAS,YACpE,OAAO,aAAa,QAAQ;EAE9B,IAAI,UAAU,SAAS,yBAAyB,SAAS,aAAa,WAAW,GAAG;GAClF,MAAM,aAAa,SAAS,aAAa;GACzC,IAAI,WAAW,IAAI,SAAS,gBAAgB,WAAW,GAAG,SAAS,YACjE,OAAO,aAAa,WAAW,IAAI;EAEvC;CACF;CAEA,MAAM,cAAc,KAAK,SAAS,2BAA2B,KAAK,cAAc;CAChF,IAAI,CAAC,aAAa,OAAO;CAEzB,MAAM,SAAS,MAA0B,SACvC,gBAAgB,IAAI,KAAK,CAAC,gBAAgB,IAAI,IAAc,IAAI,aAAa,IAAI,IAAI;CAEvF,IAAI,YAAY,SAAS,uBACvB,OAAO,MAAM,YAAY,IAAI,MAAM,WAAW;CAEhD,IAAI,YAAY,SAAS,yBAAyB,YAAY,aAAa,WAAW,GAAG;EACvF,MAAM,aAAa,YAAY,aAAa;EAC5C,IAAI,WAAW,IAAI,SAAS,cAAc,OAAO;EACjD,OAAO,MAAM,WAAW,GAAG,MAAM,WAAW,IAAI;CAClD;AAEF;;;;;;;;;;;;;;;AAgBA,SAAS,gBAAgB,MAAkC;CACzD,IAAI;CACJ,IAAI;EACF,MAAM,MAAM,MAAM;GAAE,YAAY;GAAU,SAAS,CAAC,cAAc,KAAK;EAAE,CAAC;CAC5E,QAAQ;EACN;CACF;CAEA,MAAM,OAAO,IAAI,QAAQ;CACzB,MAAM,kBAAkB,qBAAqB,IAAI;CACjD,MAAM,QAAQ,IAAI,YAAY,IAAI;CAElC,KAAK,MAAM,QAAQ,MAAM;EACvB,MAAM,WAAW,oBAAoB,MAAM,eAAe;EAC1D,IAAI,CAAC,UAAU;EACf,MAAM,QAAQ,SAAS;EACvB,MAAM,MAAM,SAAS;EACrB,IAAI,MAAM,OAAO,MAAM,UAAU,OAAO,KAAK,mBAAmB;CAClE;CAEA,OAAO,MAAM,SAAS;AACxB;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,mBAAmB,UAA2C,CAAC,GAAW;CACxF,MAAM,UAAU,KAAK,QAAQ,QAAQ,WAAW,QAAQ,IAAI,CAAC;CAM7D,MAAM,gCAA+B,IAAI,IAAI;CAE7C,OAAO;EACL,MAAM;EACN,SAAS;EACT,UAAU,QAAQ;GAChB,IAAI,oCAAoC,OAAO;EAEjD;EACA,KAAK,IAAI;GACP,IAAI,OAAO,kCAAkC,OAAO;GAIpD,OAAO,WAAW,uBAAuB;IAAE,OAF7B,cAAc;KAAE;KAAS,QAAQ,QAAQ;IAAO,CAEf;IAAG;GAAkB,CAAC,CAAC;EACxE;;;;;;;;EAQA,UAAU,MAAM,IAAI;GAClB,IAAI,KAAK,aAAa,SAAS,OAAO,OAAO;GAC7C,IAAI,CAAC,kBAAkB,EAAE,GAAG,OAAO;GAEnC,MAAM,WAAW,gBAAgB,IAAI;GACrC,IAAI,aAAa,QAAW,cAAc,IAAI,IAAI,QAAQ;EAG5D;;;;;;;;;;;;;;;EAeA,MAAM,UAAU,SAAS;GACvB,IAAI,kBAAkB,QAAQ,IAAI,GAMhC;QAAI,MAL4B,QAAQ,sBAAsB;KAC5D,MAAM,QAAQ;KACd,MAAM,QAAQ;IAChB,CAAC,GAEsB,OAAO,CAAC;GAAC;GAKlC,IAAI,QAAQ,SAAS,UAAU,OAAO;GACtC,IAAI,CAAC,kBAAkB,QAAQ,IAAI,GAAG,OAAO;GAG7C,MAAM,OAAO,gBAAgB,MADJ,QAAQ,KAAK,CACC;GACvC,MAAM,OAAO,cAAc,IAAI,QAAQ,IAAI;GAG3C,IAAI,SAAS,QAAW,cAAc,IAAI,QAAQ,MAAM,IAAI;GAM5D,IAAI,SAAS,UAAa,SAAS,QAAW,OAAO;GAKrD,IAAI,SAAS,MAAM;IACjB,KAAK,YAAY,IAAI,KAAK;KAAE,MAAM;KAAe,MAAM;IAAI,CAAC;IAI5D,OAAO,CAAC;GACV;EAKF;CACF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"projection.mjs","names":[],"sources":["../../../../../../../web/src/vite/projection.ts"],"sourcesContent":["/**\n * Projection — the compile-time AST transform that strips a page module's\n * six server exports before the CLIENT graph forms.\n *\n * Removes `export const route/middleware/validation/loader/metadata/prefix = ...`\n * (const-arrow form) and `export async function loader(...) {...}`\n * (function-declaration form — a page declares these as separate named\n * exports, not one fused object, so both forms are real), plus any import\n * OR top-level declaration that becomes\n * unreferenced ONLY as a result of that removal. The default export (the\n * page component) and every other non-server-named export — including the\n * synchronous, no-argument `register()` lifecycle hook — survive\n * unconditionally, regardless of what they reference — classification is by\n * FILE, not by what an export does with data (`c604f0bc` §9).\n *\n * ATTRIBUTION APPLIES TO LOCAL DECLARATIONS, NOT JUST IMPORTS. A page or\n * layout hoists things to module scope for two ordinary reasons, and the\n * reference graph tells them apart without guessing:\n *\n * `const publishCart: Middleware = ...` + `export const middleware =\n * [publishCart]` (`v5/app/.../products/web/layout.tsx:30,43`) — the only\n * reader is a server export being removed, so the binding goes with it.\n *\n * `const COMMON_TIMEZONES = [...]` read by the default export\n * (`v5/app/.../account/settings.page.tsx:118,129`) — a surviving reader, so\n * it survives.\n *\n * That is the SAME rule already applied to import bindings, extended to the\n * other binding kind. It is deliberately not a widening of\n * `SERVER_EXPORT_NAMES`, and not \"accept what I don't recognise\": a\n * declaration is kept when something the client keeps reads it, dropped when\n * nothing does AND dropping it cannot delete a side effect, and REFUSED\n * otherwise — see `isDefinitionShapedInit`.\n *\n * This is NOT Gate A (`resolveId` path refusal), Gate B (inline secret\n * reads) or Gate C (emitted-output verification) — those are separate,\n * later slices. Projection runs first; the gates enforce after.\n */\nimport { parse } from \"@babel/parser\";\nimport MagicString from \"magic-string\";\nimport path from \"node:path\";\nimport type { Plugin } from \"vite\";\n\n/**\n * Exported so Gate C (`gate-c-verify.ts`) can re-derive \"does the emitted\n * bundle contain a server export as a top-level binding\" from this exact set\n * rather than hand-typing a second copy that could drift from projection's\n * own list.\n */\nexport const SERVER_EXPORT_NAMES = new Set([\n \"route\",\n \"middleware\",\n \"validation\",\n \"loader\",\n \"metadata\",\n \"prefix\",\n]);\n\n/**\n * Recognized client-safe assets that always survive projection untouched,\n * whether imported bare (`import \"./x.css\"`) or with specifiers\n * (`import styles from \"./x.module.css\"`) — `c604f0bc` §3 names CSS\n * explicitly; the rest of this list is the same \"never guess, but a known\n * asset extension is not ambiguous\" reasoning extended to the other static\n * asset kinds Vite treats as URL/asset imports, not executable code.\n */\nconst ASSET_EXTENSION_RE =\n /\\.(css|scss|sass|less|styl|stylus|svg|png|jpe?g|gif|webp|avif|ico|woff2?|ttf|eot|otf)(\\?.*)?$/i;\n\n/**\n * Top-level statement types that need no ambiguity check and are never\n * touched by removal: import declarations are handled by their own\n * survives/orphaned logic below, and every export (other than the 6 server\n * names) plus type-only declarations survive unconditionally per\n * `c604f0bc` §9 (\"classify FILES, not the data they touch\").\n *\n * `ExportAllDeclaration` (`export * from \"./x\"` / `export * as ns from\n * \"./x\"`) is deliberately NOT in this set — it can forward ANY name from its\n * source module, including a server export, and is refused explicitly below\n * rather than assumed safe.\n */\nconst ALWAYS_SAFE_STATEMENT_TYPES = new Set([\n \"ExportNamedDeclaration\",\n \"ExportDefaultDeclaration\",\n \"TSInterfaceDeclaration\",\n \"TSTypeAliasDeclaration\",\n \"EmptyStatement\",\n]);\n\n/**\n * Thrown when projection encounters an attribution-ambiguous top-level\n * statement (`c604f0bc` §3: \"the compiler must not guess\"). Carries the\n * file/statement/fix fields the plugin's `transform` hook formats into the\n * build-failure message — never silently kept or silently dropped.\n */\nexport class ProjectionAmbiguityError extends Error {\n constructor(\n public readonly file: string,\n public readonly statement: string,\n public readonly line: number,\n public readonly explanation: string,\n public readonly fix: string,\n ) {\n super(\n [\n `Projection refused to guess: an ambiguous top-level statement in the client build.`,\n ``,\n `File: ${file}:${line}`,\n `Statement: ${statement}`,\n `Cause: ${explanation}`,\n `Fix: ${fix}`,\n ].join(\"\\n\"),\n );\n this.name = \"ProjectionAmbiguityError\";\n }\n}\n\nexport interface ProjectionResult {\n code: string;\n map: ReturnType<MagicString[\"generateMap\"]>;\n}\n\n/** A top-level `const`/`function`/`class` awaiting attribution by reference. */\ninterface LocalDeclaration {\n stmt: any;\n /** The module-scope names it binds. */\n names: Set<string>;\n /** Whether removing it could delete a side effect — see `isDefinitionShapedInit`. */\n definitionShaped: boolean;\n removed: boolean;\n}\n\nfunction hasSurvivingReader(\n local: LocalDeclaration,\n survivingNames: Set<string>,\n): boolean {\n for (const name of local.names) {\n if (survivingNames.has(name)) return true;\n }\n return false;\n}\n\nfunction isKnownSafeAsset(source: string): boolean {\n return ASSET_EXTENSION_RE.test(source);\n}\n\n/**\n * Matches `export const <name> = ...` only when the declaration has exactly\n * one declarator — every server export in every fixture and v5/app page is\n * written one-const-per-export (`product-details.page.tsx:15-18,42-69,71-74`);\n * a multi-declarator `export const a = 1, b = 2` is left to the ambiguity\n * path below rather than guessing which half is server-only.\n */\nfunction isServerExportDeclaration(stmt: any): boolean {\n if (stmt.type !== \"ExportNamedDeclaration\" || !stmt.declaration) return false;\n const decl = stmt.declaration;\n if (decl.type === \"VariableDeclaration\" && decl.declarations.length === 1) {\n const id = decl.declarations[0].id;\n return id?.type === \"Identifier\" && SERVER_EXPORT_NAMES.has(id.name);\n }\n if (decl.type === \"FunctionDeclaration\") {\n return !!decl.id && SERVER_EXPORT_NAMES.has(decl.id.name);\n }\n return false;\n}\n\n/**\n * Generic duck-typed AST walk (no `@babel/traverse` dependency — this\n * package only needs `@babel/parser` + `@babel/types`-shaped nodes).\n * Collects every `Identifier`/`JSXIdentifier` name reachable from `node`,\n * used to decide whether an import binding still has a reader once the 6\n * server exports are gone. Over-collecting (e.g. counting an object\n * property key as a \"use\") only ever biases toward KEEPING an import, never\n * toward dropping one that is still needed — the safe direction for a\n * heuristic that must not guess in the removal 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 (\n key === \"type\" ||\n key === \"start\" ||\n key === \"end\" ||\n key === \"loc\" ||\n key === \"range\"\n )\n continue;\n if (\n key === \"leadingComments\" ||\n key === \"trailingComments\" ||\n key === \"innerComments\" ||\n key === \"extra\"\n ) {\n continue;\n }\n collectIdentifierNames(record[key], names);\n }\n}\n\n/**\n * Top-level statements that BIND a name, and are therefore attributable by the\n * reference graph rather than by guessing. Everything outside this set and\n * `ALWAYS_SAFE_STATEMENT_TYPES` declares nothing — a bare `console.log(\"boot\")`\n * has no binding to trace a reader from, which is why it stays a hard refusal.\n */\nconst DECLARATION_STATEMENT_TYPES = new Set([\n \"VariableDeclaration\",\n \"FunctionDeclaration\",\n \"ClassDeclaration\",\n]);\n\nfunction collectPatternNames(node: any, names: Set<string>): void {\n if (!node || typeof node !== \"object\") return;\n switch (node.type) {\n case \"Identifier\":\n names.add(node.name);\n return;\n case \"ObjectPattern\":\n for (const property of node.properties) {\n collectPatternNames(\n property.type === \"RestElement\" ? property.argument : property.value,\n names,\n );\n }\n return;\n case \"ArrayPattern\":\n for (const element of node.elements) collectPatternNames(element, names);\n return;\n case \"AssignmentPattern\":\n collectPatternNames(node.left, names);\n return;\n case \"RestElement\":\n collectPatternNames(node.argument, names);\n return;\n }\n}\n\n/** The names a top-level declaration introduces into module scope. */\nfunction declaredNames(stmt: any): Set<string> {\n const names = new Set<string>();\n if (stmt.type === \"VariableDeclaration\") {\n for (const declarator of stmt.declarations)\n collectPatternNames(declarator.id, names);\n } else if (stmt.id?.type === \"Identifier\") {\n names.add(stmt.id.name);\n }\n return names;\n}\n\n/**\n * Whether EVALUATING this initializer can run anything.\n *\n * This is the whole safety argument for removing an unreferenced declaration.\n * A function definition or a literal only creates a value, so dropping it can\n * only drop a binding nothing reads. A call, a `new`, an `await`, a member\n * access (a getter) — those can do work, and \"was that work for the server or\n * for the client?\" is precisely the question projection must not answer by\n * guessing (`c604f0bc` §3). `const _ = installPolyfill()` with no reader at all\n * is the shape this predicate exists to refuse rather than silently delete.\n *\n * Conservative by construction: an unrecognized node type is NOT\n * definition-shaped, so a new syntax form arrives as a refusal with a message,\n * never as a silent removal.\n */\nfunction isDefinitionShapedInit(node: any): boolean {\n // `let x;` — a bare binding with nothing to evaluate.\n if (!node) return true;\n\n switch (node.type) {\n case \"ArrowFunctionExpression\":\n case \"FunctionExpression\":\n case \"StringLiteral\":\n case \"NumericLiteral\":\n case \"BooleanLiteral\":\n case \"NullLiteral\":\n case \"BigIntLiteral\":\n case \"RegExpLiteral\":\n // A bare identifier read is a binding lookup, not a computation.\n case \"Identifier\":\n return true;\n case \"TemplateLiteral\":\n return node.expressions.every((expression: any) =>\n isDefinitionShapedInit(expression),\n );\n case \"UnaryExpression\":\n return (\n node.operator !== \"delete\" && isDefinitionShapedInit(node.argument)\n );\n case \"ArrayExpression\":\n return node.elements.every(\n (element: any) =>\n element === null ||\n (element.type !== \"SpreadElement\" && isDefinitionShapedInit(element)),\n );\n case \"ObjectExpression\":\n // Spread and computed keys both evaluate arbitrary expressions; a getter\n // or setter defines a body that runs on ACCESS, which the surviving half\n // could still trigger — none of them are definitions.\n return node.properties.every(\n (property: any) =>\n property.type === \"ObjectProperty\" &&\n !property.computed &&\n isDefinitionShapedInit(property.value),\n );\n // TS-only wrappers erase at compile time; look through them.\n case \"TSAsExpression\":\n case \"TSSatisfiesExpression\":\n case \"TSNonNullExpression\":\n case \"TSTypeAssertion\":\n case \"TSInstantiationExpression\":\n case \"ParenthesizedExpression\":\n return isDefinitionShapedInit(node.expression);\n default:\n return false;\n }\n}\n\n/**\n * Statement-level form of the above. A `class` is excluded on purpose: static\n * blocks, decorators and computed member keys all run at class-definition time,\n * so a class is only ever kept or refused, never silently removed.\n */\nfunction isDefinitionShapedStatement(stmt: any): boolean {\n if (stmt.type === \"FunctionDeclaration\") return true;\n if (stmt.type !== \"VariableDeclaration\") return false;\n return stmt.declarations.every((declarator: any) =>\n isDefinitionShapedInit(declarator.init),\n );\n}\n\nfunction removeStatement(s: MagicString, code: string, node: any): void {\n let end = node.end as number;\n // Swallow one trailing newline so a removed statement doesn't leave a\n // blank line behind — cosmetic only, the output's correctness never\n // depends on it.\n if (code[end] === \"\\r\" && code[end + 1] === \"\\n\") end += 2;\n else if (code[end] === \"\\n\") end += 1;\n s.remove(node.start as number, end);\n}\n\nfunction statementSnippet(code: string, node: any): string {\n return code\n .slice(node.start as number, node.end as number)\n .split(\"\\n\")[0]\n .trim();\n}\n\n/**\n * The transform itself: parse, remove the 6 server exports and every import\n * orphaned only by that removal, fail closed on anything attribution-\n * ambiguous. `filePath` is only used for error messages (`c604f0bc` §7 —\n * fence errors must name the file).\n */\nexport function projectModule(\n code: string,\n filePath: string,\n): ProjectionResult {\n const ast = parse(code, {\n sourceType: \"module\",\n plugins: [\"typescript\", \"jsx\"],\n });\n\n const s = new MagicString(code);\n const body = ast.program.body as any[];\n\n const removedServerExports: any[] = [];\n const importDeclarations: any[] = [];\n const localDeclarations: LocalDeclaration[] = [];\n\n for (const stmt of body) {\n if (stmt.type === \"ImportDeclaration\") {\n importDeclarations.push(stmt);\n continue;\n }\n const isNamespaceReexport =\n stmt.type === \"ExportNamedDeclaration\" &&\n stmt.source != null &&\n (stmt.specifiers as any[] | undefined)?.some(\n (specifier) => specifier.type === \"ExportNamespaceSpecifier\",\n );\n\n if (stmt.type === \"ExportAllDeclaration\" || isNamespaceReexport) {\n // `export * from \"./source\"` (and `export * as ns from \"./source\"`,\n // which Babel parses as an `ExportNamedDeclaration` carrying an\n // `ExportNamespaceSpecifier` rather than as `ExportAllDeclaration` —\n // hence the second check above) re-exports every name the source\n // module exports, sight unseen.\n // Projection classifies by file (`c604f0bc` §9) and never opens a\n // second file to resolve what a re-export actually forwards — doing so\n // would mean parsing and walking the source module too, i.e. a second\n // parser. Whether the source exports one of the 6 server names is\n // therefore unknowable here, so this is attribution-ambiguous the same\n // way an unrecognized top-level statement is, and gets the same\n // refusal rather than an assumption that it is safe.\n throw new ProjectionAmbiguityError(\n filePath,\n statementSnippet(code, stmt),\n stmt.loc.start.line,\n `a star re-export forwards every name the source module exports, including possibly one of the 6 known server exports (route, middleware, validation, loader, metadata, prefix) — projection cannot inspect the source module's exports without parsing a second file, so it can't tell whether this leaks a server-only binding into the client bundle`,\n `replace the star re-export with explicit named re-exports (export { ComponentA, ComponentB } from \"./source\"), listing only the client-safe names`,\n );\n }\n if (isServerExportDeclaration(stmt)) {\n removedServerExports.push(stmt);\n continue;\n }\n if (ALWAYS_SAFE_STATEMENT_TYPES.has(stmt.type)) continue;\n if (DECLARATION_STATEMENT_TYPES.has(stmt.type)) {\n // Attributable by the reference graph — decided below, once it is known\n // which statements survive. NOT accepted here.\n localDeclarations.push({\n stmt,\n names: declaredNames(stmt),\n definitionShaped: isDefinitionShapedStatement(stmt),\n removed: false,\n });\n continue;\n }\n\n // Attribution-IMPOSSIBLE: not an import, not one of the 6 known server\n // exports, not another export, not a type-only declaration, and it binds\n // no name for a reader to point at. Fail closed rather than guess which\n // side of the fence it belongs on (`c604f0bc` §3).\n throw new ProjectionAmbiguityError(\n filePath,\n statementSnippet(code, stmt),\n stmt.loc.start.line,\n `top-level executable code that declares nothing — outside the 6 known server exports (route, middleware, validation, loader, metadata, prefix), and binding no name, so projection has no reader to attribute it by and can't tell whether it belongs to the server or the client`,\n `move universal static declarations and their imports into export function register(), or mark the code with an explicit .server/.client file; server-only work can instead move inside one of the 6 declared server exports`,\n );\n }\n\n const removedLocals = new Set<any>();\n\n /**\n * Every name READ by something that survives projection. Imports are excluded\n * so an import specifier never counts as a use of itself, and a local\n * declaration does not count as a use of ITSELF either — otherwise a\n * self-recursive server-only helper would pin its own binding alive forever.\n *\n * Over-collecting (an object property key, a shadowing parameter) only ever\n * biases toward KEEPING, never toward dropping something still needed — the\n * safe direction for a heuristic that must not guess in the removal\n * direction.\n */\n function collectSurvivingNames(): Set<string> {\n const names = new Set<string>();\n for (const stmt of body) {\n if (stmt.type === \"ImportDeclaration\") continue;\n if (removedServerExports.includes(stmt) || removedLocals.has(stmt))\n continue;\n const own = new Set<string>();\n collectIdentifierNames(stmt, own);\n if (DECLARATION_STATEMENT_TYPES.has(stmt.type)) {\n for (const name of declaredNames(stmt)) own.delete(name);\n }\n for (const name of own) names.add(name);\n }\n return names;\n }\n\n // Fixpoint, not one pass: a server-only helper can be reached only through\n // ANOTHER server-only helper, and dropping the first orphans the second.\n let survivingNames = collectSurvivingNames();\n for (let changed = true; changed;) {\n changed = false;\n for (const local of localDeclarations) {\n if (local.removed || !local.definitionShaped) continue;\n if (hasSurvivingReader(local, survivingNames)) continue;\n local.removed = true;\n removedLocals.add(local.stmt);\n changed = true;\n }\n if (changed) survivingNames = collectSurvivingNames();\n }\n\n for (const local of localDeclarations) {\n if (local.removed || hasSurvivingReader(local, survivingNames)) continue;\n\n // Nothing the client keeps reads it, so it belongs to the server exports\n // being removed — but its initializer can RUN, and a side effect is not\n // attributable by the reference graph. Keeping it ships server work to the\n // browser; dropping it deletes a side effect the client may depend on.\n // Refuse rather than pick one (`c604f0bc` §3).\n throw new ProjectionAmbiguityError(\n filePath,\n statementSnippet(code, local.stmt),\n local.stmt.loc.start.line,\n `a top-level declaration read only by the server exports being removed, but whose initializer executes code rather than just defining a value — projection can't tell whether that work is server-only or a side effect the client depends on`,\n `move universal static declarations and their imports into export function register(), move a server-only initializer inside the export that reads it, or split it into an explicit .server/.client file`,\n );\n }\n\n for (const decl of importDeclarations) {\n const source = decl.source.value as string;\n if (isKnownSafeAsset(source)) continue; // always survives, no orphan check\n\n if (decl.specifiers.length === 0) {\n // A bare side-effect import that isn't a recognized asset extension is\n // just as attribution-ambiguous as an executable statement — could be\n // a server-only side effect or something the client genuinely needs.\n throw new ProjectionAmbiguityError(\n filePath,\n statementSnippet(code, decl),\n decl.loc.start.line,\n `a bare side-effect import with no recognized client-safe asset extension — projection can't tell if it belongs only to the server exports being removed or must ship to the client`,\n `move universal static declarations and their imports into export function register(), or mark it with an explicit .server/.client file; server-only work can instead move inside one of the 6 declared server exports`,\n );\n }\n\n const isUsed = decl.specifiers.some((spec: any) =>\n survivingNames.has(spec.local.name),\n );\n if (!isUsed) removeStatement(s, code, decl);\n }\n\n for (const stmt of removedServerExports) {\n removeStatement(s, code, stmt);\n }\n\n for (const stmt of removedLocals) {\n removeStatement(s, code, stmt);\n }\n\n return {\n code: s.toString(),\n map: s.generateMap({ hires: true, source: filePath }),\n };\n}\n\nexport function isProjectableFile(id: string): boolean {\n const base = path.basename(id.split(\"?\")[0]);\n if (/\\.page\\.tsx?$/.test(base)) return true;\n if (base === \"layout.tsx\" || base === \"layout.ts\") return true;\n // NAMED layouts — `dashboard.layout.tsx` and friends — are subjects too.\n //\n // Only the exact name `layout.tsx` is POSITIONAL (discovered by its folder).\n // A named layout is addressed by import instead, which is a documented part of\n // the contract: `v5/app/src/web/layouts/dashboard.layout.tsx` says so in its\n // own header, and modules opt in with two re-export lines.\n //\n // Projection did not recognise them, and the consequence was not cosmetic.\n // `dashboard.layout.tsx` calls `navService.forUser()` INSIDE its `loader` —\n // exactly where server work belongs. But because the file was not a subject,\n // the loader was never stripped, so its `navService` import survived into the\n // client graph and dragged auth, the user model and three Node builtins with\n // it. The app was right and the subject test was wrong.\n //\n // Matching `*.layout.tsx` rather than a list of known layout names is\n // deliberate: an enumerated list is the shape that has produced every other\n // boundary defect here (canon `eb0c5ee8`).\n if (/\\.layout\\.tsx?$/.test(base)) return true;\n if (base === \"root.tsx\") return true;\n return false;\n}\n\nconst HMR_RUNTIME_SPECIFIER = \"@warlock.js/web/client/runtime\";\n\n/**\n * The projected module shares its scope with application source, so the helper\n * import must not redeclare a name the application already owns. A suffix is\n * only needed for the deliberately unlikely collision, but making it\n * deterministic keeps the generated HMR module valid for every page shape.\n */\nfunction hmrRegisterModulesBinding(code: string): string {\n const base = \"__warlockRegisterModules\";\n let binding = base;\n let index = 2;\n\n while (new RegExp(`\\\\b${binding}\\\\b`).test(code)) {\n binding = `${base}${index++}`;\n }\n\n return binding;\n}\n\n/**\n * The client-build Vite plugin. Scoped to `*.page.tsx`/`layout.tsx`/`root.tsx`\n * and skipped entirely for the SSR build (`options.ssr`) — the server still\n * needs `route`/`middleware`/`validation`/`loader`/`metadata`/`prefix` intact.\n */\nexport function projection(): Plugin {\n return {\n name: \"warlock:projection\",\n enforce: \"pre\",\n transform(code, id, options) {\n if (options?.ssr) return null;\n if (!isProjectableFile(id)) return null;\n\n try {\n const { code: transformed, map } = projectModule(code, id);\n const registerModules = hmrRegisterModulesBinding(transformed);\n return {\n code:\n `import { registerModules as ${registerModules} } from \"${HMR_RUNTIME_SPECIFIER}\";\\n` +\n `${transformed}\\n` +\n `if (import.meta.hot) import.meta.hot.accept((replacement) => { if (replacement) ${registerModules}([replacement]); });\\n`,\n map,\n };\n } catch (error) {\n if (error instanceof ProjectionAmbiguityError) {\n this.error(error.message);\n }\n throw error;\n }\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"projection.mjs","names":[],"sources":["../../../../../../../web/src/vite/projection.ts"],"sourcesContent":["/**\n * Projection — the compile-time AST transform that strips a page module's\n * six server exports before the CLIENT graph forms.\n *\n * Removes `export const route/middleware/validation/loader/metadata/prefix = ...`\n * (const-arrow form) and `export async function loader(...) {...}`\n * (function-declaration form — a page declares these as separate named\n * exports, not one fused object, so both forms are real), plus any import\n * OR top-level declaration that becomes\n * unreferenced ONLY as a result of that removal. The default export (the\n * page component) and every other non-server-named export — including the\n * synchronous, no-argument `register()` lifecycle hook — survive\n * unconditionally, regardless of what they reference — classification is by\n * FILE, not by what an export does with data (`c604f0bc` §9).\n *\n * ATTRIBUTION APPLIES TO LOCAL DECLARATIONS, NOT JUST IMPORTS. A page or\n * layout hoists things to module scope for two ordinary reasons, and the\n * reference graph tells them apart without guessing:\n *\n * `const publishCart: Middleware = ...` + `export const middleware =\n * [publishCart]` (`v5/app/.../products/web/layout.tsx:30,43`) — the only\n * reader is a server export being removed, so the binding goes with it.\n *\n * `const COMMON_TIMEZONES = [...]` read by the default export\n * (`v5/app/.../account/settings.page.tsx:118,129`) — a surviving reader, so\n * it survives.\n *\n * That is the SAME rule already applied to import bindings, extended to the\n * other binding kind. It is deliberately not a widening of\n * `SERVER_EXPORT_NAMES`, and not \"accept what I don't recognise\": a\n * declaration is kept when something the client keeps reads it, dropped when\n * nothing does AND dropping it cannot delete a side effect, and REFUSED\n * otherwise — see `isDefinitionShapedInit`.\n *\n * This is NOT Gate A (`resolveId` path refusal), Gate B (inline secret\n * reads) or Gate C (emitted-output verification) — those are separate,\n * later slices. Projection runs first; the gates enforce after.\n */\nimport { parse } from \"@babel/parser\";\nimport MagicString from \"magic-string\";\nimport path from \"node:path\";\nimport type { Plugin } from \"vite\";\n\n/**\n * Exported so Gate C (`gate-c-verify.ts`) can re-derive \"does the emitted\n * bundle contain a server export as a top-level binding\" from this exact set\n * rather than hand-typing a second copy that could drift from projection's\n * own list.\n */\nexport const SERVER_EXPORT_NAMES = new Set([\n \"route\",\n \"middleware\",\n \"validation\",\n \"loader\",\n \"metadata\",\n \"prefix\",\n]);\n\n/**\n * Recognized client-safe assets that always survive projection untouched,\n * whether imported bare (`import \"./x.css\"`) or with specifiers\n * (`import styles from \"./x.module.css\"`) — `c604f0bc` §3 names CSS\n * explicitly; the rest of this list is the same \"never guess, but a known\n * asset extension is not ambiguous\" reasoning extended to the other static\n * asset kinds Vite treats as URL/asset imports, not executable code.\n */\nconst ASSET_EXTENSION_RE =\n /\\.(css|scss|sass|less|styl|stylus|svg|png|jpe?g|gif|webp|avif|ico|woff2?|ttf|eot|otf)(\\?.*)?$/i;\n\n/**\n * Top-level statement types that need no ambiguity check and are never\n * touched by removal: import declarations are handled by their own\n * survives/orphaned logic below, and every export (other than the 6 server\n * names) plus type-only declarations survive unconditionally per\n * `c604f0bc` §9 (\"classify FILES, not the data they touch\").\n *\n * `ExportAllDeclaration` (`export * from \"./x\"` / `export * as ns from\n * \"./x\"`) is deliberately NOT in this set — it can forward ANY name from its\n * source module, including a server export, and is refused explicitly below\n * rather than assumed safe.\n */\nconst ALWAYS_SAFE_STATEMENT_TYPES = new Set([\n \"ExportNamedDeclaration\",\n \"ExportDefaultDeclaration\",\n \"TSInterfaceDeclaration\",\n \"TSTypeAliasDeclaration\",\n \"EmptyStatement\",\n]);\n\n/**\n * Thrown when projection encounters an attribution-ambiguous top-level\n * statement (`c604f0bc` §3: \"the compiler must not guess\"). Carries the\n * file/statement/fix fields the plugin's `transform` hook formats into the\n * build-failure message — never silently kept or silently dropped.\n */\nexport class ProjectionAmbiguityError extends Error {\n constructor(\n public readonly file: string,\n public readonly statement: string,\n public readonly line: number,\n public readonly explanation: string,\n public readonly fix: string,\n ) {\n super(\n [\n `Projection refused to guess: an ambiguous top-level statement in the client build.`,\n ``,\n `File: ${file}:${line}`,\n `Statement: ${statement}`,\n `Cause: ${explanation}`,\n `Fix: ${fix}`,\n ].join(\"\\n\"),\n );\n this.name = \"ProjectionAmbiguityError\";\n }\n}\n\nexport interface ProjectionResult {\n code: string;\n map: ReturnType<MagicString[\"generateMap\"]>;\n}\n\n/** A top-level `const`/`function`/`class` awaiting attribution by reference. */\ninterface LocalDeclaration {\n stmt: any;\n /** The module-scope names it binds. */\n names: Set<string>;\n /** Whether removing it could delete a side effect — see `isDefinitionShapedInit`. */\n definitionShaped: boolean;\n removed: boolean;\n}\n\nfunction hasSurvivingReader(local: LocalDeclaration, survivingNames: Set<string>): boolean {\n for (const name of local.names) {\n if (survivingNames.has(name)) return true;\n }\n return false;\n}\n\nfunction isKnownSafeAsset(source: string): boolean {\n return ASSET_EXTENSION_RE.test(source);\n}\n\n/**\n * Matches `export const <name> = ...` only when the declaration has exactly\n * one declarator — every server export in every fixture and v5/app page is\n * written one-const-per-export (`product-details.page.tsx:15-18,42-69,71-74`);\n * a multi-declarator `export const a = 1, b = 2` is left to the ambiguity\n * path below rather than guessing which half is server-only.\n */\nfunction isServerExportDeclaration(stmt: any): boolean {\n if (stmt.type !== \"ExportNamedDeclaration\" || !stmt.declaration) return false;\n const decl = stmt.declaration;\n if (decl.type === \"VariableDeclaration\" && decl.declarations.length === 1) {\n const id = decl.declarations[0].id;\n return id?.type === \"Identifier\" && SERVER_EXPORT_NAMES.has(id.name);\n }\n if (decl.type === \"FunctionDeclaration\") {\n return !!decl.id && SERVER_EXPORT_NAMES.has(decl.id.name);\n }\n return false;\n}\n\n/**\n * Generic duck-typed AST walk (no `@babel/traverse` dependency — this\n * package only needs `@babel/parser` + `@babel/types`-shaped nodes).\n * Collects every `Identifier`/`JSXIdentifier` name reachable from `node`,\n * used to decide whether an import binding still has a reader once the 6\n * server exports are gone. Over-collecting (e.g. counting an object\n * property key as a \"use\") only ever biases toward KEEPING an import, never\n * toward dropping one that is still needed — the safe direction for a\n * heuristic that must not guess in the removal 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\")\n continue;\n if (\n key === \"leadingComments\" ||\n key === \"trailingComments\" ||\n key === \"innerComments\" ||\n key === \"extra\"\n ) {\n continue;\n }\n collectIdentifierNames(record[key], names);\n }\n}\n\n/**\n * Top-level statements that BIND a name, and are therefore attributable by the\n * reference graph rather than by guessing. Everything outside this set and\n * `ALWAYS_SAFE_STATEMENT_TYPES` declares nothing — a bare `console.log(\"boot\")`\n * has no binding to trace a reader from, which is why it stays a hard refusal.\n */\nconst DECLARATION_STATEMENT_TYPES = new Set([\n \"VariableDeclaration\",\n \"FunctionDeclaration\",\n \"ClassDeclaration\",\n]);\n\nfunction collectPatternNames(node: any, names: Set<string>): void {\n if (!node || typeof node !== \"object\") return;\n switch (node.type) {\n case \"Identifier\":\n names.add(node.name);\n return;\n case \"ObjectPattern\":\n for (const property of node.properties) {\n collectPatternNames(\n property.type === \"RestElement\" ? property.argument : property.value,\n names,\n );\n }\n return;\n case \"ArrayPattern\":\n for (const element of node.elements) collectPatternNames(element, names);\n return;\n case \"AssignmentPattern\":\n collectPatternNames(node.left, names);\n return;\n case \"RestElement\":\n collectPatternNames(node.argument, names);\n return;\n }\n}\n\n/** The names a top-level declaration introduces into module scope. */\nfunction declaredNames(stmt: any): Set<string> {\n const names = new Set<string>();\n if (stmt.type === \"VariableDeclaration\") {\n for (const declarator of stmt.declarations) collectPatternNames(declarator.id, names);\n } else if (stmt.id?.type === \"Identifier\") {\n names.add(stmt.id.name);\n }\n return names;\n}\n\n/**\n * Whether EVALUATING this initializer can run anything.\n *\n * This is the whole safety argument for removing an unreferenced declaration.\n * A function definition or a literal only creates a value, so dropping it can\n * only drop a binding nothing reads. A call, a `new`, an `await`, a member\n * access (a getter) — those can do work, and \"was that work for the server or\n * for the client?\" is precisely the question projection must not answer by\n * guessing (`c604f0bc` §3). `const _ = installPolyfill()` with no reader at all\n * is the shape this predicate exists to refuse rather than silently delete.\n *\n * Conservative by construction: an unrecognized node type is NOT\n * definition-shaped, so a new syntax form arrives as a refusal with a message,\n * never as a silent removal.\n */\nfunction isDefinitionShapedInit(node: any): boolean {\n // `let x;` — a bare binding with nothing to evaluate.\n if (!node) return true;\n\n switch (node.type) {\n case \"ArrowFunctionExpression\":\n case \"FunctionExpression\":\n case \"StringLiteral\":\n case \"NumericLiteral\":\n case \"BooleanLiteral\":\n case \"NullLiteral\":\n case \"BigIntLiteral\":\n case \"RegExpLiteral\":\n // A bare identifier read is a binding lookup, not a computation.\n case \"Identifier\":\n return true;\n case \"TemplateLiteral\":\n return node.expressions.every((expression: any) => isDefinitionShapedInit(expression));\n case \"UnaryExpression\":\n return node.operator !== \"delete\" && isDefinitionShapedInit(node.argument);\n case \"ArrayExpression\":\n return node.elements.every(\n (element: any) =>\n element === null || (element.type !== \"SpreadElement\" && isDefinitionShapedInit(element)),\n );\n case \"ObjectExpression\":\n // Spread and computed keys both evaluate arbitrary expressions; a getter\n // or setter defines a body that runs on ACCESS, which the surviving half\n // could still trigger — none of them are definitions.\n return node.properties.every(\n (property: any) =>\n property.type === \"ObjectProperty\" &&\n !property.computed &&\n isDefinitionShapedInit(property.value),\n );\n // TS-only wrappers erase at compile time; look through them.\n case \"TSAsExpression\":\n case \"TSSatisfiesExpression\":\n case \"TSNonNullExpression\":\n case \"TSTypeAssertion\":\n case \"TSInstantiationExpression\":\n case \"ParenthesizedExpression\":\n return isDefinitionShapedInit(node.expression);\n default:\n return false;\n }\n}\n\n/**\n * Statement-level form of the above. A `class` is excluded on purpose: static\n * blocks, decorators and computed member keys all run at class-definition time,\n * so a class is only ever kept or refused, never silently removed.\n */\nfunction isDefinitionShapedStatement(stmt: any): boolean {\n if (stmt.type === \"FunctionDeclaration\") return true;\n if (stmt.type !== \"VariableDeclaration\") return false;\n return stmt.declarations.every((declarator: any) => isDefinitionShapedInit(declarator.init));\n}\n\nfunction removeStatement(s: MagicString, code: string, node: any): void {\n let end = node.end as number;\n // Swallow one trailing newline so a removed statement doesn't leave a\n // blank line behind — cosmetic only, the output's correctness never\n // depends on it.\n if (code[end] === \"\\r\" && code[end + 1] === \"\\n\") end += 2;\n else if (code[end] === \"\\n\") end += 1;\n s.remove(node.start as number, end);\n}\n\nfunction statementSnippet(code: string, node: any): string {\n return code\n .slice(node.start as number, node.end as number)\n .split(\"\\n\")[0]\n .trim();\n}\n\n/**\n * The transform itself: parse, remove the 6 server exports and every import\n * orphaned only by that removal, fail closed on anything attribution-\n * ambiguous. `filePath` is only used for error messages (`c604f0bc` §7 —\n * fence errors must name the file).\n */\nexport function projectModule(code: string, filePath: string): ProjectionResult {\n const ast = parse(code, {\n sourceType: \"module\",\n plugins: [\"typescript\", \"jsx\"],\n });\n\n const s = new MagicString(code);\n const body = ast.program.body as any[];\n\n const removedServerExports: any[] = [];\n const importDeclarations: any[] = [];\n const localDeclarations: LocalDeclaration[] = [];\n\n for (const stmt of body) {\n if (stmt.type === \"ImportDeclaration\") {\n importDeclarations.push(stmt);\n continue;\n }\n const isNamespaceReexport =\n stmt.type === \"ExportNamedDeclaration\" &&\n stmt.source != null &&\n (stmt.specifiers as any[] | undefined)?.some(\n (specifier) => specifier.type === \"ExportNamespaceSpecifier\",\n );\n\n if (stmt.type === \"ExportAllDeclaration\" || isNamespaceReexport) {\n // `export * from \"./source\"` (and `export * as ns from \"./source\"`,\n // which Babel parses as an `ExportNamedDeclaration` carrying an\n // `ExportNamespaceSpecifier` rather than as `ExportAllDeclaration` —\n // hence the second check above) re-exports every name the source\n // module exports, sight unseen.\n // Projection classifies by file (`c604f0bc` §9) and never opens a\n // second file to resolve what a re-export actually forwards — doing so\n // would mean parsing and walking the source module too, i.e. a second\n // parser. Whether the source exports one of the 6 server names is\n // therefore unknowable here, so this is attribution-ambiguous the same\n // way an unrecognized top-level statement is, and gets the same\n // refusal rather than an assumption that it is safe.\n throw new ProjectionAmbiguityError(\n filePath,\n statementSnippet(code, stmt),\n stmt.loc.start.line,\n `a star re-export forwards every name the source module exports, including possibly one of the 6 known server exports (route, middleware, validation, loader, metadata, prefix) — projection cannot inspect the source module's exports without parsing a second file, so it can't tell whether this leaks a server-only binding into the client bundle`,\n `replace the star re-export with explicit named re-exports (export { ComponentA, ComponentB } from \"./source\"), listing only the client-safe names`,\n );\n }\n if (isServerExportDeclaration(stmt)) {\n removedServerExports.push(stmt);\n continue;\n }\n if (ALWAYS_SAFE_STATEMENT_TYPES.has(stmt.type)) continue;\n if (DECLARATION_STATEMENT_TYPES.has(stmt.type)) {\n // Attributable by the reference graph — decided below, once it is known\n // which statements survive. NOT accepted here.\n localDeclarations.push({\n stmt,\n names: declaredNames(stmt),\n definitionShaped: isDefinitionShapedStatement(stmt),\n removed: false,\n });\n continue;\n }\n\n // Attribution-IMPOSSIBLE: not an import, not one of the 6 known server\n // exports, not another export, not a type-only declaration, and it binds\n // no name for a reader to point at. Fail closed rather than guess which\n // side of the fence it belongs on (`c604f0bc` §3).\n throw new ProjectionAmbiguityError(\n filePath,\n statementSnippet(code, stmt),\n stmt.loc.start.line,\n `top-level executable code that declares nothing — outside the 6 known server exports (route, middleware, validation, loader, metadata, prefix), and binding no name, so projection has no reader to attribute it by and can't tell whether it belongs to the server or the client`,\n `move universal static declarations and their imports into export function register(), or mark the code with an explicit .server/.client file; server-only work can instead move inside one of the 6 declared server exports`,\n );\n }\n\n const removedLocals = new Set<any>();\n\n /**\n * Every name READ by something that survives projection. Imports are excluded\n * so an import specifier never counts as a use of itself, and a local\n * declaration does not count as a use of ITSELF either — otherwise a\n * self-recursive server-only helper would pin its own binding alive forever.\n *\n * Over-collecting (an object property key, a shadowing parameter) only ever\n * biases toward KEEPING, never toward dropping something still needed — the\n * safe direction for a heuristic that must not guess in the removal\n * direction.\n */\n function collectSurvivingNames(): Set<string> {\n const names = new Set<string>();\n for (const stmt of body) {\n if (stmt.type === \"ImportDeclaration\") continue;\n if (removedServerExports.includes(stmt) || removedLocals.has(stmt)) continue;\n const own = new Set<string>();\n collectIdentifierNames(stmt, own);\n if (DECLARATION_STATEMENT_TYPES.has(stmt.type)) {\n for (const name of declaredNames(stmt)) own.delete(name);\n }\n for (const name of own) names.add(name);\n }\n return names;\n }\n\n // Fixpoint, not one pass: a server-only helper can be reached only through\n // ANOTHER server-only helper, and dropping the first orphans the second.\n let survivingNames = collectSurvivingNames();\n for (let changed = true; changed;) {\n changed = false;\n for (const local of localDeclarations) {\n if (local.removed || !local.definitionShaped) continue;\n if (hasSurvivingReader(local, survivingNames)) continue;\n local.removed = true;\n removedLocals.add(local.stmt);\n changed = true;\n }\n if (changed) survivingNames = collectSurvivingNames();\n }\n\n for (const local of localDeclarations) {\n if (local.removed || hasSurvivingReader(local, survivingNames)) continue;\n\n // Nothing the client keeps reads it, so it belongs to the server exports\n // being removed — but its initializer can RUN, and a side effect is not\n // attributable by the reference graph. Keeping it ships server work to the\n // browser; dropping it deletes a side effect the client may depend on.\n // Refuse rather than pick one (`c604f0bc` §3).\n throw new ProjectionAmbiguityError(\n filePath,\n statementSnippet(code, local.stmt),\n local.stmt.loc.start.line,\n `a top-level declaration read only by the server exports being removed, but whose initializer executes code rather than just defining a value — projection can't tell whether that work is server-only or a side effect the client depends on`,\n `move universal static declarations and their imports into export function register(), move a server-only initializer inside the export that reads it, or split it into an explicit .server/.client file`,\n );\n }\n\n for (const decl of importDeclarations) {\n const source = decl.source.value as string;\n if (isKnownSafeAsset(source)) continue; // always survives, no orphan check\n\n if (decl.specifiers.length === 0) {\n // A bare side-effect import that isn't a recognized asset extension is\n // just as attribution-ambiguous as an executable statement — could be\n // a server-only side effect or something the client genuinely needs.\n throw new ProjectionAmbiguityError(\n filePath,\n statementSnippet(code, decl),\n decl.loc.start.line,\n `a bare side-effect import with no recognized client-safe asset extension — projection can't tell if it belongs only to the server exports being removed or must ship to the client`,\n `move universal static declarations and their imports into export function register(), or mark it with an explicit .server/.client file; server-only work can instead move inside one of the 6 declared server exports`,\n );\n }\n\n const isUsed = decl.specifiers.some((spec: any) => survivingNames.has(spec.local.name));\n if (!isUsed) removeStatement(s, code, decl);\n }\n\n for (const stmt of removedServerExports) {\n removeStatement(s, code, stmt);\n }\n\n for (const stmt of removedLocals) {\n removeStatement(s, code, stmt);\n }\n\n return {\n code: s.toString(),\n map: s.generateMap({ hires: true, source: filePath }),\n };\n}\n\nexport function isProjectableFile(id: string): boolean {\n const base = path.basename(id.split(\"?\")[0]);\n if (/\\.page\\.tsx?$/.test(base)) return true;\n if (base === \"layout.tsx\" || base === \"layout.ts\") return true;\n // NAMED layouts — `dashboard.layout.tsx` and friends — are subjects too.\n //\n // Only the exact name `layout.tsx` is POSITIONAL (discovered by its folder).\n // A named layout is addressed by import instead, which is a documented part of\n // the contract: `v5/app/src/web/layouts/dashboard.layout.tsx` says so in its\n // own header, and modules opt in with two re-export lines.\n //\n // Projection did not recognise them, and the consequence was not cosmetic.\n // `dashboard.layout.tsx` calls `navService.forUser()` INSIDE its `loader` —\n // exactly where server work belongs. But because the file was not a subject,\n // the loader was never stripped, so its `navService` import survived into the\n // client graph and dragged auth, the user model and three Node builtins with\n // it. The app was right and the subject test was wrong.\n //\n // Matching `*.layout.tsx` rather than a list of known layout names is\n // deliberate: an enumerated list is the shape that has produced every other\n // boundary defect here (canon `eb0c5ee8`).\n if (/\\.layout\\.tsx?$/.test(base)) return true;\n if (base === \"root.tsx\") return true;\n return false;\n}\n\nconst HMR_RUNTIME_SPECIFIER = \"@warlock.js/web/client/runtime\";\n\n/**\n * The projected module shares its scope with application source, so the helper\n * import must not redeclare a name the application already owns. A suffix is\n * only needed for the deliberately unlikely collision, but making it\n * deterministic keeps the generated HMR module valid for every page shape.\n */\nfunction hmrRegisterModulesBinding(code: string): string {\n const base = \"__warlockRegisterModules\";\n let binding = base;\n let index = 2;\n\n while (new RegExp(`\\\\b${binding}\\\\b`).test(code)) {\n binding = `${base}${index++}`;\n }\n\n return binding;\n}\n\n/**\n * The client-build Vite plugin. Scoped to `*.page.tsx`/`layout.tsx`/`root.tsx`\n * and skipped entirely for the SSR build (`options.ssr`) — the server still\n * needs `route`/`middleware`/`validation`/`loader`/`metadata`/`prefix` intact.\n */\nexport function projection(): Plugin {\n return {\n name: \"warlock:projection\",\n enforce: \"pre\",\n transform(code, id, options) {\n if (options?.ssr) return null;\n if (!isProjectableFile(id)) return null;\n\n try {\n const { code: transformed, map } = projectModule(code, id);\n const registerModules = hmrRegisterModulesBinding(transformed);\n return {\n code:\n `import { registerModules as ${registerModules} } from \"${HMR_RUNTIME_SPECIFIER}\";\\n` +\n `${transformed}\\n` +\n `if (import.meta.hot) import.meta.hot.accept((replacement) => { if (replacement) ${registerModules}([replacement]); });\\n`,\n map,\n };\n } catch (error) {\n if (error instanceof ProjectionAmbiguityError) {\n this.error(error.message);\n }\n throw error;\n }\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ import { createHydrationClientEntry } from "./hydration-entries.mjs";
2
+
3
+ //#region ../web/src/vite/react-refresh-preamble.ts
4
+ /** POSIX-normalised, case-folded — Vite ids are `/`-separated on Windows too. */
5
+ function normalizeModuleId(id) {
6
+ const [filepath] = id.split("?");
7
+ return filepath.replace(/\\/g, "/").toLowerCase();
8
+ }
9
+ /**
10
+ * Public specifier for the React Fast Refresh preamble module this connector
11
+ * injects. See {@link resolveReactFastRefreshPlugins}.
12
+ */
13
+ const REACT_REFRESH_PREAMBLE_ID = "virtual:warlock/react-refresh-preamble";
14
+ /** The `\0`-prefixed form Rollup uses to mark a module as not-a-file. */
15
+ const RESOLVED_REACT_REFRESH_PREAMBLE_ID = `\0${REACT_REFRESH_PREAMBLE_ID}`;
16
+ /**
17
+ * React Fast Refresh, or nothing at all — never half of it.
18
+ *
19
+ * TWO plugins, and the second one is not optional. `@vitejs/plugin-react`
20
+ * emits, into every client component module, a wrapper that reads
21
+ * `window.$RefreshReg$` and THROWS "can't detect preamble" if it is missing.
22
+ * The preamble that defines it normally arrives through Vite's
23
+ * `transformIndexHtml`, and this pipeline has no HTML for Vite to transform:
24
+ * the document is React's SSR output and the only script appended to it is
25
+ * the hydration entry (`./create-page-route-handler.ts:95`). So the preamble
26
+ * is delivered as a virtual module that the hydration entry imports FIRST.
27
+ *
28
+ * "First" is load-bearing, twice over. ESM evaluates a module's imports in
29
+ * source order before the importer's own body, so an import placed at
30
+ * position 0 runs before `virtual:warlock/pages` pulls in a single component
31
+ * — which is what `$RefreshReg$` has to exist for — and before `react-dom`
32
+ * initialises, which is what `injectIntoGlobalHook` has to precede.
33
+ *
34
+ * The preamble TEXT is read from the plugin's own `preambleCode` export
35
+ * rather than copied here, so a version bump cannot leave this file holding
36
+ * a stale runtime contract. If that export ever stops being a string, Fast
37
+ * Refresh is DECLINED entirely (empty array) instead of registered without
38
+ * its preamble — a loud "no HMR" beats a component graph that throws on
39
+ * first paint.
40
+ *
41
+ * Dev-only by construction, like everything else this method is called from:
42
+ * `apply: "serve"` on our own plugin, `apply: "serve"` on the plugin's
43
+ * refresh half, and `skipFastRefresh` on `config.isProduction` inside it. The
44
+ * PRODUCTION client bundle cannot reach any of this — it is built by
45
+ * `buildWarlockHydrationClient` (`./index.ts:185`), which composes
46
+ * `warlockClientBoundary()` and nothing else, and never calls this method.
47
+ *
48
+ * @param paths resolved by `WebConnector.resolvePaths`
49
+ */
50
+ async function resolveReactFastRefreshPlugins(paths) {
51
+ let viteReact;
52
+ try {
53
+ ({default: viteReact} = await import("@vitejs/plugin-react"));
54
+ } catch {
55
+ console.warn("[warlock:web] React Fast Refresh is OFF: `@vitejs/plugin-react` is not installed. Edits to a component will reload the page instead of hot-swapping it. Install it with `npm i -D @vitejs/plugin-react` to enable it.");
56
+ return [];
57
+ }
58
+ const preambleCode = viteReact.preambleCode;
59
+ if (typeof preambleCode !== "string") {
60
+ console.warn("[warlock:web] React Fast Refresh is OFF: this `@vitejs/plugin-react` no longer exports `preambleCode`, so the refresh preamble cannot be injected into the hydration entry. Registering the plugin without it would make every component module throw \"can't detect preamble\" in the browser.");
61
+ return [];
62
+ }
63
+ const preambleSource = preambleCode.replace("__BASE__", "/");
64
+ const hydrationEntryId = normalizeModuleId(createHydrationClientEntry(paths.webRoot).sourcePath);
65
+ return [{
66
+ name: "warlock:react-refresh-preamble",
67
+ enforce: "pre",
68
+ apply: "serve",
69
+ applyToEnvironment: (environment) => environment.config.consumer === "client",
70
+ resolveId(source) {
71
+ if (source === REACT_REFRESH_PREAMBLE_ID) return RESOLVED_REACT_REFRESH_PREAMBLE_ID;
72
+ return null;
73
+ },
74
+ load(id) {
75
+ if (id === RESOLVED_REACT_REFRESH_PREAMBLE_ID) return preambleSource;
76
+ return null;
77
+ },
78
+ transform(code, id) {
79
+ if (normalizeModuleId(id) !== hydrationEntryId) return null;
80
+ return {
81
+ code: `import ${JSON.stringify(REACT_REFRESH_PREAMBLE_ID)};\n${code}`,
82
+ map: null
83
+ };
84
+ }
85
+ }, ...viteReact()];
86
+ }
87
+
88
+ //#endregion
89
+ export { resolveReactFastRefreshPlugins };
90
+ //# sourceMappingURL=react-refresh-preamble.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react-refresh-preamble.mjs","names":[],"sources":["../../../../../../../web/src/vite/react-refresh-preamble.ts"],"sourcesContent":["import type { Plugin, PluginOption } from \"vite\";\nimport { createHydrationClientEntry } from \"./hydration-entries\";\n\n/** `@vitejs/plugin-react`'s default export, plus the one static it publishes. */\ntype ReactPluginFactory = ((options?: Record<string, unknown>) => PluginOption[]) & {\n preambleCode?: string;\n};\n\n/** POSIX-normalised, case-folded — Vite ids are `/`-separated on Windows too. */\nfunction normalizeModuleId(id: string): string {\n const [filepath] = id.split(\"?\");\n\n return filepath.replace(/\\\\/g, \"/\").toLowerCase();\n}\n\n/**\n * Public specifier for the React Fast Refresh preamble module this connector\n * injects. See {@link resolveReactFastRefreshPlugins}.\n */\nconst REACT_REFRESH_PREAMBLE_ID = \"virtual:warlock/react-refresh-preamble\";\n\n/** The `\\0`-prefixed form Rollup uses to mark a module as not-a-file. */\nconst RESOLVED_REACT_REFRESH_PREAMBLE_ID = `\\0${REACT_REFRESH_PREAMBLE_ID}`;\n\n/**\n * React Fast Refresh, or nothing at all — never half of it.\n *\n * TWO plugins, and the second one is not optional. `@vitejs/plugin-react`\n * emits, into every client component module, a wrapper that reads\n * `window.$RefreshReg$` and THROWS \"can't detect preamble\" if it is missing.\n * The preamble that defines it normally arrives through Vite's\n * `transformIndexHtml`, and this pipeline has no HTML for Vite to transform:\n * the document is React's SSR output and the only script appended to it is\n * the hydration entry (`./create-page-route-handler.ts:95`). So the preamble\n * is delivered as a virtual module that the hydration entry imports FIRST.\n *\n * \"First\" is load-bearing, twice over. ESM evaluates a module's imports in\n * source order before the importer's own body, so an import placed at\n * position 0 runs before `virtual:warlock/pages` pulls in a single component\n * — which is what `$RefreshReg$` has to exist for — and before `react-dom`\n * initialises, which is what `injectIntoGlobalHook` has to precede.\n *\n * The preamble TEXT is read from the plugin's own `preambleCode` export\n * rather than copied here, so a version bump cannot leave this file holding\n * a stale runtime contract. If that export ever stops being a string, Fast\n * Refresh is DECLINED entirely (empty array) instead of registered without\n * its preamble — a loud \"no HMR\" beats a component graph that throws on\n * first paint.\n *\n * Dev-only by construction, like everything else this method is called from:\n * `apply: \"serve\"` on our own plugin, `apply: \"serve\"` on the plugin's\n * refresh half, and `skipFastRefresh` on `config.isProduction` inside it. The\n * PRODUCTION client bundle cannot reach any of this — it is built by\n * `buildWarlockHydrationClient` (`./index.ts:185`), which composes\n * `warlockClientBoundary()` and nothing else, and never calls this method.\n *\n * @param paths resolved by `WebConnector.resolvePaths`\n */\nexport async function resolveReactFastRefreshPlugins(paths: {\n webRoot: string;\n}): Promise<PluginOption[]> {\n let viteReact: ReactPluginFactory;\n\n try {\n // OPTIONAL peer (`web/package.json`'s `peerDependenciesMeta`), so this is\n // a lazy `await import` exactly like the `vite` one above it, and\n // `@vitejs/plugin-react` is listed in `WEB_OPTIONAL_PEERS` for the same\n // reason `vite` is.\n ({ default: viteReact } = (await import(\"@vitejs/plugin-react\")) as unknown as {\n default: ReactPluginFactory;\n });\n } catch {\n console.warn(\n \"[warlock:web] React Fast Refresh is OFF: `@vitejs/plugin-react` is not installed. \" +\n \"Edits to a component will reload the page instead of hot-swapping it. \" +\n \"Install it with `npm i -D @vitejs/plugin-react` to enable it.\",\n );\n\n return [];\n }\n\n const preambleCode = viteReact.preambleCode;\n\n if (typeof preambleCode !== \"string\") {\n console.warn(\n \"[warlock:web] React Fast Refresh is OFF: this `@vitejs/plugin-react` no longer exports \" +\n \"`preambleCode`, so the refresh preamble cannot be injected into the hydration entry. \" +\n \"Registering the plugin without it would make every component module throw \" +\n '\"can\\'t detect preamble\" in the browser.',\n );\n\n return [];\n }\n\n // `base` is never set on the config below, so it is Vite's default `\"/\"`.\n const preambleSource = preambleCode.replace(\"__BASE__\", \"/\");\n const hydrationEntryId = normalizeModuleId(createHydrationClientEntry(paths.webRoot).sourcePath);\n\n const preamblePlugin: Plugin = {\n name: \"warlock:react-refresh-preamble\",\n // BEFORE `warlock:projection` and the gates would be wrong and BEFORE\n // esbuild's TS transform is required: this prepends one import statement\n // to TypeScript source, so it has to see the file before anything lowers\n // it. It touches exactly one module, so it cannot reorder anything else.\n enforce: \"pre\",\n apply: \"serve\",\n // The preamble is browser state (`window.$RefreshReg$`). The SSR\n // environment must never evaluate it — there is no `window` there, and\n // the server render must stay byte-identical to what it produced before\n // this plugin existed.\n applyToEnvironment: (environment) => environment.config.consumer === \"client\",\n resolveId(source) {\n if (source === REACT_REFRESH_PREAMBLE_ID) return RESOLVED_REACT_REFRESH_PREAMBLE_ID;\n\n return null;\n },\n load(id) {\n if (id === RESOLVED_REACT_REFRESH_PREAMBLE_ID) return preambleSource;\n\n return null;\n },\n transform(code, id) {\n if (normalizeModuleId(id) !== hydrationEntryId) return null;\n\n return { code: `import ${JSON.stringify(REACT_REFRESH_PREAMBLE_ID)};\\n${code}`, map: null };\n },\n };\n\n return [preamblePlugin, ...viteReact()];\n}\n"],"mappings":";;;;AASA,SAAS,kBAAkB,IAAoB;CAC7C,MAAM,CAAC,YAAY,GAAG,MAAM,GAAG;CAE/B,OAAO,SAAS,QAAQ,OAAO,GAAG,CAAC,CAAC,YAAY;AAClD;;;;;AAMA,MAAM,4BAA4B;;AAGlC,MAAM,qCAAqC,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoChD,eAAsB,+BAA+B,OAEzB;CAC1B,IAAI;CAEJ,IAAI;EAKF,CAAC,CAAE,SAAS,aAAe,MAAM,OAAO;CAG1C,QAAQ;EACN,QAAQ,KACN,uNAGF;EAEA,OAAO,CAAC;CACV;CAEA,MAAM,eAAe,UAAU;CAE/B,IAAI,OAAO,iBAAiB,UAAU;EACpC,QAAQ,KACN,iSAIF;EAEA,OAAO,CAAC;CACV;CAGA,MAAM,iBAAiB,aAAa,QAAQ,YAAY,GAAG;CAC3D,MAAM,mBAAmB,kBAAkB,2BAA2B,MAAM,OAAO,CAAC,CAAC,UAAU;CAgC/F,OAAO,CAAC;EA7BN,MAAM;EAKN,SAAS;EACT,OAAO;EAKP,qBAAqB,gBAAgB,YAAY,OAAO,aAAa;EACrE,UAAU,QAAQ;GAChB,IAAI,WAAW,2BAA2B,OAAO;GAEjD,OAAO;EACT;EACA,KAAK,IAAI;GACP,IAAI,OAAO,oCAAoC,OAAO;GAEtD,OAAO;EACT;EACA,UAAU,MAAM,IAAI;GAClB,IAAI,kBAAkB,EAAE,MAAM,kBAAkB,OAAO;GAEvD,OAAO;IAAE,MAAM,UAAU,KAAK,UAAU,yBAAyB,EAAE,KAAK;IAAQ,KAAK;GAAK;EAC5F;CAGmB,GAAG,GAAG,UAAU,CAAC;AACxC"}
@@ -0,0 +1,135 @@
1
+ import { moduleKey } from "../shared/module-key.mjs";
2
+ import { isAppSourcePath, isRecognizedUniversalSurface, isServerFile } from "./gate-a-resolve.mjs";
3
+ import { isProjectableFile, projectModule } from "./projection.mjs";
4
+ import { parse } from "@babel/parser";
5
+
6
+ //#region ../web/src/vite/ssr-client-view.ts
7
+ const CODE_MODULE_EXTENSION = /\.([cm]?[jt]sx?)$/;
8
+ /**
9
+ * Whether `id` is a client surface WITHOUT having to be reached through the
10
+ * projected-import walk first — the seed set `clientViewOf` marks before any
11
+ * graph traversal has happened.
12
+ *
13
+ * Deliberately asks nothing about WHERE the file lives. A file under
14
+ * `src/web/**` is not, by that location alone, on the client boundary: dev's
15
+ * `middleware`/`loader`/etc. exports are stripped by projection before this
16
+ * function is ever consulted for THEM specifically, and a plain server-only
17
+ * helper reached only through `root.tsx`'s (stripped) `middleware` export —
18
+ * `optional-auth.ts` importing `cookie-session.middleware.ts` — has no route
19
+ * into the surviving graph at all. Location used to override that and refuse
20
+ * it anyway; the boundary is the post-projection import graph, per canon
21
+ * `10f6041c`, the same rule `gate-a-resolve.ts` already enforces by having
22
+ * deliberately removed its own location-based rule 4 (see `ruleViolation`'s
23
+ * comment there). A file that genuinely belongs in the client graph — a
24
+ * `.page.tsx`, a `layout.tsx`, the app root, or anything one of those imports
25
+ * — is still admitted, either directly by `isRecognizedUniversalSurface` or by
26
+ * `resolveId`'s graph walk (`clientBoundModules` / `clientImportsByModule`)
27
+ * once something already in the graph imports it. Nothing under `src/web/**`
28
+ * needs a location exemption to be reached that way.
29
+ */
30
+ function isStatelessClientSurface(id, appRoot) {
31
+ const bare = moduleKey(id);
32
+ if (!isAppSourcePath(bare, appRoot)) return false;
33
+ if (isServerFile(bare, appRoot)) return false;
34
+ return isProjectableFile(bare) || isRecognizedUniversalSurface(bare);
35
+ }
36
+ function collectImportSpecifiers(code) {
37
+ const ast = parse(code, {
38
+ sourceType: "module",
39
+ plugins: ["typescript", "jsx"]
40
+ });
41
+ const imports = /* @__PURE__ */ new Set();
42
+ function walk(node) {
43
+ if (!node || typeof node !== "object") return;
44
+ if (Array.isArray(node)) {
45
+ for (const child of node) walk(child);
46
+ return;
47
+ }
48
+ const record = node;
49
+ if ((record.type === "ImportDeclaration" || record.type === "ExportNamedDeclaration" || record.type === "ExportAllDeclaration") && record.source?.type === "StringLiteral") imports.add(record.source.value);
50
+ if (record.type === "CallExpression" && record.callee?.type === "Import" && record.arguments?.[0]?.type === "StringLiteral") imports.add(record.arguments[0].value);
51
+ if (record.type === "ImportExpression" && record.source?.type === "StringLiteral") imports.add(record.source.value);
52
+ for (const [key, child] of Object.entries(record)) {
53
+ if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "range" || key.endsWith("Comments") || key === "extra") continue;
54
+ walk(child);
55
+ }
56
+ }
57
+ walk(ast.program);
58
+ return imports;
59
+ }
60
+ function clientViewOf(state, code, id) {
61
+ const key = moduleKey(id);
62
+ if (!state.clientBoundModules.has(key) && !isStatelessClientSurface(key, state.appRoot)) return;
63
+ state.clientBoundModules.add(key);
64
+ if (!CODE_MODULE_EXTENSION.test(key)) {
65
+ state.clientImportsByModule.set(key, /* @__PURE__ */ new Set());
66
+ return code;
67
+ }
68
+ const clientCode = isProjectableFile(key) ? projectModule(code, key).code : code;
69
+ state.clientImportsByModule.set(key, collectImportSpecifiers(clientCode));
70
+ return clientCode;
71
+ }
72
+ function markResolvedClientModule(state, resolved) {
73
+ if (!resolved) return;
74
+ const id = typeof resolved === "string" ? resolved : resolved.id;
75
+ if (!id.includes("\0")) state.clientBoundModules.add(moduleKey(id));
76
+ }
77
+ function isServerEnvironment(context) {
78
+ return context.environment?.config.consumer === "server";
79
+ }
80
+ /**
81
+ * Keeps the production/client pipeline byte-identical, while giving Gate A
82
+ * and Gate B a validation-only view in Vite's development SSR environment.
83
+ * SSR still evaluates the original source: only the gates receive the
84
+ * projected client view, so loader/server exports retain their legitimate
85
+ * server access while component-visible code is refused before evaluation.
86
+ */
87
+ function clientEnvironmentOnly(plugin, ssrState) {
88
+ const validatesDevSsr = plugin.name === "warlock:gate-a-resolve" || plugin.name === "warlock:gate-b-secrets";
89
+ const originalTransform = typeof plugin.transform === "function" ? plugin.transform : plugin.transform?.handler;
90
+ const originalResolveId = typeof plugin.resolveId === "function" ? plugin.resolveId : plugin.resolveId?.handler;
91
+ const originalBuildStart = typeof plugin.buildStart === "function" ? plugin.buildStart : plugin.buildStart?.handler;
92
+ return {
93
+ ...plugin,
94
+ applyToEnvironment(environment) {
95
+ return environment.config.consumer === "client" || validatesDevSsr && environment.config.consumer === "server";
96
+ },
97
+ buildStart: originalBuildStart ? function(...args) {
98
+ if (isServerEnvironment(this)) return;
99
+ return originalBuildStart.apply(this, args);
100
+ } : void 0,
101
+ transform: originalTransform ? async function(code, id, options) {
102
+ if (!isServerEnvironment(this)) return originalTransform.call(this, code, id, options);
103
+ const clientCode = clientViewOf(ssrState, code, id);
104
+ if (clientCode === void 0) return null;
105
+ const transformed = await originalTransform.call(this, clientCode, id, {
106
+ ...options,
107
+ ssr: false
108
+ });
109
+ if (plugin.name === "warlock:gate-a-resolve" && originalResolveId) for (const source of ssrState.clientImportsByModule.get(moduleKey(id)) ?? []) markResolvedClientModule(ssrState, await originalResolveId.call(this, source, id, {
110
+ attributes: {},
111
+ isEntry: false,
112
+ ssr: false
113
+ }));
114
+ return transformed;
115
+ } : void 0,
116
+ resolveId: originalResolveId ? async function(source, importer, options) {
117
+ if (!isServerEnvironment(this)) return originalResolveId.call(this, source, importer, options);
118
+ if (!importer) return null;
119
+ const importerKey = moduleKey(importer);
120
+ if (!(ssrState.clientBoundModules.has(importerKey) || isStatelessClientSurface(importerKey, ssrState.appRoot))) return null;
121
+ const survivingImports = ssrState.clientImportsByModule.get(importerKey);
122
+ if (survivingImports && !survivingImports.has(source)) return null;
123
+ const resolved = await originalResolveId.call(this, source, importer, {
124
+ ...options,
125
+ ssr: false
126
+ });
127
+ markResolvedClientModule(ssrState, resolved);
128
+ return resolved;
129
+ } : void 0
130
+ };
131
+ }
132
+
133
+ //#endregion
134
+ export { clientEnvironmentOnly };
135
+ //# sourceMappingURL=ssr-client-view.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssr-client-view.mjs","names":[],"sources":["../../../../../../../web/src/vite/ssr-client-view.ts"],"sourcesContent":["import { parse } from \"@babel/parser\";\nimport type { Plugin } from \"vite\";\nimport { isAppSourcePath, isRecognizedUniversalSurface, isServerFile } from \"./gate-a-resolve\";\nimport { isProjectableFile, projectModule } from \"./projection\";\nimport { moduleKey } from \"../shared/module-key\";\n\nexport type SsrBoundaryState = {\n readonly appRoot: string;\n readonly clientBoundModules: Set<string>;\n readonly clientImportsByModule: Map<string, Set<string>>;\n};\n\nconst CODE_MODULE_EXTENSION = /\\.([cm]?[jt]sx?)$/;\n\n/**\n * Whether `id` is a client surface WITHOUT having to be reached through the\n * projected-import walk first — the seed set `clientViewOf` marks before any\n * graph traversal has happened.\n *\n * Deliberately asks nothing about WHERE the file lives. A file under\n * `src/web/**` is not, by that location alone, on the client boundary: dev's\n * `middleware`/`loader`/etc. exports are stripped by projection before this\n * function is ever consulted for THEM specifically, and a plain server-only\n * helper reached only through `root.tsx`'s (stripped) `middleware` export —\n * `optional-auth.ts` importing `cookie-session.middleware.ts` — has no route\n * into the surviving graph at all. Location used to override that and refuse\n * it anyway; the boundary is the post-projection import graph, per canon\n * `10f6041c`, the same rule `gate-a-resolve.ts` already enforces by having\n * deliberately removed its own location-based rule 4 (see `ruleViolation`'s\n * comment there). A file that genuinely belongs in the client graph — a\n * `.page.tsx`, a `layout.tsx`, the app root, or anything one of those imports\n * — is still admitted, either directly by `isRecognizedUniversalSurface` or by\n * `resolveId`'s graph walk (`clientBoundModules` / `clientImportsByModule`)\n * once something already in the graph imports it. Nothing under `src/web/**`\n * needs a location exemption to be reached that way.\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 isProjectableFile(bare) || isRecognizedUniversalSurface(bare);\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 (record.type === \"ImportExpression\" && record.source?.type === \"StringLiteral\") {\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(state: SsrBoundaryState, code: string, id: string): string | undefined {\n const key = moduleKey(id);\n if (!state.clientBoundModules.has(key) && !isStatelessClientSurface(key, state.appRoot)) {\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) ? projectModule(code, key).code : 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 */\nexport function clientEnvironmentOnly(plugin: Plugin, ssrState: SsrBoundaryState): Plugin {\n const validatesDevSsr =\n plugin.name === \"warlock:gate-a-resolve\" || plugin.name === \"warlock:gate-b-secrets\";\n const originalTransform =\n typeof plugin.transform === \"function\" ? plugin.transform : plugin.transform?.handler;\n const originalResolveId =\n typeof plugin.resolveId === \"function\" ? plugin.resolveId : plugin.resolveId?.handler;\n const originalBuildStart =\n typeof plugin.buildStart === \"function\" ? plugin.buildStart : 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(this, clientCode, id, {\n ...options,\n ssr: false,\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(moduleKey(id)) ?? []) {\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 = ssrState.clientImportsByModule.get(importerKey);\n if (survivingImports && !survivingImports.has(source)) return null;\n\n const resolved = await originalResolveId.call(this, source, importer, {\n ...options,\n ssr: false,\n });\n markResolvedClientModule(ssrState, resolved);\n return resolved;\n }\n : undefined,\n };\n}\n"],"mappings":"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"}