@warlock.js/web 5.5.0 → 5.6.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 (138) hide show
  1. package/CHANGELOG.md +27 -5
  2. package/esm/build/contribution.d.mts +2 -2
  3. package/esm/build/contribution.mjs.map +1 -1
  4. package/esm/build/discover-pages.mjs +127 -205
  5. package/esm/build/discover-pages.mjs.map +1 -1
  6. package/esm/build/generate-pages-barrel.mjs +5 -3
  7. package/esm/build/generate-pages-barrel.mjs.map +1 -1
  8. package/esm/build/public-files.mjs +29 -1
  9. package/esm/build/public-files.mjs.map +1 -1
  10. package/esm/build/read-metadata-keys.mjs +225 -0
  11. package/esm/build/read-metadata-keys.mjs.map +1 -0
  12. package/esm/build/read-route-exports.mjs.map +1 -1
  13. package/esm/client/build-hydrated-tree.mjs +1 -1
  14. package/esm/client/build-hydrated-tree.mjs.map +1 -1
  15. package/esm/client/navigation/document-metadata.mjs +108 -0
  16. package/esm/client/navigation/document-metadata.mjs.map +1 -0
  17. package/esm/client/navigation/navigation-root.mjs +23 -103
  18. package/esm/client/navigation/navigation-root.mjs.map +1 -1
  19. package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -1
  20. package/esm/client/navigation/use-query-string.d.mts +35 -0
  21. package/esm/client/navigation/use-query-string.mjs +68 -0
  22. package/esm/client/navigation/use-query-string.mjs.map +1 -0
  23. package/esm/client/runtime/index.d.mts +1 -1
  24. package/esm/client/runtime/index.mjs +1 -1
  25. package/esm/client/runtime/manifest.mjs.map +1 -1
  26. package/esm/client/runtime/matcher.mjs.map +1 -1
  27. package/esm/client/runtime/types.d.mts +1 -1
  28. package/esm/components/link.mjs.map +1 -1
  29. package/esm/core/src/router/normalize-route-path.mjs +33 -0
  30. package/esm/core/src/router/normalize-route-path.mjs.map +1 -0
  31. package/esm/{hydration → entry}/index.mjs +1 -1
  32. package/esm/entry/index.mjs.map +1 -0
  33. package/esm/hydration-payload.mjs.map +1 -1
  34. package/esm/index.d.mts +3 -2
  35. package/esm/index.mjs +3 -2
  36. package/esm/loaders.d.mts +10 -3
  37. package/esm/localization.mjs.map +1 -1
  38. package/esm/metadata.d.mts +4 -4
  39. package/esm/metadata.mjs.map +1 -1
  40. package/esm/node_modules/.pnpm/@mongez_concat-route@1.2.0/node_modules/@mongez/concat-route/esm/index.mjs +33 -0
  41. package/esm/node_modules/.pnpm/@mongez_concat-route@1.2.0/node_modules/@mongez/concat-route/esm/index.mjs.map +1 -0
  42. package/esm/{runtime/register-modules.d.mts → register-modules.d.mts} +1 -1
  43. package/esm/{runtime/register-modules.mjs → register-modules.mjs} +1 -1
  44. package/esm/register-modules.mjs.map +1 -0
  45. package/esm/route.d.mts +35 -2
  46. package/esm/routing/duplicate-route-path.mjs +22 -0
  47. package/esm/routing/duplicate-route-path.mjs.map +1 -0
  48. package/esm/routing/layout-level.mjs +50 -0
  49. package/esm/routing/layout-level.mjs.map +1 -0
  50. package/esm/routing/layout-policy.mjs.map +1 -1
  51. package/esm/routing/query-string.d.mts +23 -1
  52. package/esm/routing/query-string.mjs +23 -1
  53. package/esm/routing/query-string.mjs.map +1 -1
  54. package/esm/routing/route-identity.mjs +27 -2
  55. package/esm/routing/route-identity.mjs.map +1 -1
  56. package/esm/routing/route-table.mjs.map +1 -1
  57. package/esm/routing/router-events.d.mts +5 -4
  58. package/esm/routing/router-events.mjs.map +1 -1
  59. package/esm/server/create-page-route-handler.d.mts +2 -2
  60. package/esm/server/create-page-route-handler.mjs +6 -6
  61. package/esm/server/create-page-route-handler.mjs.map +1 -1
  62. package/esm/server/{dev-server.mjs → dev-error-transport.mjs} +2 -2
  63. package/esm/server/dev-error-transport.mjs.map +1 -0
  64. package/esm/server/execute-page-request.mjs +87 -23
  65. package/esm/server/execute-page-request.mjs.map +1 -1
  66. package/esm/server/execute-page-request.types.d.mts +10 -1
  67. package/esm/server/fold-layout-loaders.mjs +36 -0
  68. package/esm/server/fold-layout-loaders.mjs.map +1 -0
  69. package/esm/server/hydration-client-url.mjs +2 -2
  70. package/esm/server/hydration-client-url.mjs.map +1 -1
  71. package/esm/server/index.d.mts +2 -2
  72. package/esm/server/index.mjs +2 -2
  73. package/esm/server/install-page-routes-from-manifest.d.mts +1 -1
  74. package/esm/server/install-page-routes-from-manifest.mjs +43 -40
  75. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  76. package/esm/server/install-page-routes.d.mts +22 -2
  77. package/esm/server/install-page-routes.mjs +108 -42
  78. package/esm/server/install-page-routes.mjs.map +1 -1
  79. package/esm/server/install-production-page-routes.mjs.map +1 -1
  80. package/esm/server/match-page-route.mjs.map +1 -1
  81. package/esm/server/page-file-change.mjs.map +1 -1
  82. package/esm/server/page-route-reload.mjs +4 -3
  83. package/esm/server/page-route-reload.mjs.map +1 -1
  84. package/esm/server/register-production-public-files.mjs +25 -1
  85. package/esm/server/register-production-public-files.mjs.map +1 -1
  86. package/esm/server/render-page.d.mts +11 -48
  87. package/esm/server/render-page.mjs +13 -55
  88. package/esm/server/render-page.mjs.map +1 -1
  89. package/esm/server/resolve-page-metadata.mjs.map +1 -1
  90. package/esm/server/resolve-route-validation-input.mjs +11 -0
  91. package/esm/server/resolve-route-validation-input.mjs.map +1 -0
  92. package/esm/server/route-validation-error.mjs +32 -0
  93. package/esm/server/route-validation-error.mjs.map +1 -0
  94. package/esm/server/settle-page-response.mjs +5 -3
  95. package/esm/server/settle-page-response.mjs.map +1 -1
  96. package/esm/server/unregistered-pages.mjs +2 -1
  97. package/esm/server/unregistered-pages.mjs.map +1 -1
  98. package/esm/server/web-connector-factory.mjs +1 -1
  99. package/esm/server/web-connector-factory.mjs.map +1 -1
  100. package/esm/server/web-connector.mjs +24 -321
  101. package/esm/server/web-connector.mjs.map +1 -1
  102. package/esm/shared/module-key.mjs +15 -0
  103. package/esm/shared/module-key.mjs.map +1 -0
  104. package/esm/shared/to-posix.mjs +13 -0
  105. package/esm/shared/to-posix.mjs.map +1 -0
  106. package/esm/shared.mjs +24 -0
  107. package/esm/shared.mjs.map +1 -1
  108. package/esm/vite/build-client.mjs.map +1 -1
  109. package/esm/vite/dev-server-config.mjs +240 -0
  110. package/esm/vite/dev-server-config.mjs.map +1 -0
  111. package/esm/vite/gate-a-resolve.d.mts +5 -4
  112. package/esm/vite/gate-a-resolve.mjs +31 -70
  113. package/esm/vite/gate-a-resolve.mjs.map +1 -1
  114. package/esm/vite/gate-b-secrets.mjs.map +1 -1
  115. package/esm/vite/gate-c-verify.d.mts +1 -1
  116. package/esm/vite/gate-c-verify.mjs +1 -1
  117. package/esm/vite/gate-c-verify.mjs.map +1 -1
  118. package/esm/vite/hydration-entries.mjs +5 -7
  119. package/esm/vite/hydration-entries.mjs.map +1 -1
  120. package/esm/vite/index.d.mts +1 -1
  121. package/esm/vite/index.mjs +4 -110
  122. package/esm/vite/index.mjs.map +1 -1
  123. package/esm/vite/page-registry-plugin.mjs +9 -26
  124. package/esm/vite/page-registry-plugin.mjs.map +1 -1
  125. package/esm/vite/projection.mjs.map +1 -1
  126. package/esm/vite/react-refresh-preamble.mjs +90 -0
  127. package/esm/vite/react-refresh-preamble.mjs.map +1 -0
  128. package/esm/vite/ssr-client-view.mjs +135 -0
  129. package/esm/vite/ssr-client-view.mjs.map +1 -0
  130. package/llms-full.txt +558 -482
  131. package/llms.txt +2 -2
  132. package/package.json +6 -6
  133. package/skills/create-a-page/SKILL.md +348 -295
  134. package/skills/navigate-on-the-client/SKILL.md +212 -189
  135. package/esm/hydration/index.mjs.map +0 -1
  136. package/esm/runtime/register-modules.mjs.map +0 -1
  137. package/esm/server/dev-server.mjs.map +0 -1
  138. /package/esm/{hydration → entry}/index.d.mts +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"install-page-routes.mjs","names":[],"sources":["../../../../../../../web/src/server/install-page-routes.ts"],"sourcesContent":["/**\r\n * Registers every page {@link discoverPageFiles} finds under `<appSrcRoot>`\r\n * into Warlock's router (`router.get`, `core/src/router/router.ts:359-361`)\r\n * so `router.scanDevServer(fastify)` —\r\n * the sanctioned dev-server dispatch path (server matching is Warlock's\r\n * router; there is no second server matcher) — picks\r\n * it up. Replaces the two hand-rolled `fastify.get()` calls this file's\r\n * sibling, `dev-server.ts`, used to make directly.\r\n *\r\n * DELIBERATE EXCEPTION to \"web has no core dependency\", same\r\n * reasoning `dev-server.ts`'s own header comment records: this module is not\r\n * exported from either package barrel and is not part of `web/package.json`'s\r\n * dependency graph — dev/CLI bootstrap only.\r\n *\r\n * Scope note: a page's\r\n * `route.path` is now composed with the `prefix` export of EVERY `layout.tsx`\r\n * on its path — outermost first (`composeRoutePath` below) — before\r\n * registration and before the collision check, so `home.page.tsx`\r\n * (`path: \"/\"`, main layout `prefix: \"/\"`) resolves to `/` and\r\n * `products.page.tsx` (`path: \"/\"`, products layout `prefix: \"/products\"`)\r\n * resolves to `/products` — no collision. A page with no `layout.tsx` on its\r\n * path composes against the implicit root prefix `\"/\"` (e.g. `/contact-us`,\r\n * `/hydration-demo`, both unaffected by composition).\r\n *\r\n * WHICH PAGES EXIST is answered by {@link discoverPageFiles}\r\n * (`web/src/build/discover-pages.ts`) — the same walk production's build\r\n * shares — so this file owns no directory-walking of its own; it serves the\r\n * page root (`<appSrcRoot>/web/**`) exactly as discovery enumerates it. WHAT\r\n * ROUTE A PAGE ANSWERS ON stays this file's own job: each page and its nearest layout are still evaluated\r\n * through Vite (`vite.ssrLoadModule`), never read statically, because a dev\r\n * page module must be the one Vite serves, warm cache and all.\r\n */\r\nimport path from \"node:path\";\r\nimport type { ViteDevServer } from \"vite\";\r\nimport {\r\n discoverPageFiles,\r\n ErrorPageDeclaresRouteError,\r\n isErrorPageFile,\r\n layoutChainFor,\r\n toPosix,\r\n} from \"../build/discover-pages\";\r\nimport { NonLiteralRouteExportError, readRouteExports } from \"../build/read-route-exports\";\r\nimport { composeRoutePath } from \"../routing/compose-route-path\";\r\nimport { deriveFilesystemRoutePath } from \"../routing/filesystem-route\";\r\nimport { NestedLayoutsNotSupportedError, selectPageLayout } from \"../routing/layout-policy\";\r\nimport {\r\n canonicalizeRouteExport,\r\n resolvePageRouteCache,\r\n resolvePageRouteName,\r\n type PageCacheOptIn,\r\n} from \"../routing/route-identity\";\r\nimport { publishRouteTable } from \"../routing/route-table\";\r\nimport { Response, type Router } from \"@warlock.js/core\";\r\nimport { createPageRouteHandler } from \"./create-page-route-handler\";\r\nimport type { ErrorPageModule } from \"./error-page\";\r\nimport type { PipelineLoader, PipelineMiddleware } from \"./execute-page-request\";\r\nimport { isLoaderShortCircuit } from \"./settle-page-response\";\r\nimport { devHandlerStylesheetUrls } from \"./stylesheet-urls\";\r\nimport {\r\n createNotFoundRouteHandler,\r\n DuplicateNotFoundPageError,\r\n isNotFoundPageFile,\r\n NotFoundPageDeclaresRouteError,\r\n NOT_FOUND_ROUTE_NAME,\r\n NOT_FOUND_ROUTE_PATH,\r\n type RegisteredRouteShape,\r\n} from \"./not-found-page\";\r\n\r\n/** Re-exported so `web/src/server/index.ts`'s existing barrel export keeps resolving. */\r\nexport { composeRoutePath };\r\n\r\nexport type PageRouteExport = string | { path: string; name?: string; cache?: PageCacheOptIn };\r\n\r\nexport type PageModuleShape = {\r\n route?: PageRouteExport;\r\n};\r\n\r\nexport type InstalledPageRoute = {\r\n /** The canonical declared route path, before layout-prefix composition. */\r\n declaredPath: string;\r\n path: string;\r\n name: string;\r\n file: string;\r\n layoutFile: string | undefined;\r\n};\r\n\r\n/**\r\n * Ownership key for the framework's fallback 404 route. A NUL-prefixed value\r\n * cannot be a real filesystem path, so it cannot collide with an app page's\r\n * canonical source-file key.\r\n */\r\nexport const FRAMEWORK_DEFAULT_NOT_FOUND_SOURCE_FILE = \"\\0warlock:framework-default-404\";\r\n\r\n/**\r\n * The page's application-source-relative POSIX source path used as the router's\r\n * stable ownership key. `appSrcRoot`'s own basename preserves the existing\r\n * `src/web/...` source-file convention.\r\n */\r\nfunction canonicalSourceFileFor(pageFile: string, appSrcRoot: string): string {\r\n return `${path.basename(appSrcRoot)}/${toPosix(path.relative(appSrcRoot, pageFile))}`;\r\n}\r\n\r\nfunction filesystemPageFileFor(pageFile: string, appSrcRoot: string): string {\r\n return toPosix(path.relative(path.join(appSrcRoot, \"web\"), pageFile));\r\n}\r\n\r\n/**\r\n * Resolve the stable identity used to distinguish a route-export edit from an\r\n * ordinary component-body edit. The declared path is retained before layout\r\n * composition so `/settings` under `/admin` compares with the next declared\r\n * `/settings`, not with the effective `/admin/settings` route.\r\n */\r\nexport function resolvePageRouteIdentity(\r\n routeExport: PageRouteExport | undefined,\r\n pageFile: string,\r\n appSrcRoot: string,\r\n): Pick<InstalledPageRoute, \"declaredPath\" | \"name\"> {\r\n const filesystemPageFile = filesystemPageFileFor(pageFile, appSrcRoot);\r\n\r\n if (routeExport === undefined) {\r\n return {\r\n declaredPath: deriveFilesystemRoutePath({ pageFile: filesystemPageFile }),\r\n name: resolvePageRouteName(routeExport, filesystemPageFile),\r\n };\r\n }\r\n\r\n return {\r\n declaredPath: canonicalizeRouteExport(routeExport, pageFile).path,\r\n name: resolvePageRouteName(routeExport, filesystemPageFile),\r\n };\r\n}\r\n\r\nexport type LayoutModuleShape = {\r\n /** Universal registration hook; invoked on this real namespace, never a composed wrapper. */\r\n register?: () => unknown;\r\n prefix?: string;\r\n /**\r\n * The default export — the thing that puts an element in the document, and\r\n * therefore the ONLY export that decides whether a layout counts against the\r\n * single-rendering-layout rule (`../routing/layout-policy.ts`). In dev the\r\n * module is loaded, so this is a fact rather than a guess.\r\n */\r\n default?: unknown;\r\n /** The layout's guards, in the order it declared them. */\r\n middleware?: readonly PipelineMiddleware[];\r\n loader?: PipelineLoader;\r\n};\r\n\r\n/** How this module gets a layout module namespace — `vite.ssrLoadModule`, in practice. */\r\ntype LoadLayout = (layoutFile: string) => Promise<LayoutModuleShape>;\r\n\r\n/**\r\n * The page's layout LEVEL, resolved from its whole chain rather than from the\r\n * one layout nearest to it.\r\n *\r\n * The render pipeline has exactly one layout slot per page\r\n * (`execute-page-request.ts`'s `PageRouteEntry[\"triple\"]`), so the chain has to\r\n * be collapsed into one module before it reaches a handler. Two things collapse\r\n * differently and both matter:\r\n *\r\n * - RENDERING is a selection: at most one layout on the chain may render, and\r\n * the policy picks it. `renders` is read off the loaded module\r\n * (`typeof module.default !== \"undefined\"`), never off the filename — a\r\n * `middleware`-only layout has no default export and is not a wrapper, and\r\n * passing a bare path to `selectPageLayout` would have it read as a rendering\r\n * one, which is the conservative default and the wrong answer here.\r\n * - MIDDLEWARE and PREFIX are compositions: every layout on the path\r\n * contributes, outermost first. A guard on an outer layout that the page's\r\n * own directory knows nothing about is exactly the guard that must still run,\r\n * and a prefix nobody composed is a URL nobody wrote down.\r\n */\r\ntype LayoutLevel = {\r\n /** Every `layout.tsx` from the web root down to the page's directory, outermost first. */\r\n chain: string[];\r\n /**\r\n * The module id the handler's layout slot is registered under, or `undefined`\r\n * when the page has no layout at all: the layout that RENDERS, or — when none\r\n * does — the nearest one, which is the slot dev has always used and so the\r\n * choice that changes nothing but the middleware for a chain with no wrapper\r\n * in it.\r\n */\r\n layoutFile: string | undefined;\r\n /** Every layout's `prefix`, composed outermost first — `discoverPages`' own reduction. */\r\n prefix: string;\r\n /** Declared prefixes keyed by layout directory relative to this page's web root. */\r\n prefixesByDirectory: Readonly<Record<string, string>>;\r\n};\r\n\r\nasync function resolveLayoutLevel(\r\n pageFile: string,\r\n webRoot: string,\r\n loadLayout: LoadLayout,\r\n): Promise<LayoutLevel> {\r\n const chain = layoutChainFor(pageFile, webRoot);\r\n const modules = await Promise.all(chain.map(loadLayout));\r\n const selection = selectPageLayout(\r\n chain.map((layout, index) => ({\r\n layout,\r\n renders: typeof modules[index].default !== \"undefined\",\r\n })),\r\n );\r\n\r\n if (selection.type === \"rejected\") {\r\n throw new NestedLayoutsNotSupportedError(pageFile, selection.layouts);\r\n }\r\n\r\n return {\r\n chain,\r\n layoutFile: selection.type === \"selected\" ? selection.layout : chain.at(-1),\r\n prefix: modules.reduce(\r\n (composed, layoutModule) => composeRoutePath(composed, layoutModule.prefix ?? \"/\"),\r\n \"/\",\r\n ),\r\n prefixesByDirectory: Object.fromEntries(\r\n chain.flatMap((layoutFile, index) => {\r\n const prefix = modules[index].prefix;\r\n\r\n return prefix === undefined\r\n ? []\r\n : [[toPosix(path.relative(webRoot, path.dirname(layoutFile))), prefix]];\r\n }),\r\n ),\r\n };\r\n}\r\n\r\n/**\r\n * The layout slot's module for ONE request: the slot host's own namespace, with\r\n * the whole chain's middleware in place of its own — outermost first, which is\r\n * the order stage 3 runs the array in (`execute-page-request.ts:519-524`) and\r\n * the order an outer `optionalAuth` needs in order to have resolved an identity\r\n * before an inner `gate()` checks it.\r\n *\r\n * Loaded per call, not once at install time: a dev layout module must be the\r\n * one Vite is currently serving, edits and all.\r\n */\r\nasync function composeLayoutLevel(\r\n level: LayoutLevel & { layoutFile: string },\r\n loadLayout: LoadLayout,\r\n): Promise<LayoutModuleShape> {\r\n const modules = await Promise.all(level.chain.map(loadLayout));\r\n const hostIndex = level.chain.indexOf(level.layoutFile);\r\n const host = modules[hostIndex];\r\n\r\n return {\r\n ...host,\r\n middleware: modules.flatMap((layoutModule) => [...(layoutModule.middleware ?? [])]),\r\n loader: async (context) => {\r\n let hostData: unknown;\r\n\r\n for (let index = 0; index < modules.length; index++) {\r\n const value = await modules[index].loader?.(context);\r\n\r\n if (value instanceof Response || isLoaderShortCircuit(value)) return value;\r\n if (index === hostIndex) hostData = value;\r\n }\r\n\r\n return hostData;\r\n },\r\n };\r\n}\r\n\r\nexport type InstallPageRoutesOptions = {\r\n router: Router;\r\n vite: ViteDevServer;\r\n /** v5/app/src — pages live under \"<appSrcRoot>/web/**\". */\r\n appSrcRoot: string;\r\n /** v5/app/src/web/root.tsx — the single global app-root file. */\r\n appFile: string;\r\n /**\r\n * The application root Vite's dev server serves from — `dev-server.ts`'s\r\n * `paths.appRoot`, i.e. `<appRoot>/src === appSrcRoot` by default. Every\r\n * handler's stylesheet URLs are expressed relative to THIS, because that is\r\n * the root Vite's dev server actually resolves `/…` URLs against\r\n * (`stylesheet-urls.ts`'s `devStylesheetUrls`) — not `appSrcRoot`, which is\r\n * one directory level in.\r\n *\r\n * OPTIONAL and defaulted to `path.dirname(appSrcRoot)`: the caller that\r\n * wires dev boot (`web-connector.ts`) does not pass this field today, and\r\n * that default is exactly the relationship it constructs `appSrcRoot` from\r\n * (`appSrcRoot = path.join(appRoot, \"src\")`) — correct for every actual\r\n * deployment, and overridable by a caller with a non-default layout.\r\n */\r\n appRoot?: string;\r\n /** Browser module loaded after the server-rendered application and payload. */\r\n hydrationClientModuleUrl?: string;\r\n /**\r\n * UNUSED. Retained on this type only because `web-connector.ts` still builds\r\n * an options object naming it (`devStylesheetUrls(paths.appRoot,\r\n * paths.appFile)`, computed once for the whole application). Each handler\r\n * now computes its OWN stylesheet chain — `[root, ...outer-to-inner matched\r\n * layouts, page]`, via `devHandlerStylesheetUrls` — inside the registration\r\n * loop below, because a single application-wide list cannot express \"this\r\n * page's own CSS\" without also carrying every other page's.\r\n */\r\n stylesheetUrls?: readonly string[];\r\n /** Same helper `dev-server.ts` exports — passed in, not imported, to avoid a dev-server.ts <-> this-file cycle. */\r\n};\r\n\r\n/**\r\n * Registers every discoverable page into `options.router`. Throws\r\n * IMMEDIATELY, naming both files, the moment two pages declare the same\r\n * `route.path` — a registration-time failure, not a runtime 404 one of them\r\n * silently loses.\r\n *\r\n * Pages with no `route` export derive their path and name from their location\r\n * below `src/web`, using the same pure filesystem-routing helper as the build.\r\n */\r\nexport async function installPageRoutes(\r\n options: InstallPageRoutesOptions,\r\n): Promise<InstalledPageRoute[]> {\r\n const { router, vite, appSrcRoot, appFile, hydrationClientModuleUrl } = options;\r\n // See `InstallPageRoutesOptions.appRoot` for why this default, not\r\n // `appSrcRoot` itself, is the root every handler's CSS is resolved against.\r\n const stylesheetRoot = options.appRoot ?? path.dirname(appSrcRoot);\r\n const discovered = [...discoverPageFiles(appSrcRoot)].sort((left, right) =>\r\n left.pageFile < right.pageFile ? -1 : left.pageFile > right.pageFile ? 1 : 0,\r\n );\r\n\r\n // THE NOT-FOUND PAGE IS TAKEN OUT OF THE ORDINARY LOOP, not filtered inside\r\n // it. It has no `route` export to read, no path to compose and no collision\r\n // to check — every step below is about a page with a URL, and `404.page.tsx`\r\n // does not have one. Registering it here would put it at `/404`, which is not\r\n // a page anybody asked to be able to visit.\r\n const errorPageFiles = discovered.filter((page) => isErrorPageFile(page.pageFile));\r\n const notFoundPageFiles = discovered.filter((page) => isNotFoundPageFile(page.pageFile));\r\n const pageFiles = discovered.filter(\r\n (page) => !isNotFoundPageFile(page.pageFile) && !isErrorPageFile(page.pageFile),\r\n );\r\n\r\n if (errorPageFiles.length > 1) {\r\n throw new Error(\r\n `Two error pages were found: ${errorPageFiles.map((page) => page.pageFile).join(\", \")}.`,\r\n );\r\n }\r\n\r\n const errorPageFile = errorPageFiles[0]?.pageFile;\r\n\r\n // Parse only: the error boundary must remain lazy until a request actually\r\n // fails, while a route export is still rejected at install time.\r\n if (errorPageFile !== undefined) {\r\n const declarations = readRouteExports(errorPageFile);\r\n if (!declarations.ok) throw new NonLiteralRouteExportError(declarations.rejection);\r\n if (declarations.route !== undefined) throw new ErrorPageDeclaresRouteError(errorPageFile);\r\n }\r\n const loadErrorPage =\r\n errorPageFile === undefined\r\n ? undefined\r\n : () => vite.ssrLoadModule(errorPageFile) as Promise<ErrorPageModule>;\r\n\r\n if (notFoundPageFiles.length > 1) {\r\n throw new DuplicateNotFoundPageError(notFoundPageFiles.map((page) => page.pageFile));\r\n }\r\n\r\n const installed: InstalledPageRoute[] = [];\r\n const fileByPath = new Map<string, string>();\r\n\r\n for (const { pageFile, webRoot } of pageFiles) {\r\n const pageModule = (await vite.ssrLoadModule(pageFile)) as PageModuleShape;\r\n\r\n const sourceFile = canonicalSourceFileFor(pageFile, appSrcRoot);\r\n\r\n // Route identity is explicit when declared and filesystem-derived otherwise.\r\n const { declaredPath: routePath, name } = resolvePageRouteIdentity(\r\n pageModule.route,\r\n pageFile,\r\n appSrcRoot,\r\n );\r\n\r\n // Validated at INSTALL time, with everything else — a malformed `cache`\r\n // opt-in fails boot, not the first request that would have served it.\r\n const cache = resolvePageRouteCache(pageModule.route, pageFile);\r\n\r\n const loadLayout: LoadLayout = (layoutFile) =>\r\n vite.ssrLoadModule(layoutFile) as Promise<LayoutModuleShape>;\r\n const layoutLevel = await resolveLayoutLevel(pageFile, webRoot, loadLayout);\r\n const { layoutFile, prefix: layoutPrefix } = layoutLevel;\r\n\r\n const effectivePath =\r\n pageModule.route === undefined\r\n ? deriveFilesystemRoutePath({\r\n pageFile: filesystemPageFileFor(pageFile, appSrcRoot),\r\n layoutPrefixes: layoutLevel.prefixesByDirectory,\r\n })\r\n : composeRoutePath(layoutPrefix, routePath);\r\n\r\n const existingFile = fileByPath.get(effectivePath);\r\n\r\n if (existingFile) {\r\n throw new Error(\r\n `installPageRoutes: composed route path \"${effectivePath}\" (layout ` +\r\n `prefix \"${layoutPrefix}\" + route.path \"${routePath}\") is declared by two ` +\r\n `pages (web/src/server/install-page-routes.ts) — \"${existingFile}\" and ` +\r\n `\"${pageFile}\". Every page's composed route path must be unique.`,\r\n );\r\n }\r\n\r\n fileByPath.set(effectivePath, pageFile);\r\n\r\n // Every registered handler gets ITS OWN immutable, ordered, deduped CSS\r\n // chain: root, then every matched layout outer to inner\r\n // (`layoutLevel.chain`), then the page — the same order the render\r\n // pipeline loads that chain in, so cascade order matches load order.\r\n // Computed once here, at registration, not per request: dev re-registers\r\n // on every restart, so a stale chain cannot outlive the source edit that\r\n // changed it.\r\n const stylesheetUrls = devHandlerStylesheetUrls(stylesheetRoot, [\r\n appFile,\r\n ...layoutLevel.chain,\r\n pageFile,\r\n ]);\r\n\r\n await router.withSourceFile(sourceFile, () =>\r\n router.get(\r\n effectivePath,\r\n // The handler itself is `createPageRouteHandler`\r\n // (`web/src/server/create-page-route-handler.ts`) — a named seam a\r\n // future `type: \"page\"` route can bind to, and testable without a Vite\r\n // server. Vite appears here only as the dev answer to \"how do I load a\r\n // module\"; the handler takes that as an input and knows nothing else\r\n // about it.\r\n createPageRouteHandler({\r\n path: effectivePath,\r\n name,\r\n appFile,\r\n pageFile,\r\n layoutFile,\r\n // The layout slot's id resolves to the COMPOSED level — every layout's\r\n // middleware, in chain order — and every other id goes straight to\r\n // Vite. A one-layout chain has nothing to compose, so it is left to\r\n // resolve as the exact module Vite hands back, untouched.\r\n loadModule:\r\n layoutLevel.chain.length > 1 && layoutFile !== undefined\r\n ? (moduleId) =>\r\n moduleId === layoutFile\r\n ? composeLayoutLevel({ ...layoutLevel, layoutFile }, loadLayout)\r\n : vite.ssrLoadModule(moduleId)\r\n : (moduleId) => vite.ssrLoadModule(moduleId),\r\n // Registration tracks real module namespaces, not the composed\r\n // layout wrapper above. Loading the raw chain per request also lets\r\n // Vite hand over a replacement namespace after an HMR update; the\r\n // helper's WeakSet then gives that new identity its one invocation.\r\n loadRegistrationLayouts: () => Promise.all(layoutLevel.chain.map(loadLayout)),\r\n hydrationClientModuleUrl,\r\n loadErrorPage,\r\n stylesheetUrls,\r\n cache,\r\n }),\r\n // `isPage` marks this route as SSR-served. Pages and API routes share one\r\n // router and one route-name namespace, so the router's duplicate-name\r\n // error reads this flag to say which claimant is the page.\r\n { name, isPage: true },\r\n ),\r\n );\r\n\r\n installed.push({\r\n declaredPath: routePath,\r\n path: effectivePath,\r\n name,\r\n file: pageFile,\r\n layoutFile,\r\n });\r\n }\r\n\r\n /*\r\n THE CATCH-ALL, registered LAST and only when this application has a page\r\n surface at all. \"Configured with web, no pages yet\" is a legal state, and an\r\n application serving no pages has no page 404 to answer with — its unmatched\r\n URLs stay core's to answer, exactly as they are today.\r\n\r\n Registered even when the application ships no `404.page.tsx`: the framework\r\n default still answers 404, so an application that has not written one yet\r\n gets the right STATUS from the first request, and adding the file later\r\n changes the body and nothing else.\r\n */\r\n if (pageFiles.length > 0 || notFoundPageFiles.length > 0) {\r\n const notFoundPageFile = notFoundPageFiles[0]?.pageFile;\r\n\r\n // Read at INSTALL time, so a `route` export on the not-found page is\r\n // refused at boot with everything else — not on the first request that\r\n // misses, which is the one request nobody is watching.\r\n if (notFoundPageFile !== undefined) {\r\n const notFoundModule = (await vite.ssrLoadModule(notFoundPageFile)) as PageModuleShape;\r\n\r\n if (notFoundModule.route !== undefined) {\r\n throw new NotFoundPageDeclaresRouteError(notFoundPageFile);\r\n }\r\n }\r\n\r\n const registerNotFoundRoute = () =>\r\n router.get(\r\n NOT_FOUND_ROUTE_PATH,\r\n createNotFoundRouteHandler({\r\n renderPage:\r\n notFoundPageFile === undefined\r\n ? undefined\r\n : createPageRouteHandler({\r\n path: NOT_FOUND_ROUTE_PATH,\r\n name: NOT_FOUND_ROUTE_NAME,\r\n appFile,\r\n pageFile: notFoundPageFile,\r\n // NO LAYOUT, deliberately, and it is the same trade as \"no\r\n // loader on the 404 page\": a layout brings its whole chain's\r\n // middleware with it, and a guard that redirects or throws on\r\n // the not-found path turns a missing page into an incident. The\r\n // page renders inside the application root and nothing else.\r\n layoutFile: undefined,\r\n loadModule: (moduleId) => vite.ssrLoadModule(moduleId),\r\n hydrationClientModuleUrl,\r\n loadErrorPage,\r\n // NO LAYOUT means no layout CSS either — just root and the\r\n // not-found page's own stylesheets, same reasoning as above.\r\n stylesheetUrls: devHandlerStylesheetUrls(stylesheetRoot, [\r\n appFile,\r\n notFoundPageFile,\r\n ]),\r\n // The URL that missed IS this route's pattern for this request.\r\n matchPath: (requestPath) => requestPath,\r\n statusForRenderedOk: 404,\r\n skipPageLoader: true,\r\n }),\r\n }),\r\n // `isPage` for the same reason every other page route carries it: the\r\n // router's duplicate-name error reads the flag to say which claimant is\r\n // the page.\r\n { name: NOT_FOUND_ROUTE_NAME, isPage: true },\r\n );\r\n\r\n if (notFoundPageFile === undefined) {\r\n await router.withSourceFile(FRAMEWORK_DEFAULT_NOT_FOUND_SOURCE_FILE, registerNotFoundRoute);\r\n } else {\r\n await router.withSourceFile(\r\n canonicalSourceFileFor(notFoundPageFile, appSrcRoot),\r\n registerNotFoundRoute,\r\n );\r\n }\r\n }\r\n\r\n /*\r\n Published from the SAME loop that registered the routes, so `href()` and the\r\n router cannot disagree about where a name points. It happens here rather\r\n than in the caller because a caller that forgets leaves every `<Link>` on\r\n the server throwing at render — and dev republishes on every restart, which\r\n is why the table replaces wholesale instead of merging: a deleted page's\r\n name has to stop resolving.\r\n */\r\n publishRouteTable(installed, \"installPageRoutes (dev)\");\r\n\r\n return installed;\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"install-page-routes.mjs","names":["resolveComposedLayoutLevel"],"sources":["../../../../../../../web/src/server/install-page-routes.ts"],"sourcesContent":["/**\r\n * Registers every page {@link discoverPageFiles} finds under `<appSrcRoot>`\r\n * into Warlock's router (`router.get`, `core/src/router/router.ts:359-361`)\r\n * so `router.scanDevServer(fastify)` —\r\n * the sanctioned dev-server dispatch path (server matching is Warlock's\r\n * router; there is no second server matcher) — picks\r\n * it up. Replaces the two hand-rolled `fastify.get()` calls this file's\r\n * sibling, `dev-error-transport.ts`, used to make directly.\r\n *\r\n * DELIBERATE EXCEPTION to \"web has no core dependency\", same\r\n * reasoning `dev-error-transport.ts`'s own header comment records: this module is not\r\n * exported from either package barrel and is not part of `web/package.json`'s\r\n * dependency graph — dev/CLI bootstrap only.\r\n *\r\n * Scope note: a page's\r\n * `route.path` is now composed with the `prefix` export of EVERY `layout.tsx`\r\n * on its path — outermost first (`composeRoutePath` below) — before\r\n * registration and before the collision check, so `home.page.tsx`\r\n * (`path: \"/\"`, main layout `prefix: \"/\"`) resolves to `/` and\r\n * `products.page.tsx` (`path: \"/\"`, products layout `prefix: \"/products\"`)\r\n * resolves to `/products` — no collision. A page with no `layout.tsx` on its\r\n * path composes against the implicit root prefix `\"/\"` (e.g. `/contact-us`,\r\n * `/hydration-demo`, both unaffected by composition).\r\n *\r\n * WHICH PAGES EXIST is answered by {@link discoverPageFiles}\r\n * (`web/src/build/discover-pages.ts`) — the same walk production's build\r\n * shares — so this file owns no directory-walking of its own; it serves the\r\n * page root (`<appSrcRoot>/web/**`) exactly as discovery enumerates it. WHAT\r\n * ROUTE A PAGE ANSWERS ON stays this file's own job: each page and its nearest layout are still evaluated\r\n * through Vite (`vite.ssrLoadModule`), never read statically, because a dev\r\n * page module must be the one Vite serves, warm cache and all.\r\n */\r\nimport path from \"node:path\";\r\nimport type { ViteDevServer } from \"vite\";\r\nimport {\r\n discoverPageFiles,\r\n ErrorPageDeclaresRouteError,\r\n isErrorPageFile,\r\n layoutChainFor,\r\n} from \"../build/discover-pages\";\r\nimport { NonLiteralRouteExportError, readRouteExports } from \"../build/read-route-exports\";\r\nimport { composeRoutePath } from \"../routing/compose-route-path\";\r\nimport { duplicateRoutePathMessage } from \"../routing/duplicate-route-path\";\r\nimport { deriveFilesystemRoutePath } from \"../routing/filesystem-route\";\r\nimport { resolveLayoutLevel as resolveComposedLayoutLevel } from \"../routing/layout-level\";\r\nimport { PageFileSegmentNotSupportedError } from \"../routing/page-file-segment\";\r\nimport { toPosix } from \"../shared/to-posix\";\r\nimport {\r\n resolvePageRouteCache,\r\n resolvePageRouteIdentity,\r\n resolvePageRouteName,\r\n type PageCacheOptIn,\r\n} from \"../routing/route-identity\";\r\nimport { publishRouteTable } from \"../routing/route-table\";\r\nimport { type FastifyInstance, type Router } from \"@warlock.js/core\";\r\nimport { createPageRouteHandler } from \"./create-page-route-handler\";\r\nimport type { ErrorPageModule } from \"./error-page\";\r\nimport type { PipelineLoader, PipelineMiddleware } from \"./execute-page-request\";\r\nimport { foldLayoutLoaders } from \"./fold-layout-loaders\";\r\nimport { devHandlerStylesheetUrls } from \"./stylesheet-urls\";\r\nimport {\r\n createNotFoundRouteHandler,\r\n DuplicateNotFoundPageError,\r\n isNotFoundPageFile,\r\n NotFoundPageDeclaresRouteError,\r\n NOT_FOUND_ROUTE_NAME,\r\n NOT_FOUND_ROUTE_PATH,\r\n} from \"./not-found-page\";\r\n\r\n/** Re-exported so `web/src/server/index.ts`'s existing barrel export keeps resolving. */\r\nexport { composeRoutePath };\r\n\r\nexport type PageRouteExport = string | { path: string; name?: string; cache?: PageCacheOptIn };\r\n\r\nexport type PageModuleShape = {\r\n route?: PageRouteExport;\r\n};\r\n\r\nexport type InstalledPageRoute = {\r\n /** The canonical declared route path, before layout-prefix composition. */\r\n declaredPath: string;\r\n path: string;\r\n name: string;\r\n file: string;\r\n layoutFile: string | undefined;\r\n};\r\n\r\n/**\r\n * Ownership key for the framework's fallback 404 route. A NUL-prefixed value\r\n * cannot be a real filesystem path, so it cannot collide with an app page's\r\n * canonical source-file key.\r\n */\r\nexport const FRAMEWORK_DEFAULT_NOT_FOUND_SOURCE_FILE = \"\\0warlock:framework-default-404\";\r\n\r\n/**\r\n * The page's application-source-relative POSIX source path used as the router's\r\n * stable ownership key. `appSrcRoot`'s own basename preserves the existing\r\n * `src/web/...` source-file convention.\r\n */\r\nfunction canonicalSourceFileFor(pageFile: string, appSrcRoot: string): string {\r\n return `${path.basename(appSrcRoot)}/${toPosix(path.relative(appSrcRoot, pageFile))}`;\r\n}\r\n\r\nexport function filesystemPageFileFor(pageFile: string, appSrcRoot: string): string {\r\n return toPosix(path.relative(path.join(appSrcRoot, \"web\"), pageFile));\r\n}\r\n\r\n/**\r\n * Raised when a page's own module fails to load in dev (`vite.ssrLoadModule`\r\n * rejects on the page file itself — never a layout, which this class does not\r\n * cover).\r\n *\r\n * Before this class existed, that rejection propagated straight out of\r\n * `installPageRoutes`'s loop and failed the WHOLE install: every other page's\r\n * route went unregistered along with the broken one, and the failure named\r\n * neither the page file nor which module actually threw. One bad page taking\r\n * every other page down with it is a worse outage than the bad page alone, so\r\n * this error is what the broken page's own route now fails with instead —\r\n * named, with its cause attached, while the rest of the application keeps\r\n * serving.\r\n */\r\nexport class PageModuleLoadError extends Error {\r\n public constructor(\r\n public readonly pageFile: string,\r\n cause: unknown,\r\n ) {\r\n const rawCause = cause instanceof Error ? cause.message : String(cause);\r\n // The cause is someone else's text — a bundler's, a module resolver's — so\r\n // it may or may not end in punctuation. Without this it runs straight into\r\n // the next sentence, and the seam is exactly where a reader stops trusting\r\n // the message.\r\n const causeMessage = /[.!?]$/.test(rawCause.trim()) ? rawCause.trim() : `${rawCause.trim()}.`;\r\n\r\n super(\r\n `\"${pageFile}\" failed to load: ${causeMessage} Every other page still installed and is ` +\r\n \"still serving; fix the error in this page's module and it will start serving again.\",\r\n { cause },\r\n );\r\n this.name = \"PageModuleLoadError\";\r\n }\r\n}\r\n\r\n/**\r\n * Registers a page whose OWN module failed to load at its filesystem-derived\r\n * URL, so a request there reports {@link PageModuleLoadError} — naming the\r\n * page file and the underlying cause — instead of a bare 404 that explains\r\n * nothing.\r\n *\r\n * Only the filesystem-derived path is attempted: the page's `route` export\r\n * cannot be read (that requires the very module that failed to load), and its\r\n * layout chain is not resolved either, so no layout prefix composes into this\r\n * path. When the filesystem path itself is not derivable\r\n * (`deriveFilesystemRoutePath` rejects a segment), there is no path left to\r\n * register a route at, so nothing is registered — the caller reports both\r\n * failures loudly at boot and moves on.\r\n */\r\nasync function registerFailedPageRoute(input: {\r\n router: Router;\r\n pageFile: string;\r\n appSrcRoot: string;\r\n loadError: unknown;\r\n fileByPath: Map<string, string>;\r\n}): Promise<void> {\r\n const { router, pageFile, appSrcRoot, loadError, fileByPath } = input;\r\n const attributed = new PageModuleLoadError(pageFile, loadError);\r\n const filesystemPageFile = filesystemPageFileFor(pageFile, appSrcRoot);\r\n\r\n let effectivePath: string;\r\n\r\n try {\r\n effectivePath = deriveFilesystemRoutePath({ pageFile: filesystemPageFile });\r\n } catch (segmentError) {\r\n if (!(segmentError instanceof PageFileSegmentNotSupportedError)) throw segmentError;\r\n\r\n // No filesystem path to register a route at, so this page cannot answer\r\n // its own URL with the attributed error — the request that would have hit\r\n // it 404s instead, unregistered, and the existing unregistered-page\r\n // reporter (`web/src/server/unregistered-pages.ts`) is what explains that\r\n // 404 to whoever is looking. Reported loudly here so the underlying load\r\n // failure is not lost as well.\r\n console.error(attributed.message);\r\n console.error(segmentError.message);\r\n\r\n return;\r\n }\r\n\r\n const existingFile = fileByPath.get(effectivePath);\r\n\r\n if (existingFile) {\r\n throw new Error(duplicateRoutePathMessage({ effectivePath, existingFile, newFile: pageFile }));\r\n }\r\n\r\n fileByPath.set(effectivePath, pageFile);\r\n\r\n await router.withSourceFile(canonicalSourceFileFor(pageFile, appSrcRoot), () =>\r\n router.get(\r\n effectivePath,\r\n async () => {\r\n throw attributed;\r\n },\r\n { name: resolvePageRouteName(undefined, filesystemPageFile), isPage: true },\r\n ),\r\n );\r\n}\r\n\r\nexport type LayoutModuleShape = {\r\n /** Universal registration hook; invoked on this real namespace, never a composed wrapper. */\r\n register?: () => unknown;\r\n prefix?: string;\r\n /**\r\n * The default export — the thing that puts an element in the document, and\r\n * therefore the ONLY export that decides whether a layout counts against the\r\n * single-rendering-layout rule (`../routing/layout-policy.ts`). In dev the\r\n * module is loaded, so this is a fact rather than a guess.\r\n */\r\n default?: unknown;\r\n /** The layout's guards, in the order it declared them. */\r\n middleware?: readonly PipelineMiddleware[];\r\n loader?: PipelineLoader;\r\n};\r\n\r\n/** How this module gets a layout module namespace — `vite.ssrLoadModule`, in practice. */\r\ntype LoadLayout = (layoutFile: string) => Promise<LayoutModuleShape>;\r\n\r\n/**\r\n * The page's layout LEVEL, resolved from its whole chain rather than from the\r\n * one layout nearest to it.\r\n *\r\n * The render pipeline has exactly one layout slot per page\r\n * (`execute-page-request.ts`'s `PageRouteEntry[\"triple\"]`), so the chain has to\r\n * be collapsed into one module before it reaches a handler. Two things collapse\r\n * differently and both matter:\r\n *\r\n * - RENDERING is a selection: at most one layout on the chain may render, and\r\n * the policy picks it. `renders` is read off the loaded module\r\n * (`typeof module.default !== \"undefined\"`), never off the filename — a\r\n * `middleware`-only layout has no default export and is not a wrapper, and\r\n * passing a bare path to the policy would have it read as a rendering one,\r\n * which is the conservative default and the wrong answer here.\r\n * - MIDDLEWARE and PREFIX are compositions: every layout on the path\r\n * contributes, outermost first. A guard on an outer layout that the page's\r\n * own directory knows nothing about is exactly the guard that must still run,\r\n * and a prefix nobody composed is a URL nobody wrote down.\r\n *\r\n * The selection and prefix composition rules themselves do not depend on\r\n * where a layout's module came from, so they are not re-derived here — see\r\n * `../routing/layout-level.ts`'s `resolveLayoutLevel`, which production's\r\n * installer calls against the same rules. This function's own job is\r\n * everything that DOES depend on the source: walking the chain\r\n * (`layoutChainFor`), loading each module (`loadLayout`) and keying declared\r\n * prefixes by filesystem directory for {@link deriveFilesystemRoutePath}.\r\n */\r\ntype LayoutLevel = {\r\n /** Every `layout.tsx` from the web root down to the page's directory, outermost first. */\r\n chain: string[];\r\n /**\r\n * The module id the handler's layout slot is registered under, or `undefined`\r\n * when the page has no layout at all: the layout that RENDERS, or — when none\r\n * does — the nearest one, which is the slot dev has always used and so the\r\n * choice that changes nothing but the middleware for a chain with no wrapper\r\n * in it.\r\n */\r\n layoutFile: string | undefined;\r\n /** Every layout's `prefix`, composed outermost first — `discoverPages`' own reduction. */\r\n prefix: string;\r\n /** Declared prefixes keyed by layout directory relative to this page's web root. */\r\n prefixesByDirectory: Readonly<Record<string, string>>;\r\n};\r\n\r\nasync function resolveLayoutLevel(\r\n pageFile: string,\r\n webRoot: string,\r\n loadLayout: LoadLayout,\r\n): Promise<LayoutLevel> {\r\n const chain = layoutChainFor(pageFile, webRoot);\r\n const modules = await Promise.all(chain.map(loadLayout));\r\n const level = resolveComposedLayoutLevel(\r\n pageFile,\r\n chain.map((layoutFile, index) => ({\r\n id: layoutFile,\r\n renders: typeof modules[index].default !== \"undefined\",\r\n prefix: modules[index].prefix,\r\n })),\r\n );\r\n\r\n return {\r\n chain,\r\n layoutFile: level.hostId,\r\n prefix: level.prefix,\r\n prefixesByDirectory: Object.fromEntries(\r\n chain.flatMap((layoutFile, index) => {\r\n const prefix = modules[index].prefix;\r\n\r\n return prefix === undefined\r\n ? []\r\n : [[toPosix(path.relative(webRoot, path.dirname(layoutFile))), prefix]];\r\n }),\r\n ),\r\n };\r\n}\r\n\r\n/**\r\n * The layout slot's module for ONE request: the slot host's own namespace, with\r\n * the whole chain's middleware in place of its own — outermost first, which is\r\n * the order stage 3 runs the array in (`execute-page-request.ts:519-524`) and\r\n * the order an outer `optionalAuth` needs in order to have resolved an identity\r\n * before an inner `gate()` checks it.\r\n *\r\n * Loaded per call, not once at install time: a dev layout module must be the\r\n * one Vite is currently serving, edits and all.\r\n */\r\nasync function composeLayoutLevel(\r\n level: LayoutLevel & { layoutFile: string },\r\n loadLayout: LoadLayout,\r\n): Promise<LayoutModuleShape> {\r\n const modules = await Promise.all(level.chain.map(loadLayout));\r\n const hostIndex = level.chain.indexOf(level.layoutFile);\r\n const host = modules[hostIndex];\r\n\r\n return {\r\n ...host,\r\n middleware: modules.flatMap((layoutModule) => [...(layoutModule.middleware ?? [])]),\r\n loader: foldLayoutLoaders(\r\n modules.map((layoutModule) => layoutModule.loader),\r\n hostIndex,\r\n ),\r\n };\r\n}\r\n\r\nexport type InstallPageRoutesOptions = {\r\n router: Router;\r\n vite: ViteDevServer;\r\n /** v5/app/src — pages live under \"<appSrcRoot>/web/**\". */\r\n appSrcRoot: string;\r\n /** v5/app/src/web/root.tsx — the single global app-root file. */\r\n appFile: string;\r\n /**\r\n * The application root Vite's dev server serves from — `dev-error-transport.ts`'s\r\n * `paths.appRoot`, i.e. `<appRoot>/src === appSrcRoot` by default. Every\r\n * handler's stylesheet URLs are expressed relative to THIS, because that is\r\n * the root Vite's dev server actually resolves `/…` URLs against\r\n * (`stylesheet-urls.ts`'s `devStylesheetUrls`) — not `appSrcRoot`, which is\r\n * one directory level in.\r\n *\r\n * OPTIONAL and defaulted to `path.dirname(appSrcRoot)`: the caller that\r\n * wires dev boot (`web-connector.ts`) does not pass this field today, and\r\n * that default is exactly the relationship it constructs `appSrcRoot` from\r\n * (`appSrcRoot = path.join(appRoot, \"src\")`) — correct for every actual\r\n * deployment, and overridable by a caller with a non-default layout.\r\n */\r\n appRoot?: string;\r\n /** Browser module loaded after the server-rendered application and payload. */\r\n hydrationClientModuleUrl?: string;\r\n /**\r\n * UNUSED. Retained on this type only because `web-connector.ts` still builds\r\n * an options object naming it (`devStylesheetUrls(paths.appRoot,\r\n * paths.appFile)`, computed once for the whole application). Each handler\r\n * now computes its OWN stylesheet chain — `[root, ...outer-to-inner matched\r\n * layouts, page]`, via `devHandlerStylesheetUrls` — inside the registration\r\n * loop below, because a single application-wide list cannot express \"this\r\n * page's own CSS\" without also carrying every other page's.\r\n */\r\n stylesheetUrls?: readonly string[];\r\n /**\r\n * The Fastify instance `HttpConnector.boot()` published, resolved by the\r\n * caller on the NODE side (`web-connector.ts`) and forwarded to every\r\n * `createPageRouteHandler` call below instead of letting that factory read\r\n * `container.get(\"http.server\")` for itself.\r\n *\r\n * Load-bearing, not a convenience: Vite's SSR module runner\r\n * (`vite.ssrLoadModule`, used throughout this file) evaluates\r\n * `create-page-route-handler.ts` as a SECOND copy of `@warlock.js/core`, with\r\n * its own `container` that `HttpConnector.boot()` never wrote to. Reading the\r\n * container from inside that SSR graph therefore always misses, however\r\n * early or late this file calls it — the value has to arrive as a plain\r\n * argument from a caller on the Node side, where the real container lives.\r\n *\r\n * OPTIONAL so existing callers (and this file's own unit tests, which seed\r\n * `container.set(\"http.server\", …)` instead) keep resolving through the\r\n * container exactly as before — omitted here means \"not supplied\", the same\r\n * distinction `createPageRouteHandler` itself draws from `PageRouteHandlerOptions.httpServer`.\r\n */\r\n httpServer?: FastifyInstance;\r\n};\r\n\r\n/**\r\n * Registers every discoverable page into `options.router`. Throws\r\n * IMMEDIATELY, naming both files, the moment two pages declare the same\r\n * `route.path` — a registration-time failure, not a runtime 404 one of them\r\n * silently loses.\r\n *\r\n * Pages with no `route` export derive their path and name from their location\r\n * below `src/web`, using the same pure filesystem-routing helper as the build.\r\n */\r\nexport async function installPageRoutes(\r\n options: InstallPageRoutesOptions,\r\n): Promise<InstalledPageRoute[]> {\r\n const { router, vite, appSrcRoot, appFile, hydrationClientModuleUrl, httpServer } = options;\r\n // Spread conditionally, never as a bare `httpServer,` property: an explicit\r\n // `httpServer: undefined` key is its own signal to `createPageRouteHandler`\r\n // (`\"httpServer\" in options\"`, `create-page-route-handler.ts`) — \"no server,\r\n // on purpose\" — and this file must not manufacture that signal on behalf of\r\n // a caller (this file's own unit tests, most callers) that never supplied\r\n // one and means to fall back to the container instead.\r\n const httpServerOption = httpServer === undefined ? {} : { httpServer };\r\n // See `InstallPageRoutesOptions.appRoot` for why this default, not\r\n // `appSrcRoot` itself, is the root every handler's CSS is resolved against.\r\n const stylesheetRoot = options.appRoot ?? path.dirname(appSrcRoot);\r\n const discovered = [...discoverPageFiles(appSrcRoot)].sort((left, right) =>\r\n left.pageFile < right.pageFile ? -1 : left.pageFile > right.pageFile ? 1 : 0,\r\n );\r\n\r\n // THE NOT-FOUND PAGE IS TAKEN OUT OF THE ORDINARY LOOP, not filtered inside\r\n // it. It has no `route` export to read, no path to compose and no collision\r\n // to check — every step below is about a page with a URL, and `404.page.tsx`\r\n // does not have one. Registering it here would put it at `/404`, which is not\r\n // a page anybody asked to be able to visit.\r\n const errorPageFiles = discovered.filter((page) => isErrorPageFile(page.pageFile));\r\n const notFoundPageFiles = discovered.filter((page) => isNotFoundPageFile(page.pageFile));\r\n const pageFiles = discovered.filter(\r\n (page) => !isNotFoundPageFile(page.pageFile) && !isErrorPageFile(page.pageFile),\r\n );\r\n\r\n if (errorPageFiles.length > 1) {\r\n throw new Error(\r\n `Two error pages were found: ${errorPageFiles.map((page) => page.pageFile).join(\", \")}.`,\r\n );\r\n }\r\n\r\n const errorPageFile = errorPageFiles[0]?.pageFile;\r\n\r\n // Parse only: the error boundary must remain lazy until a request actually\r\n // fails, while a route export is still rejected at install time.\r\n if (errorPageFile !== undefined) {\r\n const declarations = readRouteExports(errorPageFile);\r\n if (!declarations.ok) throw new NonLiteralRouteExportError(declarations.rejection);\r\n if (declarations.route !== undefined) throw new ErrorPageDeclaresRouteError(errorPageFile);\r\n }\r\n const loadErrorPage =\r\n errorPageFile === undefined\r\n ? undefined\r\n : () => vite.ssrLoadModule(errorPageFile) as Promise<ErrorPageModule>;\r\n\r\n if (notFoundPageFiles.length > 1) {\r\n throw new DuplicateNotFoundPageError(notFoundPageFiles.map((page) => page.pageFile));\r\n }\r\n\r\n const installed: InstalledPageRoute[] = [];\r\n const fileByPath = new Map<string, string>();\r\n\r\n for (const { pageFile, webRoot } of pageFiles) {\r\n let pageModule: PageModuleShape;\r\n\r\n try {\r\n pageModule = (await vite.ssrLoadModule(pageFile)) as PageModuleShape;\r\n } catch (loadError) {\r\n // THE PAGE ITSELF MUST NOT ABORT THE INSTALL: every other page still\r\n // needs to install and serve. Layout loading is deliberately NOT\r\n // wrapped here — a broken layout is a different failure with a\r\n // different blast radius (it can affect more than one page) and is out\r\n // of this card's scope.\r\n await registerFailedPageRoute({ router, pageFile, appSrcRoot, loadError, fileByPath });\r\n\r\n continue;\r\n }\r\n\r\n const sourceFile = canonicalSourceFileFor(pageFile, appSrcRoot);\r\n\r\n // Route identity is explicit when declared and filesystem-derived otherwise.\r\n const { path: routePath, name } = resolvePageRouteIdentity(\r\n pageModule.route,\r\n filesystemPageFileFor(pageFile, appSrcRoot),\r\n pageFile,\r\n );\r\n\r\n // Validated at INSTALL time, with everything else — a malformed `cache`\r\n // opt-in fails boot, not the first request that would have served it.\r\n const cache = resolvePageRouteCache(pageModule.route, pageFile);\r\n\r\n const loadLayout: LoadLayout = (layoutFile) =>\r\n vite.ssrLoadModule(layoutFile) as Promise<LayoutModuleShape>;\r\n const layoutLevel = await resolveLayoutLevel(pageFile, webRoot, loadLayout);\r\n const { layoutFile, prefix: layoutPrefix } = layoutLevel;\r\n\r\n const effectivePath =\r\n pageModule.route === undefined\r\n ? deriveFilesystemRoutePath({\r\n pageFile: filesystemPageFileFor(pageFile, appSrcRoot),\r\n layoutPrefixes: layoutLevel.prefixesByDirectory,\r\n })\r\n : composeRoutePath(layoutPrefix, routePath);\r\n\r\n const existingFile = fileByPath.get(effectivePath);\r\n\r\n if (existingFile) {\r\n throw new Error(\r\n duplicateRoutePathMessage({\r\n effectivePath,\r\n existingFile,\r\n newFile: pageFile,\r\n composition: { layoutPrefix, routePath },\r\n }),\r\n );\r\n }\r\n\r\n fileByPath.set(effectivePath, pageFile);\r\n\r\n // Every registered handler gets ITS OWN immutable, ordered, deduped CSS\r\n // chain: root, then every matched layout outer to inner\r\n // (`layoutLevel.chain`), then the page — the same order the render\r\n // pipeline loads that chain in, so cascade order matches load order.\r\n // Computed once here, at registration, not per request: dev re-registers\r\n // on every restart, so a stale chain cannot outlive the source edit that\r\n // changed it.\r\n const stylesheetUrls = devHandlerStylesheetUrls(stylesheetRoot, [\r\n appFile,\r\n ...layoutLevel.chain,\r\n pageFile,\r\n ]);\r\n\r\n await router.withSourceFile(sourceFile, () =>\r\n router.get(\r\n effectivePath,\r\n // The handler itself is `createPageRouteHandler`\r\n // (`web/src/server/create-page-route-handler.ts`) — a named seam a\r\n // future `type: \"page\"` route can bind to, and testable without a Vite\r\n // server. Vite appears here only as the dev answer to \"how do I load a\r\n // module\"; the handler takes that as an input and knows nothing else\r\n // about it.\r\n createPageRouteHandler({\r\n path: effectivePath,\r\n name,\r\n appFile,\r\n pageFile,\r\n layoutFile,\r\n // The layout slot's id resolves to the COMPOSED level — every layout's\r\n // middleware, in chain order — and every other id goes straight to\r\n // Vite. A one-layout chain has nothing to compose, so it is left to\r\n // resolve as the exact module Vite hands back, untouched.\r\n loadModule:\r\n layoutLevel.chain.length > 1 && layoutFile !== undefined\r\n ? (moduleId) =>\r\n moduleId === layoutFile\r\n ? composeLayoutLevel({ ...layoutLevel, layoutFile }, loadLayout)\r\n : vite.ssrLoadModule(moduleId)\r\n : (moduleId) => vite.ssrLoadModule(moduleId),\r\n // Registration tracks real module namespaces, not the composed\r\n // layout wrapper above. Loading the raw chain per request also lets\r\n // Vite hand over a replacement namespace after an HMR update; the\r\n // helper's WeakSet then gives that new identity its one invocation.\r\n loadRegistrationLayouts: () => Promise.all(layoutLevel.chain.map(loadLayout)),\r\n hydrationClientModuleUrl,\r\n loadErrorPage,\r\n stylesheetUrls,\r\n cache,\r\n ...httpServerOption,\r\n }),\r\n // `isPage` marks this route as SSR-served. Pages and API routes share one\r\n // router and one route-name namespace, so the router's duplicate-name\r\n // error reads this flag to say which claimant is the page.\r\n { name, isPage: true },\r\n ),\r\n );\r\n\r\n installed.push({\r\n declaredPath: routePath,\r\n path: effectivePath,\r\n name,\r\n file: pageFile,\r\n layoutFile,\r\n });\r\n }\r\n\r\n /*\r\n THE CATCH-ALL, registered LAST and only when this application has a page\r\n surface at all. \"Configured with web, no pages yet\" is a legal state, and an\r\n application serving no pages has no page 404 to answer with — its unmatched\r\n URLs stay core's to answer, exactly as they are today.\r\n\r\n Registered even when the application ships no `404.page.tsx`: the framework\r\n default still answers 404, so an application that has not written one yet\r\n gets the right STATUS from the first request, and adding the file later\r\n changes the body and nothing else.\r\n */\r\n if (pageFiles.length > 0 || notFoundPageFiles.length > 0) {\r\n const notFoundPageFile = notFoundPageFiles[0]?.pageFile;\r\n\r\n // Read at INSTALL time, so a `route` export on the not-found page is\r\n // refused at boot with everything else — not on the first request that\r\n // misses, which is the one request nobody is watching.\r\n if (notFoundPageFile !== undefined) {\r\n const notFoundModule = (await vite.ssrLoadModule(notFoundPageFile)) as PageModuleShape;\r\n\r\n if (notFoundModule.route !== undefined) {\r\n throw new NotFoundPageDeclaresRouteError(notFoundPageFile);\r\n }\r\n }\r\n\r\n const registerNotFoundRoute = () =>\r\n router.get(\r\n NOT_FOUND_ROUTE_PATH,\r\n createNotFoundRouteHandler({\r\n renderPage:\r\n notFoundPageFile === undefined\r\n ? undefined\r\n : createPageRouteHandler({\r\n path: NOT_FOUND_ROUTE_PATH,\r\n name: NOT_FOUND_ROUTE_NAME,\r\n appFile,\r\n pageFile: notFoundPageFile,\r\n // NO LAYOUT, deliberately, and it is the same trade as \"no\r\n // loader on the 404 page\": a layout brings its whole chain's\r\n // middleware with it, and a guard that redirects or throws on\r\n // the not-found path turns a missing page into an incident. The\r\n // page renders inside the application root and nothing else.\r\n layoutFile: undefined,\r\n loadModule: (moduleId) => vite.ssrLoadModule(moduleId),\r\n hydrationClientModuleUrl,\r\n loadErrorPage,\r\n // NO LAYOUT means no layout CSS either — just root and the\r\n // not-found page's own stylesheets, same reasoning as above.\r\n stylesheetUrls: devHandlerStylesheetUrls(stylesheetRoot, [\r\n appFile,\r\n notFoundPageFile,\r\n ]),\r\n // The URL that missed IS this route's pattern for this request.\r\n matchPath: (requestPath) => requestPath,\r\n statusForRenderedOk: 404,\r\n skipPageLoader: true,\r\n ...httpServerOption,\r\n }),\r\n }),\r\n // `isPage` for the same reason every other page route carries it: the\r\n // router's duplicate-name error reads the flag to say which claimant is\r\n // the page.\r\n { name: NOT_FOUND_ROUTE_NAME, isPage: true },\r\n );\r\n\r\n if (notFoundPageFile === undefined) {\r\n await router.withSourceFile(FRAMEWORK_DEFAULT_NOT_FOUND_SOURCE_FILE, registerNotFoundRoute);\r\n } else {\r\n await router.withSourceFile(\r\n canonicalSourceFileFor(notFoundPageFile, appSrcRoot),\r\n registerNotFoundRoute,\r\n );\r\n }\r\n }\r\n\r\n /*\r\n Published from the SAME loop that registered the routes, so `href()` and the\r\n router cannot disagree about where a name points. It happens here rather\r\n than in the caller because a caller that forgets leaves every `<Link>` on\r\n the server throwing at render — and dev republishes on every restart, which\r\n is why the table replaces wholesale instead of merging: a deleted page's\r\n name has to stop resolving.\r\n */\r\n publishRouteTable(installed, \"installPageRoutes (dev)\");\r\n\r\n return installed;\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4FA,MAAa,0CAA0C;;;;;;AAOvD,SAAS,uBAAuB,UAAkB,YAA4B;CAC5E,OAAO,GAAG,KAAK,SAAS,UAAU,EAAE,GAAG,QAAQ,KAAK,SAAS,YAAY,QAAQ,CAAC;AACpF;AAEA,SAAgB,sBAAsB,UAAkB,YAA4B;CAClF,OAAO,QAAQ,KAAK,SAAS,KAAK,KAAK,YAAY,KAAK,GAAG,QAAQ,CAAC;AACtE;;;;;;;;;;;;;;;AAgBA,IAAa,sBAAb,cAAyC,MAAM;CAE3B;CADlB,AAAO,YACL,AAAgB,UAChB,OACA;EACA,MAAM,WAAW,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EAKtE,MAAM,eAAe,SAAS,KAAK,SAAS,KAAK,CAAC,IAAI,SAAS,KAAK,IAAI,GAAG,SAAS,KAAK,EAAE;EAE3F,MACE,IAAI,SAAS,oBAAoB,aAAa,+HAE9C,EAAE,MAAM,CACV;EAdgB;EAehB,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;;;AAgBA,eAAe,wBAAwB,OAMrB;CAChB,MAAM,EAAE,QAAQ,UAAU,YAAY,WAAW,eAAe;CAChE,MAAM,aAAa,IAAI,oBAAoB,UAAU,SAAS;CAC9D,MAAM,qBAAqB,sBAAsB,UAAU,UAAU;CAErE,IAAI;CAEJ,IAAI;EACF,gBAAgB,0BAA0B,EAAE,UAAU,mBAAmB,CAAC;CAC5E,SAAS,cAAc;EACrB,IAAI,EAAE,wBAAwB,mCAAmC,MAAM;EAQvE,QAAQ,MAAM,WAAW,OAAO;EAChC,QAAQ,MAAM,aAAa,OAAO;EAElC;CACF;CAEA,MAAM,eAAe,WAAW,IAAI,aAAa;CAEjD,IAAI,cACF,MAAM,IAAI,MAAM,0BAA0B;EAAE;EAAe;EAAc,SAAS;CAAS,CAAC,CAAC;CAG/F,WAAW,IAAI,eAAe,QAAQ;CAEtC,MAAM,OAAO,eAAe,uBAAuB,UAAU,UAAU,SACrE,OAAO,IACL,eACA,YAAY;EACV,MAAM;CACR,GACA;EAAE,MAAM,qBAAqB,QAAW,kBAAkB;EAAG,QAAQ;CAAK,CAC5E,CACF;AACF;AAkEA,eAAe,mBACb,UACA,SACA,YACsB;CACtB,MAAM,QAAQ,eAAe,UAAU,OAAO;CAC9C,MAAM,UAAU,MAAM,QAAQ,IAAI,MAAM,IAAI,UAAU,CAAC;CACvD,MAAM,QAAQA,qBACZ,UACA,MAAM,KAAK,YAAY,WAAW;EAChC,IAAI;EACJ,SAAS,OAAO,QAAQ,MAAM,CAAC,YAAY;EAC3C,QAAQ,QAAQ,MAAM,CAAC;CACzB,EAAE,CACJ;CAEA,OAAO;EACL;EACA,YAAY,MAAM;EAClB,QAAQ,MAAM;EACd,qBAAqB,OAAO,YAC1B,MAAM,SAAS,YAAY,UAAU;GACnC,MAAM,SAAS,QAAQ,MAAM,CAAC;GAE9B,OAAO,WAAW,SACd,CAAC,IACD,CAAC,CAAC,QAAQ,KAAK,SAAS,SAAS,KAAK,QAAQ,UAAU,CAAC,CAAC,GAAG,MAAM,CAAC;EAC1E,CAAC,CACH;CACF;AACF;;;;;;;;;;;AAYA,eAAe,mBACb,OACA,YAC4B;CAC5B,MAAM,UAAU,MAAM,QAAQ,IAAI,MAAM,MAAM,IAAI,UAAU,CAAC;CAC7D,MAAM,YAAY,MAAM,MAAM,QAAQ,MAAM,UAAU;CAGtD,OAAO;EACL,GAHW,QAAQ;EAInB,YAAY,QAAQ,SAAS,iBAAiB,CAAC,GAAI,aAAa,cAAc,CAAC,CAAE,CAAC;EAClF,QAAQ,kBACN,QAAQ,KAAK,iBAAiB,aAAa,MAAM,GACjD,SACF;CACF;AACF;;;;;;;;;;AAmEA,eAAsB,kBACpB,SAC+B;CAC/B,MAAM,EAAE,QAAQ,MAAM,YAAY,SAAS,0BAA0B,eAAe;CAOpF,MAAM,mBAAmB,eAAe,SAAY,CAAC,IAAI,EAAE,WAAW;CAGtE,MAAM,iBAAiB,QAAQ,WAAW,KAAK,QAAQ,UAAU;CACjE,MAAM,aAAa,CAAC,GAAG,kBAAkB,UAAU,CAAC,CAAC,CAAC,MAAM,MAAM,UAChE,KAAK,WAAW,MAAM,WAAW,KAAK,KAAK,WAAW,MAAM,WAAW,IAAI,CAC7E;CAOA,MAAM,iBAAiB,WAAW,QAAQ,SAAS,gBAAgB,KAAK,QAAQ,CAAC;CACjF,MAAM,oBAAoB,WAAW,QAAQ,SAAS,mBAAmB,KAAK,QAAQ,CAAC;CACvF,MAAM,YAAY,WAAW,QAC1B,SAAS,CAAC,mBAAmB,KAAK,QAAQ,KAAK,CAAC,gBAAgB,KAAK,QAAQ,CAChF;CAEA,IAAI,eAAe,SAAS,GAC1B,MAAM,IAAI,MACR,+BAA+B,eAAe,KAAK,SAAS,KAAK,QAAQ,CAAC,CAAC,KAAK,IAAI,EAAE,EACxF;CAGF,MAAM,gBAAgB,eAAe,EAAE,EAAE;CAIzC,IAAI,kBAAkB,QAAW;EAC/B,MAAM,eAAe,iBAAiB,aAAa;EACnD,IAAI,CAAC,aAAa,IAAI,MAAM,IAAI,2BAA2B,aAAa,SAAS;EACjF,IAAI,aAAa,UAAU,QAAW,MAAM,IAAI,4BAA4B,aAAa;CAC3F;CACA,MAAM,gBACJ,kBAAkB,SACd,eACM,KAAK,cAAc,aAAa;CAE5C,IAAI,kBAAkB,SAAS,GAC7B,MAAM,IAAI,2BAA2B,kBAAkB,KAAK,SAAS,KAAK,QAAQ,CAAC;CAGrF,MAAM,YAAkC,CAAC;CACzC,MAAM,6BAAa,IAAI,IAAoB;CAE3C,KAAK,MAAM,EAAE,UAAU,aAAa,WAAW;EAC7C,IAAI;EAEJ,IAAI;GACF,aAAc,MAAM,KAAK,cAAc,QAAQ;EACjD,SAAS,WAAW;GAMlB,MAAM,wBAAwB;IAAE;IAAQ;IAAU;IAAY;IAAW;GAAW,CAAC;GAErF;EACF;EAEA,MAAM,aAAa,uBAAuB,UAAU,UAAU;EAG9D,MAAM,EAAE,MAAM,WAAW,SAAS,yBAChC,WAAW,OACX,sBAAsB,UAAU,UAAU,GAC1C,QACF;EAIA,MAAM,QAAQ,sBAAsB,WAAW,OAAO,QAAQ;EAE9D,MAAM,cAA0B,eAC9B,KAAK,cAAc,UAAU;EAC/B,MAAM,cAAc,MAAM,mBAAmB,UAAU,SAAS,UAAU;EAC1E,MAAM,EAAE,YAAY,QAAQ,iBAAiB;EAE7C,MAAM,gBACJ,WAAW,UAAU,SACjB,0BAA0B;GACxB,UAAU,sBAAsB,UAAU,UAAU;GACpD,gBAAgB,YAAY;EAC9B,CAAC,IACD,iBAAiB,cAAc,SAAS;EAE9C,MAAM,eAAe,WAAW,IAAI,aAAa;EAEjD,IAAI,cACF,MAAM,IAAI,MACR,0BAA0B;GACxB;GACA;GACA,SAAS;GACT,aAAa;IAAE;IAAc;GAAU;EACzC,CAAC,CACH;EAGF,WAAW,IAAI,eAAe,QAAQ;EAStC,MAAM,iBAAiB,yBAAyB,gBAAgB;GAC9D;GACA,GAAG,YAAY;GACf;EACF,CAAC;EAED,MAAM,OAAO,eAAe,kBAC1B,OAAO,IACL,eAOA,uBAAuB;GACrB,MAAM;GACN;GACA;GACA;GACA;GAKA,YACE,YAAY,MAAM,SAAS,KAAK,eAAe,UAC1C,aACC,aAAa,aACT,mBAAmB;IAAE,GAAG;IAAa;GAAW,GAAG,UAAU,IAC7D,KAAK,cAAc,QAAQ,KAChC,aAAa,KAAK,cAAc,QAAQ;GAK/C,+BAA+B,QAAQ,IAAI,YAAY,MAAM,IAAI,UAAU,CAAC;GAC5E;GACA;GACA;GACA;GACA,GAAG;EACL,CAAC,GAID;GAAE;GAAM,QAAQ;EAAK,CACvB,CACF;EAEA,UAAU,KAAK;GACb,cAAc;GACd,MAAM;GACN;GACA,MAAM;GACN;EACF,CAAC;CACH;CAaA,IAAI,UAAU,SAAS,KAAK,kBAAkB,SAAS,GAAG;EACxD,MAAM,mBAAmB,kBAAkB,EAAE,EAAE;EAK/C,IAAI,qBAAqB,QAGvB;QAAI,MAF0B,KAAK,cAAc,gBAAgB,EAE/C,CAAC,UAAU,QAC3B,MAAM,IAAI,+BAA+B,gBAAgB;EAC3D;EAGF,MAAM,8BACJ,OAAO,SAEL,2BAA2B,EACzB,YACE,qBAAqB,SACjB,SACA,uBAAuB;GACrB;GACA,MAAM;GACN;GACA,UAAU;GAMV,YAAY;GACZ,aAAa,aAAa,KAAK,cAAc,QAAQ;GACrD;GACA;GAGA,gBAAgB,yBAAyB,gBAAgB,CACvD,SACA,gBACF,CAAC;GAED,YAAY,gBAAgB;GAC5B,qBAAqB;GACrB,gBAAgB;GAChB,GAAG;EACL,CAAC,EACT,CAAC,GAID;GAAE,MAAM;GAAsB,QAAQ;EAAK,CAC7C;EAEF,IAAI,qBAAqB,QACvB,MAAM,OAAO,eAAe,yCAAyC,qBAAqB;OAE1F,MAAM,OAAO,eACX,uBAAuB,kBAAkB,UAAU,GACnD,qBACF;CAEJ;CAUA,kBAAkB,WAAW,yBAAyB;CAEtD,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"install-production-page-routes.mjs","names":[],"sources":["../../../../../../../web/src/server/install-production-page-routes.ts"],"sourcesContent":["/**\r\n * Standing up page serving for a BUILT application — the production half of\r\n * `WebConnector.boot()`, kept out of the connector so the mode branch there\r\n * stays one `if` and this path stays readable on its own.\r\n *\r\n * The development path answers \"which pages exist?\" by walking `app/` and\r\n * \"what is this module?\" by asking Vite to evaluate it. A production process\r\n * can ask neither question: there is no source tree beside the bundle and no\r\n * Vite. Both answers arrived at import time instead, in the generated pages\r\n * barrel's {@link PageManifest}, and everything below is what remains once\r\n * those two questions are already answered — a guard, a wiring step and a\r\n * registration step.\r\n *\r\n * NOTHING HERE REACHES FOR VITE, transitively included. `vite` is an optional\r\n * peer, so a production install does not carry it; an import that only ever\r\n * runs in development still fails at module load in production, which is why\r\n * the one import that has to happen at boot is a dynamic one and why it names\r\n * the pipeline barrel and nothing else.\r\n */\r\nimport type { Router } from \"@warlock.js/core\";\r\nimport type { SharedStoreResolver } from \"../shared\";\r\nimport type { PageContextRunner } from \"./execute-page-request\";\r\nimport type { InstalledManifestPageRoute } from \"./install-page-routes-from-manifest\";\r\nimport type { PageManifest } from \"./page-manifest\";\r\n\r\nexport type InstallProductionPageRoutesOptions = {\r\n router: Router;\r\n /** The table the generated production barrel provided at import time. */\r\n manifest: PageManifest;\r\n /**\r\n * Core's per-request context, handed over rather than imported: core is a\r\n * type-only peer of web, and two copies of core would mean two\r\n * AsyncLocalStorage stores and a page pipeline reading an empty one.\r\n */\r\n pageContext: PageContextRunner;\r\n /** Reads the current request's store out of {@link pageContext}. */\r\n sharedStore: SharedStoreResolver;\r\n /** Same helper `dev-server.ts` exports — passed in, never imported. */\r\n /**\r\n * Where the browser fetches the hydration entry from.\r\n *\r\n * A THUNK, not a value: the URL is read out of the client build's manifest,\r\n * and a build that discovered no pages has no hydration entry for that read\r\n * to find. Deferring it means a page-free production bundle boots without\r\n * demanding a file it had no reason to emit.\r\n */\r\n resolveHydrationClientModuleUrl: () => string;\r\n /**\r\n * Where the client build wrote its output. Forwarded to\r\n * `installPageRoutesFromManifest`, never read here: every registered\r\n * handler needs its OWN stylesheet chain\r\n * (`[root, ...outer-to-inner matched layouts, page]`), and the installer is\r\n * the one place that already walks the manifest's per-page layout chains to\r\n * build one.\r\n *\r\n * OPTIONAL for the same reason `PageManifest.clientDir` is: a build that\r\n * discovered zero pages emits no client bundle, so there is no directory to\r\n * name — and no page that could need a stylesheet either.\r\n */\r\n clientDir?: string;\r\n};\r\n\r\n/**\r\n * Register every page the manifest carries, and connect the pipeline the\r\n * handlers will run in.\r\n *\r\n * An empty table registers nothing and is not an error: \"built with web, no\r\n * pages\" is a legal state of a built application, and it is also the one state\r\n * where there is no pipeline to connect and no hydration entry to point at, so\r\n * it returns before doing either.\r\n */\r\nexport async function installProductionPageRoutes(\r\n options: InstallProductionPageRoutesOptions,\r\n): Promise<InstalledManifestPageRoute[]> {\r\n const {\r\n router,\r\n manifest,\r\n pageContext,\r\n sharedStore,\r\n resolveHydrationClientModuleUrl,\r\n clientDir,\r\n } = options;\r\n\r\n if (manifest.pages.length === 0) return [];\r\n\r\n // Loaded at boot rather than statically, and through the BARREL: production\r\n // has one module graph, so the barrel's instance of the pipeline is the same\r\n // instance the manifest's page modules were linked against, and connecting\r\n // the request context on it connects it for every handler registered below.\r\n // Development cannot do this — its modules live in Vite's separate graph, and\r\n // it wires the same two seams on the copy Vite evaluated.\r\n const webServer = await import(\"./index\");\r\n\r\n webServer.connectSharedStore(sharedStore);\r\n webServer.connectPageContext(pageContext);\r\n\r\n return webServer.installPageRoutesFromManifest({\r\n router,\r\n manifest,\r\n hydrationClientModuleUrl: resolveHydrationClientModuleUrl(),\r\n clientDir,\r\n });\r\n}\r\n"],"mappings":";;;;;;;;;;AAuEA,eAAsB,4BACpB,SACuC;CACvC,MAAM,EACJ,QACA,UACA,aACA,aACA,iCACA,cACE;CAEJ,IAAI,SAAS,MAAM,WAAW,GAAG,OAAO,CAAC;CAQzC,MAAM,YAAY,MAAM,OAAO;CAE/B,UAAU,mBAAmB,WAAW;CACxC,UAAU,mBAAmB,WAAW;CAExC,OAAO,UAAU,8BAA8B;EAC7C;EACA;EACA,0BAA0B,gCAAgC;EAC1D;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"install-production-page-routes.mjs","names":[],"sources":["../../../../../../../web/src/server/install-production-page-routes.ts"],"sourcesContent":["/**\r\n * Standing up page serving for a BUILT application — the production half of\r\n * `WebConnector.boot()`, kept out of the connector so the mode branch there\r\n * stays one `if` and this path stays readable on its own.\r\n *\r\n * The development path answers \"which pages exist?\" by walking `app/` and\r\n * \"what is this module?\" by asking Vite to evaluate it. A production process\r\n * can ask neither question: there is no source tree beside the bundle and no\r\n * Vite. Both answers arrived at import time instead, in the generated pages\r\n * barrel's {@link PageManifest}, and everything below is what remains once\r\n * those two questions are already answered — a guard, a wiring step and a\r\n * registration step.\r\n *\r\n * NOTHING HERE REACHES FOR VITE, transitively included. `vite` is an optional\r\n * peer, so a production install does not carry it; an import that only ever\r\n * runs in development still fails at module load in production, which is why\r\n * the one import that has to happen at boot is a dynamic one and why it names\r\n * the pipeline barrel and nothing else.\r\n */\r\nimport type { Router } from \"@warlock.js/core\";\r\nimport type { SharedStoreResolver } from \"../shared\";\r\nimport type { PageContextRunner } from \"./execute-page-request\";\r\nimport type { InstalledManifestPageRoute } from \"./install-page-routes-from-manifest\";\r\nimport type { PageManifest } from \"./page-manifest\";\r\n\r\nexport type InstallProductionPageRoutesOptions = {\r\n router: Router;\r\n /** The table the generated production barrel provided at import time. */\r\n manifest: PageManifest;\r\n /**\r\n * Core's per-request context, handed over rather than imported: core is a\r\n * type-only peer of web, and two copies of core would mean two\r\n * AsyncLocalStorage stores and a page pipeline reading an empty one.\r\n */\r\n pageContext: PageContextRunner;\r\n /** Reads the current request's store out of {@link pageContext}. */\r\n sharedStore: SharedStoreResolver;\r\n /** Same helper `dev-error-transport.ts` exports — passed in, never imported. */\r\n /**\r\n * Where the browser fetches the hydration entry from.\r\n *\r\n * A THUNK, not a value: the URL is read out of the client build's manifest,\r\n * and a build that discovered no pages has no hydration entry for that read\r\n * to find. Deferring it means a page-free production bundle boots without\r\n * demanding a file it had no reason to emit.\r\n */\r\n resolveHydrationClientModuleUrl: () => string;\r\n /**\r\n * Where the client build wrote its output. Forwarded to\r\n * `installPageRoutesFromManifest`, never read here: every registered\r\n * handler needs its OWN stylesheet chain\r\n * (`[root, ...outer-to-inner matched layouts, page]`), and the installer is\r\n * the one place that already walks the manifest's per-page layout chains to\r\n * build one.\r\n *\r\n * OPTIONAL for the same reason `PageManifest.clientDir` is: a build that\r\n * discovered zero pages emits no client bundle, so there is no directory to\r\n * name — and no page that could need a stylesheet either.\r\n */\r\n clientDir?: string;\r\n};\r\n\r\n/**\r\n * Register every page the manifest carries, and connect the pipeline the\r\n * handlers will run in.\r\n *\r\n * An empty table registers nothing and is not an error: \"built with web, no\r\n * pages\" is a legal state of a built application, and it is also the one state\r\n * where there is no pipeline to connect and no hydration entry to point at, so\r\n * it returns before doing either.\r\n */\r\nexport async function installProductionPageRoutes(\r\n options: InstallProductionPageRoutesOptions,\r\n): Promise<InstalledManifestPageRoute[]> {\r\n const { router, manifest, pageContext, sharedStore, resolveHydrationClientModuleUrl, clientDir } =\r\n options;\r\n\r\n if (manifest.pages.length === 0) return [];\r\n\r\n // Loaded at boot rather than statically, and through the BARREL: production\r\n // has one module graph, so the barrel's instance of the pipeline is the same\r\n // instance the manifest's page modules were linked against, and connecting\r\n // the request context on it connects it for every handler registered below.\r\n // Development cannot do this — its modules live in Vite's separate graph, and\r\n // it wires the same two seams on the copy Vite evaluated.\r\n const webServer = await import(\"./index\");\r\n\r\n webServer.connectSharedStore(sharedStore);\r\n webServer.connectPageContext(pageContext);\r\n\r\n return webServer.installPageRoutesFromManifest({\r\n router,\r\n manifest,\r\n hydrationClientModuleUrl: resolveHydrationClientModuleUrl(),\r\n clientDir,\r\n });\r\n}\r\n"],"mappings":";;;;;;;;;;AAuEA,eAAsB,4BACpB,SACuC;CACvC,MAAM,EAAE,QAAQ,UAAU,aAAa,aAAa,iCAAiC,cACnF;CAEF,IAAI,SAAS,MAAM,WAAW,GAAG,OAAO,CAAC;CAQzC,MAAM,YAAY,MAAM,OAAO;CAE/B,UAAU,mBAAmB,WAAW;CACxC,UAAU,mBAAmB,WAAW;CAExC,OAAO,UAAU,8BAA8B;EAC7C;EACA;EACA,0BAA0B,gCAAgC;EAC1D;CACF,CAAC;AACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"match-page-route.mjs","names":[],"sources":["../../../../../../../web/src/server/match-page-route.ts"],"sourcesContent":["import type { PageRouteEntry } from \"./execute-page-request.types\";\n\n/**\n * Stage 1 — turn a URL into a route entry plus its params.\n *\n * ⚠ **This is a SECOND matcher, and on the HTTP path it is redundant.** Core's\n * router has already matched by the time a page handler runs, and\n * `create-page-route-handler.ts` ignores the match it is handed. The one caller\n * that genuinely needs this is `renderPage(name, options)`, which synthesizes a\n * URL with no HTTP request behind it — and that path is not wired up\n * (`connectPageRoutes()` is never called).\n *\n * Removing it from the HTTP path is carded. Two things must be proven first:\n * that these params agree with core's, since `bundle.route.params` reaches the\n * hydration payload; and that dropping `bundle.route.query` — a public type\n * member — is announced rather than slipped in.\n */\n\nfunction splitSegments(path: string): string[] {\n return path.split(\"/\").filter(segment => segment.length > 0);\n}\n\nexport function matchPath(\n pattern: string,\n pathname: string,\n): Record<string, string> | undefined {\n const patternSegments = splitSegments(pattern);\n const pathSegments = splitSegments(pathname);\n\n if (patternSegments.length !== pathSegments.length) return undefined;\n\n const params: Record<string, string> = {};\n\n for (let index = 0; index < patternSegments.length; index++) {\n const patternSegment = patternSegments[index];\n const pathSegment = pathSegments[index];\n\n if (patternSegment.startsWith(\":\")) {\n params[patternSegment.slice(1)] = decodeURIComponent(pathSegment);\n continue;\n }\n\n if (patternSegment !== pathSegment) return undefined;\n }\n\n return params;\n}\n\nexport function matchRoute(\n pathname: string,\n routes: readonly PageRouteEntry[],\n): { entry: PageRouteEntry; params: Record<string, string> } | undefined {\n for (const entry of routes) {\n const params = matchPath(entry.path, pathname);\n\n if (params) return { entry, params };\n }\n\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAkBA,SAAS,cAAc,MAAwB;CAC7C,OAAO,KAAK,MAAM,GAAG,CAAC,CAAC,QAAO,YAAW,QAAQ,SAAS,CAAC;AAC7D;AAEA,SAAgB,UACd,SACA,UACoC;CACpC,MAAM,kBAAkB,cAAc,OAAO;CAC7C,MAAM,eAAe,cAAc,QAAQ;CAE3C,IAAI,gBAAgB,WAAW,aAAa,QAAQ,OAAO;CAE3D,MAAM,SAAiC,CAAC;CAExC,KAAK,IAAI,QAAQ,GAAG,QAAQ,gBAAgB,QAAQ,SAAS;EAC3D,MAAM,iBAAiB,gBAAgB;EACvC,MAAM,cAAc,aAAa;EAEjC,IAAI,eAAe,WAAW,GAAG,GAAG;GAClC,OAAO,eAAe,MAAM,CAAC,KAAK,mBAAmB,WAAW;GAChE;EACF;EAEA,IAAI,mBAAmB,aAAa,OAAO;CAC7C;CAEA,OAAO;AACT;AAEA,SAAgB,WACd,UACA,QACuE;CACvE,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,SAAS,UAAU,MAAM,MAAM,QAAQ;EAE7C,IAAI,QAAQ,OAAO;GAAE;GAAO;EAAO;CACrC;AAGF"}
1
+ {"version":3,"file":"match-page-route.mjs","names":[],"sources":["../../../../../../../web/src/server/match-page-route.ts"],"sourcesContent":["import type { PageRouteEntry } from \"./execute-page-request.types\";\r\n\r\n/**\r\n * Stage 1 — turn a URL into a route entry plus its params.\r\n *\r\n * ⚠ **This is a SECOND matcher, and on the HTTP path it is redundant.** Core's\r\n * router has already matched by the time a page handler runs, and\r\n * `create-page-route-handler.ts` ignores the match it is handed. The one caller\r\n * that genuinely needs this is `renderPage(name, options)`, which synthesizes a\r\n * URL with no HTTP request behind it — and that path is not wired up\r\n * (`connectPageRoutes()` is never called).\r\n *\r\n * Removing it from the HTTP path is carded. Two things must be proven first:\r\n * that these params agree with core's, since `bundle.route.params` reaches the\r\n * hydration payload; and that dropping `bundle.route.query` — a public type\r\n * member — is announced rather than slipped in.\r\n */\r\n\r\nfunction splitSegments(path: string): string[] {\r\n return path.split(\"/\").filter((segment) => segment.length > 0);\r\n}\r\n\r\nexport function matchPath(pattern: string, pathname: string): Record<string, string> | undefined {\r\n const patternSegments = splitSegments(pattern);\r\n const pathSegments = splitSegments(pathname);\r\n\r\n if (patternSegments.length !== pathSegments.length) return undefined;\r\n\r\n const params: Record<string, string> = {};\r\n\r\n for (let index = 0; index < patternSegments.length; index++) {\r\n const patternSegment = patternSegments[index];\r\n const pathSegment = pathSegments[index];\r\n\r\n if (patternSegment.startsWith(\":\")) {\r\n params[patternSegment.slice(1)] = decodeURIComponent(pathSegment);\r\n continue;\r\n }\r\n\r\n if (patternSegment !== pathSegment) return undefined;\r\n }\r\n\r\n return params;\r\n}\r\n\r\nexport function matchRoute(\r\n pathname: string,\r\n routes: readonly PageRouteEntry[],\r\n): { entry: PageRouteEntry; params: Record<string, string> } | undefined {\r\n for (const entry of routes) {\r\n const params = matchPath(entry.path, pathname);\r\n\r\n if (params) return { entry, params };\r\n }\r\n\r\n return undefined;\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;AAkBA,SAAS,cAAc,MAAwB;CAC7C,OAAO,KAAK,MAAM,GAAG,CAAC,CAAC,QAAQ,YAAY,QAAQ,SAAS,CAAC;AAC/D;AAEA,SAAgB,UAAU,SAAiB,UAAsD;CAC/F,MAAM,kBAAkB,cAAc,OAAO;CAC7C,MAAM,eAAe,cAAc,QAAQ;CAE3C,IAAI,gBAAgB,WAAW,aAAa,QAAQ,OAAO;CAE3D,MAAM,SAAiC,CAAC;CAExC,KAAK,IAAI,QAAQ,GAAG,QAAQ,gBAAgB,QAAQ,SAAS;EAC3D,MAAM,iBAAiB,gBAAgB;EACvC,MAAM,cAAc,aAAa;EAEjC,IAAI,eAAe,WAAW,GAAG,GAAG;GAClC,OAAO,eAAe,MAAM,CAAC,KAAK,mBAAmB,WAAW;GAChE;EACF;EAEA,IAAI,mBAAmB,aAAa,OAAO;CAC7C;CAEA,OAAO;AACT;AAEA,SAAgB,WACd,UACA,QACuE;CACvE,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,SAAS,UAAU,MAAM,MAAM,QAAQ;EAE7C,IAAI,QAAQ,OAAO;GAAE;GAAO;EAAO;CACrC;AAGF"}
@@ -1 +1 @@
1
- {"version":3,"file":"page-file-change.mjs","names":[],"sources":["../../../../../../../web/src/server/page-file-change.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nexport const PAGE_FILE_SUFFIX = \".page.tsx\";\n\nexport type PageFileChanges = {\n added: string[];\n removed: string[];\n inspectionNeeded: string[];\n};\n\nexport type PageFileChangeOptions = {\n appRoot: string;\n appSrcRoot: string;\n installedPageFiles: readonly string[];\n fileExists?: (file: string) => boolean;\n};\n\nfunction isFile(file: string): boolean {\n try {\n return fs.statSync(file).isFile();\n } catch {\n return false;\n }\n}\n\nfunction normalizePath(appRoot: string, file: string): string {\n return path.resolve(appRoot, file.replace(/[\\\\/]+/g, path.sep));\n}\n\nfunction pathKey(file: string): string {\n const normalized = file.replace(/\\\\/g, \"/\");\n\n return process.platform === \"win32\" ? normalized.toLowerCase() : normalized;\n}\n\nexport function isPageFilePath(file: string, appSrcRoot: string): boolean {\n if (!file.endsWith(PAGE_FILE_SUFFIX)) {\n return false;\n }\n\n const relative = path.relative(appSrcRoot, file);\n\n return (\n relative !== \"\" &&\n !relative.startsWith(`..${path.sep}`) &&\n relative !== \"..\" &&\n !path.isAbsolute(relative) &&\n relative.split(path.sep)[0] === \"web\"\n );\n}\n\n/**\n * Layouts contribute to every descendant page's derived route identity, but\n * never own a route themselves. Keep this predicate aligned with\n * `page-registry-plugin.ts` so a layout lifecycle event re-derives the whole\n * page table rather than being discarded as an ordinary Vite update.\n */\nexport function isPageLayoutFilePath(file: string, appSrcRoot: string): boolean {\n const relative = path.relative(appSrcRoot, file);\n const base = path.basename(file);\n\n return (\n relative !== \"\" &&\n !relative.startsWith(`..${path.sep}`) &&\n relative !== \"..\" &&\n !path.isAbsolute(relative) &&\n relative.split(path.sep)[0] === \"web\" &&\n (base === \"layout.ts\" || base === \"layout.tsx\" || /\\.layout\\.tsx?$/.test(base))\n );\n}\n\n/** Error boundaries participate in reload tracking but never in router ownership. */\nexport function isErrorPageFilePath(file: string): boolean {\n return path.basename(file) === \"error.page.tsx\";\n}\n\nexport function classifyPageFileChanges(\n changedFiles: readonly string[],\n options: PageFileChangeOptions,\n): PageFileChanges {\n const pageRoot = path.resolve(options.appRoot, options.appSrcRoot);\n const installed = new Set(\n options.installedPageFiles.map((file) => pathKey(normalizePath(options.appRoot, file))),\n );\n const classified: PageFileChanges = { added: [], removed: [], inspectionNeeded: [] };\n const seen = new Set<string>();\n const fileExists = options.fileExists ?? isFile;\n\n for (const changedFile of changedFiles) {\n const file = normalizePath(options.appRoot, changedFile);\n const key = pathKey(file);\n\n if (seen.has(key)) {\n continue;\n }\n\n seen.add(key);\n\n // A layout has no individual router owner. Its addition, removal, or\n // in-place edit can change every descendant page's prefix/layout chain, so\n // force the existing full page-table derivation transaction. `added` is\n // deliberately the transaction's \"replacement required\" bucket here;\n // it does not claim the layout itself owns a newly-added route.\n if (isPageLayoutFilePath(file, pageRoot)) {\n (fileExists(file) ? classified.added : classified.removed).push(file);\n continue;\n }\n\n if (!isPageFilePath(file, pageRoot)) {\n continue;\n }\n\n const isInstalled = installed.has(key);\n const isErrorPage = isErrorPageFilePath(file);\n if (!fileExists(file)) {\n if (isInstalled || isErrorPage) {\n classified.removed.push(file);\n }\n continue;\n }\n\n if (isInstalled || isErrorPage) {\n classified.inspectionNeeded.push(file);\n } else {\n classified.added.push(file);\n }\n }\n\n return classified;\n}\n\nexport function hasPageFileChanges(changes: PageFileChanges): boolean {\n return (\n changes.added.length > 0 ||\n changes.removed.length > 0 ||\n changes.inspectionNeeded.length > 0\n );\n}\n"],"mappings":";;;;AAGA,MAAa,mBAAmB;AAehC,SAAS,OAAO,MAAuB;CACrC,IAAI;EACF,OAAO,GAAG,SAAS,IAAI,CAAC,CAAC,OAAO;CAClC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,cAAc,SAAiB,MAAsB;CAC5D,OAAO,KAAK,QAAQ,SAAS,KAAK,QAAQ,WAAW,KAAK,GAAG,CAAC;AAChE;AAEA,SAAS,QAAQ,MAAsB;CACrC,MAAM,aAAa,KAAK,QAAQ,OAAO,GAAG;CAE1C,OAAO,QAAQ,aAAa,UAAU,WAAW,YAAY,IAAI;AACnE;AAEA,SAAgB,eAAe,MAAc,YAA6B;CACxE,IAAI,CAAC,KAAK,oBAAyB,GACjC,OAAO;CAGT,MAAM,WAAW,KAAK,SAAS,YAAY,IAAI;CAE/C,OACE,aAAa,MACb,CAAC,SAAS,WAAW,KAAK,KAAK,KAAK,KACpC,aAAa,QACb,CAAC,KAAK,WAAW,QAAQ,KACzB,SAAS,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO;AAEpC;;;;;;;AAQA,SAAgB,qBAAqB,MAAc,YAA6B;CAC9E,MAAM,WAAW,KAAK,SAAS,YAAY,IAAI;CAC/C,MAAM,OAAO,KAAK,SAAS,IAAI;CAE/B,OACE,aAAa,MACb,CAAC,SAAS,WAAW,KAAK,KAAK,KAAK,KACpC,aAAa,QACb,CAAC,KAAK,WAAW,QAAQ,KACzB,SAAS,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO,UAC/B,SAAS,eAAe,SAAS,gBAAgB,kBAAkB,KAAK,IAAI;AAEjF;;AAGA,SAAgB,oBAAoB,MAAuB;CACzD,OAAO,KAAK,SAAS,IAAI,MAAM;AACjC;AAEA,SAAgB,wBACd,cACA,SACiB;CACjB,MAAM,WAAW,KAAK,QAAQ,QAAQ,SAAS,QAAQ,UAAU;CACjE,MAAM,YAAY,IAAI,IACpB,QAAQ,mBAAmB,KAAK,SAAS,QAAQ,cAAc,QAAQ,SAAS,IAAI,CAAC,CAAC,CACxF;CACA,MAAM,aAA8B;EAAE,OAAO,CAAC;EAAG,SAAS,CAAC;EAAG,kBAAkB,CAAC;CAAE;CACnF,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,aAAa,QAAQ,cAAc;CAEzC,KAAK,MAAM,eAAe,cAAc;EACtC,MAAM,OAAO,cAAc,QAAQ,SAAS,WAAW;EACvD,MAAM,MAAM,QAAQ,IAAI;EAExB,IAAI,KAAK,IAAI,GAAG,GACd;EAGF,KAAK,IAAI,GAAG;EAOZ,IAAI,qBAAqB,MAAM,QAAQ,GAAG;GACxC,CAAC,WAAW,IAAI,IAAI,WAAW,QAAQ,WAAW,QAAO,CAAE,KAAK,IAAI;GACpE;EACF;EAEA,IAAI,CAAC,eAAe,MAAM,QAAQ,GAChC;EAGF,MAAM,cAAc,UAAU,IAAI,GAAG;EACrC,MAAM,cAAc,oBAAoB,IAAI;EAC5C,IAAI,CAAC,WAAW,IAAI,GAAG;GACrB,IAAI,eAAe,aACjB,WAAW,QAAQ,KAAK,IAAI;GAE9B;EACF;EAEA,IAAI,eAAe,aACjB,WAAW,iBAAiB,KAAK,IAAI;OAErC,WAAW,MAAM,KAAK,IAAI;CAE9B;CAEA,OAAO;AACT;AAEA,SAAgB,mBAAmB,SAAmC;CACpE,OACE,QAAQ,MAAM,SAAS,KACvB,QAAQ,QAAQ,SAAS,KACzB,QAAQ,iBAAiB,SAAS;AAEtC"}
1
+ {"version":3,"file":"page-file-change.mjs","names":[],"sources":["../../../../../../../web/src/server/page-file-change.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nexport const PAGE_FILE_SUFFIX = \".page.tsx\";\n\nexport type PageFileChanges = {\n added: string[];\n removed: string[];\n inspectionNeeded: string[];\n};\n\nexport type PageFileChangeOptions = {\n appRoot: string;\n appSrcRoot: string;\n installedPageFiles: readonly string[];\n fileExists?: (file: string) => boolean;\n};\n\nfunction isFile(file: string): boolean {\n try {\n return fs.statSync(file).isFile();\n } catch {\n return false;\n }\n}\n\nfunction normalizePath(appRoot: string, file: string): string {\n return path.resolve(appRoot, file.replace(/[\\\\/]+/g, path.sep));\n}\n\nfunction pathKey(file: string): string {\n const normalized = file.replace(/\\\\/g, \"/\");\n\n return process.platform === \"win32\" ? normalized.toLowerCase() : normalized;\n}\n\nexport function isPageFilePath(file: string, appSrcRoot: string): boolean {\n if (!file.endsWith(PAGE_FILE_SUFFIX)) {\n return false;\n }\n\n const relative = path.relative(appSrcRoot, file);\n\n return (\n relative !== \"\" &&\n !relative.startsWith(`..${path.sep}`) &&\n relative !== \"..\" &&\n !path.isAbsolute(relative) &&\n relative.split(path.sep)[0] === \"web\"\n );\n}\n\n/**\n * Layouts contribute to every descendant page's derived route identity, but\n * never own a route themselves. Keep this predicate aligned with\n * `page-registry-plugin.ts` so a layout lifecycle event re-derives the whole\n * page table rather than being discarded as an ordinary Vite update.\n */\nexport function isPageLayoutFilePath(file: string, appSrcRoot: string): boolean {\n const relative = path.relative(appSrcRoot, file);\n const base = path.basename(file);\n\n return (\n relative !== \"\" &&\n !relative.startsWith(`..${path.sep}`) &&\n relative !== \"..\" &&\n !path.isAbsolute(relative) &&\n relative.split(path.sep)[0] === \"web\" &&\n (base === \"layout.ts\" || base === \"layout.tsx\" || /\\.layout\\.tsx?$/.test(base))\n );\n}\n\n/** Error boundaries participate in reload tracking but never in router ownership. */\nexport function isErrorPageFilePath(file: string): boolean {\n return path.basename(file) === \"error.page.tsx\";\n}\n\nexport function classifyPageFileChanges(\n changedFiles: readonly string[],\n options: PageFileChangeOptions,\n): PageFileChanges {\n const pageRoot = path.resolve(options.appRoot, options.appSrcRoot);\n const installed = new Set(\n options.installedPageFiles.map((file) => pathKey(normalizePath(options.appRoot, file))),\n );\n const classified: PageFileChanges = { added: [], removed: [], inspectionNeeded: [] };\n const seen = new Set<string>();\n const fileExists = options.fileExists ?? isFile;\n\n for (const changedFile of changedFiles) {\n const file = normalizePath(options.appRoot, changedFile);\n const key = pathKey(file);\n\n if (seen.has(key)) {\n continue;\n }\n\n seen.add(key);\n\n // A layout has no individual router owner. Its addition, removal, or\n // in-place edit can change every descendant page's prefix/layout chain, so\n // force the existing full page-table derivation transaction. `added` is\n // deliberately the transaction's \"replacement required\" bucket here;\n // it does not claim the layout itself owns a newly-added route.\n if (isPageLayoutFilePath(file, pageRoot)) {\n (fileExists(file) ? classified.added : classified.removed).push(file);\n continue;\n }\n\n if (!isPageFilePath(file, pageRoot)) {\n continue;\n }\n\n const isInstalled = installed.has(key);\n const isErrorPage = isErrorPageFilePath(file);\n if (!fileExists(file)) {\n if (isInstalled || isErrorPage) {\n classified.removed.push(file);\n }\n continue;\n }\n\n if (isInstalled || isErrorPage) {\n classified.inspectionNeeded.push(file);\n } else {\n classified.added.push(file);\n }\n }\n\n return classified;\n}\n\nexport function hasPageFileChanges(changes: PageFileChanges): boolean {\n return (\n changes.added.length > 0 || changes.removed.length > 0 || changes.inspectionNeeded.length > 0\n );\n}\n"],"mappings":";;;;AAGA,MAAa,mBAAmB;AAehC,SAAS,OAAO,MAAuB;CACrC,IAAI;EACF,OAAO,GAAG,SAAS,IAAI,CAAC,CAAC,OAAO;CAClC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,cAAc,SAAiB,MAAsB;CAC5D,OAAO,KAAK,QAAQ,SAAS,KAAK,QAAQ,WAAW,KAAK,GAAG,CAAC;AAChE;AAEA,SAAS,QAAQ,MAAsB;CACrC,MAAM,aAAa,KAAK,QAAQ,OAAO,GAAG;CAE1C,OAAO,QAAQ,aAAa,UAAU,WAAW,YAAY,IAAI;AACnE;AAEA,SAAgB,eAAe,MAAc,YAA6B;CACxE,IAAI,CAAC,KAAK,oBAAyB,GACjC,OAAO;CAGT,MAAM,WAAW,KAAK,SAAS,YAAY,IAAI;CAE/C,OACE,aAAa,MACb,CAAC,SAAS,WAAW,KAAK,KAAK,KAAK,KACpC,aAAa,QACb,CAAC,KAAK,WAAW,QAAQ,KACzB,SAAS,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO;AAEpC;;;;;;;AAQA,SAAgB,qBAAqB,MAAc,YAA6B;CAC9E,MAAM,WAAW,KAAK,SAAS,YAAY,IAAI;CAC/C,MAAM,OAAO,KAAK,SAAS,IAAI;CAE/B,OACE,aAAa,MACb,CAAC,SAAS,WAAW,KAAK,KAAK,KAAK,KACpC,aAAa,QACb,CAAC,KAAK,WAAW,QAAQ,KACzB,SAAS,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO,UAC/B,SAAS,eAAe,SAAS,gBAAgB,kBAAkB,KAAK,IAAI;AAEjF;;AAGA,SAAgB,oBAAoB,MAAuB;CACzD,OAAO,KAAK,SAAS,IAAI,MAAM;AACjC;AAEA,SAAgB,wBACd,cACA,SACiB;CACjB,MAAM,WAAW,KAAK,QAAQ,QAAQ,SAAS,QAAQ,UAAU;CACjE,MAAM,YAAY,IAAI,IACpB,QAAQ,mBAAmB,KAAK,SAAS,QAAQ,cAAc,QAAQ,SAAS,IAAI,CAAC,CAAC,CACxF;CACA,MAAM,aAA8B;EAAE,OAAO,CAAC;EAAG,SAAS,CAAC;EAAG,kBAAkB,CAAC;CAAE;CACnF,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,aAAa,QAAQ,cAAc;CAEzC,KAAK,MAAM,eAAe,cAAc;EACtC,MAAM,OAAO,cAAc,QAAQ,SAAS,WAAW;EACvD,MAAM,MAAM,QAAQ,IAAI;EAExB,IAAI,KAAK,IAAI,GAAG,GACd;EAGF,KAAK,IAAI,GAAG;EAOZ,IAAI,qBAAqB,MAAM,QAAQ,GAAG;GACxC,CAAC,WAAW,IAAI,IAAI,WAAW,QAAQ,WAAW,QAAO,CAAE,KAAK,IAAI;GACpE;EACF;EAEA,IAAI,CAAC,eAAe,MAAM,QAAQ,GAChC;EAGF,MAAM,cAAc,UAAU,IAAI,GAAG;EACrC,MAAM,cAAc,oBAAoB,IAAI;EAC5C,IAAI,CAAC,WAAW,IAAI,GAAG;GACrB,IAAI,eAAe,aACjB,WAAW,QAAQ,KAAK,IAAI;GAE9B;EACF;EAEA,IAAI,eAAe,aACjB,WAAW,iBAAiB,KAAK,IAAI;OAErC,WAAW,MAAM,KAAK,IAAI;CAE9B;CAEA,OAAO;AACT;AAEA,SAAgB,mBAAmB,SAAmC;CACpE,OACE,QAAQ,MAAM,SAAS,KAAK,QAAQ,QAAQ,SAAS,KAAK,QAAQ,iBAAiB,SAAS;AAEhG"}
@@ -1,5 +1,6 @@
1
+ import { resolvePageRouteIdentity } from "../routing/route-identity.mjs";
1
2
  import { isNotFoundPageFile } from "./not-found-page.mjs";
2
- import { resolvePageRouteIdentity } from "./install-page-routes.mjs";
3
+ import { filesystemPageFileFor } from "./install-page-routes.mjs";
3
4
  import { isErrorPageFilePath } from "./page-file-change.mjs";
4
5
  import path from "node:path";
5
6
 
@@ -43,8 +44,8 @@ async function pageRoutesNeedReplacement(changes, options) {
43
44
  replace = true;
44
45
  continue;
45
46
  }
46
- const next = resolvePageRouteIdentity(pageModule.route, file, options.appSrcRoot);
47
- if (next.declaredPath !== installed.declaredPath || next.name !== installed.name) replace = true;
47
+ const next = resolvePageRouteIdentity(pageModule.route, filesystemPageFileFor(file, options.appSrcRoot), file);
48
+ if (next.path !== installed.declaredPath || next.name !== installed.name) replace = true;
48
49
  }
49
50
  return replace;
50
51
  }
@@ -1 +1 @@
1
- {"version":3,"file":"page-route-reload.mjs","names":[],"sources":["../../../../../../../web/src/server/page-route-reload.ts"],"sourcesContent":["import path from \"node:path\";\nimport type { Router } from \"@warlock.js/core\";\nimport type { ViteDevServer } from \"vite\";\nimport type { InstalledPageRoute, PageModuleShape } from \"./install-page-routes\";\nimport { resolvePageRouteIdentity } from \"./install-page-routes\";\nimport { isNotFoundPageFile } from \"./not-found-page\";\nimport type { PageFileChanges } from \"./page-file-change\";\nimport { isErrorPageFilePath } from \"./page-file-change\";\n\nfunction pathKey(file: string): string {\n const normalized = path.resolve(file).replace(/\\\\/g, \"/\");\n\n return process.platform === \"win32\" ? normalized.toLowerCase() : normalized;\n}\n\n/**\n * Every real page file currently represented in the route table. This includes\n * a custom `404.page.tsx`, which is intentionally absent from the public page\n * table because it has no linkable URL of its own.\n */\nexport function registeredPageFiles(\n routes: ReturnType<Router[\"list\"]>,\n appSrcRoot: string,\n): string[] {\n const sourceRoot = path.dirname(appSrcRoot);\n\n return [\n ...new Set(\n routes\n .filter((route) => route.isPage && route.sourceFile && !route.sourceFile.startsWith(\"\\0\"))\n .map((route) => path.resolve(sourceRoot, route.sourceFile)),\n ),\n ];\n}\n\n/** Exact route-source owners replaced by a live page-table transaction. */\nexport function pageRouteSourceFiles(routes: ReturnType<Router[\"list\"]>): string[] {\n return [\n ...new Set(\n routes\n .filter((route) => route.isPage && route.sourceFile)\n .map((route) => route.sourceFile),\n ),\n ];\n}\n\n/**\n * Inspect only in-place page edits. Vite's SSR graph is invalidated explicitly\n * before evaluation because core's file watcher and Vite's watcher have no\n * ordering contract. Component-only edits compare equal and stay in Fast\n * Refresh; route/name changes request an atomic table replacement.\n */\nexport async function pageRoutesNeedReplacement(\n changes: PageFileChanges,\n options: {\n vite: ViteDevServer;\n appSrcRoot: string;\n installedPages: readonly InstalledPageRoute[];\n },\n): Promise<boolean> {\n let replace = changes.added.length > 0 || changes.removed.length > 0;\n\n const installedByFile = new Map(\n options.installedPages.map((page) => [pathKey(page.file), page] as const),\n );\n\n for (const file of changes.inspectionNeeded) {\n // An error boundary has no installed router route to compare against. Its\n // namespace is projected into every client composition, so any edit must\n // replace the generated table; do not load it here just to inspect it.\n if (isErrorPageFilePath(file)) return true;\n\n options.vite.environments.ssr.moduleGraph.onFileChange(file);\n const pageModule = (await options.vite.ssrLoadModule(file)) as PageModuleShape;\n\n // A valid 404 page has no route identity. Its component body remains Vite\n // HMR territory; adding an illegal route export must still enter the\n // transaction so the installer can reject it without touching live routes.\n if (isNotFoundPageFile(file)) {\n if (pageModule.route !== undefined) replace = true;\n continue;\n }\n\n const installed = installedByFile.get(pathKey(file));\n if (installed === undefined) {\n replace = true;\n continue;\n }\n\n const next = resolvePageRouteIdentity(pageModule.route, file, options.appSrcRoot);\n if (next.declaredPath !== installed.declaredPath || next.name !== installed.name) {\n replace = true;\n }\n }\n\n return replace;\n}\n"],"mappings":";;;;;;AASA,SAAS,QAAQ,MAAsB;CACrC,MAAM,aAAa,KAAK,QAAQ,IAAI,CAAC,CAAC,QAAQ,OAAO,GAAG;CAExD,OAAO,QAAQ,aAAa,UAAU,WAAW,YAAY,IAAI;AACnE;;;;;;AAOA,SAAgB,oBACd,QACA,YACU;CACV,MAAM,aAAa,KAAK,QAAQ,UAAU;CAE1C,OAAO,CACL,GAAG,IAAI,IACL,OACG,QAAQ,UAAU,MAAM,UAAU,MAAM,cAAc,CAAC,MAAM,WAAW,WAAW,IAAI,CAAC,CAAC,CACzF,KAAK,UAAU,KAAK,QAAQ,YAAY,MAAM,UAAU,CAAC,CAC9D,CACF;AACF;;AAGA,SAAgB,qBAAqB,QAA8C;CACjF,OAAO,CACL,GAAG,IAAI,IACL,OACG,QAAQ,UAAU,MAAM,UAAU,MAAM,UAAU,CAAC,CACnD,KAAK,UAAU,MAAM,UAAU,CACpC,CACF;AACF;;;;;;;AAQA,eAAsB,0BACpB,SACA,SAKkB;CAClB,IAAI,UAAU,QAAQ,MAAM,SAAS,KAAK,QAAQ,QAAQ,SAAS;CAEnE,MAAM,kBAAkB,IAAI,IAC1B,QAAQ,eAAe,KAAK,SAAS,CAAC,QAAQ,KAAK,IAAI,GAAG,IAAI,CAAU,CAC1E;CAEA,KAAK,MAAM,QAAQ,QAAQ,kBAAkB;EAI3C,IAAI,oBAAoB,IAAI,GAAG,OAAO;EAEtC,QAAQ,KAAK,aAAa,IAAI,YAAY,aAAa,IAAI;EAC3D,MAAM,aAAc,MAAM,QAAQ,KAAK,cAAc,IAAI;EAKzD,IAAI,mBAAmB,IAAI,GAAG;GAC5B,IAAI,WAAW,UAAU,QAAW,UAAU;GAC9C;EACF;EAEA,MAAM,YAAY,gBAAgB,IAAI,QAAQ,IAAI,CAAC;EACnD,IAAI,cAAc,QAAW;GAC3B,UAAU;GACV;EACF;EAEA,MAAM,OAAO,yBAAyB,WAAW,OAAO,MAAM,QAAQ,UAAU;EAChF,IAAI,KAAK,iBAAiB,UAAU,gBAAgB,KAAK,SAAS,UAAU,MAC1E,UAAU;CAEd;CAEA,OAAO;AACT"}
1
+ {"version":3,"file":"page-route-reload.mjs","names":[],"sources":["../../../../../../../web/src/server/page-route-reload.ts"],"sourcesContent":["import path from \"node:path\";\nimport { resolvePageRouteIdentity } from \"../routing/route-identity\";\nimport type { Router } from \"@warlock.js/core\";\nimport type { ViteDevServer } from \"vite\";\nimport {\n filesystemPageFileFor,\n type InstalledPageRoute,\n type PageModuleShape,\n} from \"./install-page-routes\";\nimport { isNotFoundPageFile } from \"./not-found-page\";\nimport type { PageFileChanges } from \"./page-file-change\";\nimport { isErrorPageFilePath } from \"./page-file-change\";\n\nfunction pathKey(file: string): string {\n const normalized = path.resolve(file).replace(/\\\\/g, \"/\");\n\n return process.platform === \"win32\" ? normalized.toLowerCase() : normalized;\n}\n\n/**\n * Every real page file currently represented in the route table. This includes\n * a custom `404.page.tsx`, which is intentionally absent from the public page\n * table because it has no linkable URL of its own.\n */\nexport function registeredPageFiles(\n routes: ReturnType<Router[\"list\"]>,\n appSrcRoot: string,\n): string[] {\n const sourceRoot = path.dirname(appSrcRoot);\n\n return [\n ...new Set(\n routes\n .filter((route) => route.isPage && route.sourceFile && !route.sourceFile.startsWith(\"\\0\"))\n .map((route) => path.resolve(sourceRoot, route.sourceFile)),\n ),\n ];\n}\n\n/** Exact route-source owners replaced by a live page-table transaction. */\nexport function pageRouteSourceFiles(routes: ReturnType<Router[\"list\"]>): string[] {\n return [\n ...new Set(\n routes.filter((route) => route.isPage && route.sourceFile).map((route) => route.sourceFile),\n ),\n ];\n}\n\n/**\n * Inspect only in-place page edits. Vite's SSR graph is invalidated explicitly\n * before evaluation because core's file watcher and Vite's watcher have no\n * ordering contract. Component-only edits compare equal and stay in Fast\n * Refresh; route/name changes request an atomic table replacement.\n */\nexport async function pageRoutesNeedReplacement(\n changes: PageFileChanges,\n options: {\n vite: ViteDevServer;\n appSrcRoot: string;\n installedPages: readonly InstalledPageRoute[];\n },\n): Promise<boolean> {\n let replace = changes.added.length > 0 || changes.removed.length > 0;\n\n const installedByFile = new Map(\n options.installedPages.map((page) => [pathKey(page.file), page] as const),\n );\n\n for (const file of changes.inspectionNeeded) {\n // An error boundary has no installed router route to compare against. Its\n // namespace is projected into every client composition, so any edit must\n // replace the generated table; do not load it here just to inspect it.\n if (isErrorPageFilePath(file)) return true;\n\n options.vite.environments.ssr.moduleGraph.onFileChange(file);\n const pageModule = (await options.vite.ssrLoadModule(file)) as PageModuleShape;\n\n // A valid 404 page has no route identity. Its component body remains Vite\n // HMR territory; adding an illegal route export must still enter the\n // transaction so the installer can reject it without touching live routes.\n if (isNotFoundPageFile(file)) {\n if (pageModule.route !== undefined) replace = true;\n continue;\n }\n\n const installed = installedByFile.get(pathKey(file));\n if (installed === undefined) {\n replace = true;\n continue;\n }\n\n const next = resolvePageRouteIdentity(\n pageModule.route,\n filesystemPageFileFor(file, options.appSrcRoot),\n file,\n );\n if (next.path !== installed.declaredPath || next.name !== installed.name) {\n replace = true;\n }\n }\n\n return replace;\n}\n"],"mappings":";;;;;;;AAaA,SAAS,QAAQ,MAAsB;CACrC,MAAM,aAAa,KAAK,QAAQ,IAAI,CAAC,CAAC,QAAQ,OAAO,GAAG;CAExD,OAAO,QAAQ,aAAa,UAAU,WAAW,YAAY,IAAI;AACnE;;;;;;AAOA,SAAgB,oBACd,QACA,YACU;CACV,MAAM,aAAa,KAAK,QAAQ,UAAU;CAE1C,OAAO,CACL,GAAG,IAAI,IACL,OACG,QAAQ,UAAU,MAAM,UAAU,MAAM,cAAc,CAAC,MAAM,WAAW,WAAW,IAAI,CAAC,CAAC,CACzF,KAAK,UAAU,KAAK,QAAQ,YAAY,MAAM,UAAU,CAAC,CAC9D,CACF;AACF;;AAGA,SAAgB,qBAAqB,QAA8C;CACjF,OAAO,CACL,GAAG,IAAI,IACL,OAAO,QAAQ,UAAU,MAAM,UAAU,MAAM,UAAU,CAAC,CAAC,KAAK,UAAU,MAAM,UAAU,CAC5F,CACF;AACF;;;;;;;AAQA,eAAsB,0BACpB,SACA,SAKkB;CAClB,IAAI,UAAU,QAAQ,MAAM,SAAS,KAAK,QAAQ,QAAQ,SAAS;CAEnE,MAAM,kBAAkB,IAAI,IAC1B,QAAQ,eAAe,KAAK,SAAS,CAAC,QAAQ,KAAK,IAAI,GAAG,IAAI,CAAU,CAC1E;CAEA,KAAK,MAAM,QAAQ,QAAQ,kBAAkB;EAI3C,IAAI,oBAAoB,IAAI,GAAG,OAAO;EAEtC,QAAQ,KAAK,aAAa,IAAI,YAAY,aAAa,IAAI;EAC3D,MAAM,aAAc,MAAM,QAAQ,KAAK,cAAc,IAAI;EAKzD,IAAI,mBAAmB,IAAI,GAAG;GAC5B,IAAI,WAAW,UAAU,QAAW,UAAU;GAC9C;EACF;EAEA,MAAM,YAAY,gBAAgB,IAAI,QAAQ,IAAI,CAAC;EACnD,IAAI,cAAc,QAAW;GAC3B,UAAU;GACV;EACF;EAEA,MAAM,OAAO,yBACX,WAAW,OACX,sBAAsB,MAAM,QAAQ,UAAU,GAC9C,IACF;EACA,IAAI,KAAK,SAAS,UAAU,gBAAgB,KAAK,SAAS,UAAU,MAClE,UAAU;CAEd;CAEA,OAAO;AACT"}
@@ -1,3 +1,4 @@
1
+ import { collectPublicFilesSync } from "../build/public-files.mjs";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
4
 
@@ -34,9 +35,32 @@ function assertRelativePublicFile(publicFile) {
34
35
  if (publicFile.length === 0 || publicFile.startsWith("/") || publicFile.includes("\\") || segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) throw new InvalidProductionPublicFileError(publicFile);
35
36
  return segments;
36
37
  }
38
+ /**
39
+ * Report the `public/` files present on disk that the build manifest did not
40
+ * record — i.e. files added after the last `warlock build`. They are not
41
+ * registered (the manifest, not the disk, is what production serves — see
42
+ * the module-level note on {@link PUBLIC_FILE_CACHE_MAX_AGE_SECONDS}'s
43
+ * neighbours), so they will 404 until the app is rebuilt. A missing
44
+ * `publicRoot` is the ordinary no-public-directory case, not staleness, and
45
+ * {@link collectPublicFilesSync} already reports it as `[]`.
46
+ */
47
+ function warnIfPublicBuildIsStale(publicRoot, publicFiles, warn) {
48
+ let onDisk;
49
+ try {
50
+ onDisk = collectPublicFilesSync(publicRoot);
51
+ } catch {
52
+ return;
53
+ }
54
+ const recorded = new Set(publicFiles);
55
+ const staleFiles = onDisk.filter((file) => !recorded.has(file));
56
+ if (staleFiles.length === 0) return;
57
+ warn(`[warlock:web] public/ is stale: the last \`warlock build\` did not see these files, so they will 404 in production until the app is rebuilt:
58
+ ${staleFiles.map((file) => ` - ${file}`).join("\n")}\nRun \`warlock build\` again to include them.`);
59
+ }
37
60
  /** Register the exact app-public files recorded by the successful build. */
38
- function registerProductionPublicFiles(router, clientDir, publicFiles) {
61
+ function registerProductionPublicFiles(router, clientDir, publicFiles, warn = console.warn) {
39
62
  const publicRoot = path.join(clientDir, "public");
63
+ warnIfPublicBuildIsStale(publicRoot, publicFiles, warn);
40
64
  for (const publicFile of publicFiles) {
41
65
  const absoluteFile = path.join(publicRoot, ...assertRelativePublicFile(publicFile));
42
66
  let stat;
@@ -1 +1 @@
1
- {"version":3,"file":"register-production-public-files.mjs","names":[],"sources":["../../../../../../../web/src/server/register-production-public-files.ts"],"sourcesContent":["import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport type { Router } from \"@warlock.js/core\";\r\n\r\nexport class InvalidProductionPublicFileError extends Error {\r\n public constructor(publicFile: string) {\r\n super(\r\n `Cannot register production public file ${JSON.stringify(publicFile)}: ` +\r\n \"the build manifest must contain a non-empty relative POSIX path with no \" +\r\n \"traversal segments.\",\r\n );\r\n this.name = \"InvalidProductionPublicFileError\";\r\n }\r\n}\r\n\r\nexport class MissingProductionPublicFileError extends Error {\r\n public constructor(publicFile: string, absoluteFile: string) {\r\n super(\r\n `Cannot register production public file ${JSON.stringify(publicFile)}: ` +\r\n \"the successful build manifest recorded it, but \" +\r\n `${JSON.stringify(absoluteFile)} is missing or not a file. ` +\r\n \"Run `warlock build` again to replace the incomplete artifact.\",\r\n );\r\n this.name = \"MissingProductionPublicFileError\";\r\n }\r\n}\r\n\r\n/**\r\n * Seconds, not milliseconds — `Router.file`'s `cacheTime` is the response's\r\n * own `Cache-Control: max-age` value (`core/src/http/response.ts`), unlike\r\n * `@fastify/static`'s millisecond `maxAge`.\r\n *\r\n * These files are copied verbatim from `app/public` at build time: the URL is\r\n * the developer's chosen filename, not a content hash, so a rebuild can change\r\n * a file's bytes without changing its URL. `immutable` would tell the browser\r\n * to skip revalidation forever, which is wrong here — this is a plain\r\n * `max-age`, so a stale copy is served for at most this long and then\r\n * revalidated. Five minutes bounds staleness after a deploy to something a\r\n * developer would not notice, without paying a revalidation round trip on\r\n * every load the way `max-age=0` does.\r\n */\r\nconst PUBLIC_FILE_CACHE_MAX_AGE_SECONDS = 300;\r\n\r\nfunction assertRelativePublicFile(publicFile: string): string[] {\r\n const segments = publicFile.split(\"/\");\r\n\r\n if (\r\n publicFile.length === 0 ||\r\n publicFile.startsWith(\"/\") ||\r\n publicFile.includes(\"\\\\\") ||\r\n segments.some((segment) => segment.length === 0 || segment === \".\" || segment === \"..\")\r\n ) {\r\n throw new InvalidProductionPublicFileError(publicFile);\r\n }\r\n\r\n return segments;\r\n}\r\n\r\n/** Register the exact app-public files recorded by the successful build. */\r\nexport function registerProductionPublicFiles(\r\n router: Router,\r\n clientDir: string,\r\n publicFiles: readonly string[],\r\n): void {\r\n const publicRoot = path.join(clientDir, \"public\");\r\n\r\n for (const publicFile of publicFiles) {\r\n const absoluteFile = path.join(publicRoot, ...assertRelativePublicFile(publicFile));\r\n\r\n let stat: fs.Stats;\r\n\r\n try {\r\n stat = fs.statSync(absoluteFile);\r\n } catch {\r\n throw new MissingProductionPublicFileError(publicFile, absoluteFile);\r\n }\r\n\r\n if (!stat.isFile()) {\r\n throw new MissingProductionPublicFileError(publicFile, absoluteFile);\r\n }\r\n\r\n router.file(`/${publicFile}`, absoluteFile, PUBLIC_FILE_CACHE_MAX_AGE_SECONDS);\r\n }\r\n}\r\n"],"mappings":";;;;AAIA,IAAa,mCAAb,cAAsD,MAAM;CAC1D,AAAO,YAAY,YAAoB;EACrC,MACE,0CAA0C,KAAK,UAAU,UAAU,EAAE,8FAGvE;EACA,KAAK,OAAO;CACd;AACF;AAEA,IAAa,mCAAb,cAAsD,MAAM;CAC1D,AAAO,YAAY,YAAoB,cAAsB;EAC3D,MACE,0CAA0C,KAAK,UAAU,UAAU,EAAE,mDAEhE,KAAK,UAAU,YAAY,EAAE,2FAEpC;EACA,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;;;AAgBA,MAAM,oCAAoC;AAE1C,SAAS,yBAAyB,YAA8B;CAC9D,MAAM,WAAW,WAAW,MAAM,GAAG;CAErC,IACE,WAAW,WAAW,KACtB,WAAW,WAAW,GAAG,KACzB,WAAW,SAAS,IAAI,KACxB,SAAS,MAAM,YAAY,QAAQ,WAAW,KAAK,YAAY,OAAO,YAAY,IAAI,GAEtF,MAAM,IAAI,iCAAiC,UAAU;CAGvD,OAAO;AACT;;AAGA,SAAgB,8BACd,QACA,WACA,aACM;CACN,MAAM,aAAa,KAAK,KAAK,WAAW,QAAQ;CAEhD,KAAK,MAAM,cAAc,aAAa;EACpC,MAAM,eAAe,KAAK,KAAK,YAAY,GAAG,yBAAyB,UAAU,CAAC;EAElF,IAAI;EAEJ,IAAI;GACF,OAAO,GAAG,SAAS,YAAY;EACjC,QAAQ;GACN,MAAM,IAAI,iCAAiC,YAAY,YAAY;EACrE;EAEA,IAAI,CAAC,KAAK,OAAO,GACf,MAAM,IAAI,iCAAiC,YAAY,YAAY;EAGrE,OAAO,KAAK,IAAI,cAAc,cAAc,iCAAiC;CAC/E;AACF"}
1
+ {"version":3,"file":"register-production-public-files.mjs","names":[],"sources":["../../../../../../../web/src/server/register-production-public-files.ts"],"sourcesContent":["import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport type { Router } from \"@warlock.js/core\";\r\nimport { collectPublicFilesSync } from \"../build/public-files\";\r\n\r\nexport class InvalidProductionPublicFileError extends Error {\r\n public constructor(publicFile: string) {\r\n super(\r\n `Cannot register production public file ${JSON.stringify(publicFile)}: ` +\r\n \"the build manifest must contain a non-empty relative POSIX path with no \" +\r\n \"traversal segments.\",\r\n );\r\n this.name = \"InvalidProductionPublicFileError\";\r\n }\r\n}\r\n\r\nexport class MissingProductionPublicFileError extends Error {\r\n public constructor(publicFile: string, absoluteFile: string) {\r\n super(\r\n `Cannot register production public file ${JSON.stringify(publicFile)}: ` +\r\n \"the successful build manifest recorded it, but \" +\r\n `${JSON.stringify(absoluteFile)} is missing or not a file. ` +\r\n \"Run `warlock build` again to replace the incomplete artifact.\",\r\n );\r\n this.name = \"MissingProductionPublicFileError\";\r\n }\r\n}\r\n\r\n/**\r\n * Seconds, not milliseconds — `Router.file`'s `cacheTime` is the response's\r\n * own `Cache-Control: max-age` value (`core/src/http/response.ts`), unlike\r\n * `@fastify/static`'s millisecond `maxAge`.\r\n *\r\n * These files are copied verbatim from `app/public` at build time: the URL is\r\n * the developer's chosen filename, not a content hash, so a rebuild can change\r\n * a file's bytes without changing its URL. `immutable` would tell the browser\r\n * to skip revalidation forever, which is wrong here — this is a plain\r\n * `max-age`, so a stale copy is served for at most this long and then\r\n * revalidated. Five minutes bounds staleness after a deploy to something a\r\n * developer would not notice, without paying a revalidation round trip on\r\n * every load the way `max-age=0` does.\r\n */\r\nconst PUBLIC_FILE_CACHE_MAX_AGE_SECONDS = 300;\r\n\r\nfunction assertRelativePublicFile(publicFile: string): string[] {\r\n const segments = publicFile.split(\"/\");\r\n\r\n if (\r\n publicFile.length === 0 ||\r\n publicFile.startsWith(\"/\") ||\r\n publicFile.includes(\"\\\\\") ||\r\n segments.some((segment) => segment.length === 0 || segment === \".\" || segment === \"..\")\r\n ) {\r\n throw new InvalidProductionPublicFileError(publicFile);\r\n }\r\n\r\n return segments;\r\n}\r\n\r\n/**\r\n * Report the `public/` files present on disk that the build manifest did not\r\n * record — i.e. files added after the last `warlock build`. They are not\r\n * registered (the manifest, not the disk, is what production serves — see\r\n * the module-level note on {@link PUBLIC_FILE_CACHE_MAX_AGE_SECONDS}'s\r\n * neighbours), so they will 404 until the app is rebuilt. A missing\r\n * `publicRoot` is the ordinary no-public-directory case, not staleness, and\r\n * {@link collectPublicFilesSync} already reports it as `[]`.\r\n */\r\nfunction warnIfPublicBuildIsStale(\r\n publicRoot: string,\r\n publicFiles: readonly string[],\r\n warn: (message: string) => void,\r\n): void {\r\n let onDisk: string[];\r\n\r\n try {\r\n onDisk = collectPublicFilesSync(publicRoot);\r\n } catch {\r\n return;\r\n }\r\n\r\n const recorded = new Set(publicFiles);\r\n const staleFiles = onDisk.filter((file) => !recorded.has(file));\r\n\r\n if (staleFiles.length === 0) return;\r\n\r\n const named = staleFiles.map((file) => ` - ${file}`).join(\"\\n\");\r\n\r\n warn(\r\n \"[warlock:web] public/ is stale: the last `warlock build` did not see these files, \" +\r\n \"so they will 404 in production until the app is rebuilt:\\n\" +\r\n `${named}\\n` +\r\n \"Run `warlock build` again to include them.\",\r\n );\r\n}\r\n\r\n/** Register the exact app-public files recorded by the successful build. */\r\nexport function registerProductionPublicFiles(\r\n router: Router,\r\n clientDir: string,\r\n publicFiles: readonly string[],\r\n warn: (message: string) => void = console.warn,\r\n): void {\r\n const publicRoot = path.join(clientDir, \"public\");\r\n\r\n warnIfPublicBuildIsStale(publicRoot, publicFiles, warn);\r\n\r\n for (const publicFile of publicFiles) {\r\n const absoluteFile = path.join(publicRoot, ...assertRelativePublicFile(publicFile));\r\n\r\n let stat: fs.Stats;\r\n\r\n try {\r\n stat = fs.statSync(absoluteFile);\r\n } catch {\r\n throw new MissingProductionPublicFileError(publicFile, absoluteFile);\r\n }\r\n\r\n if (!stat.isFile()) {\r\n throw new MissingProductionPublicFileError(publicFile, absoluteFile);\r\n }\r\n\r\n router.file(`/${publicFile}`, absoluteFile, PUBLIC_FILE_CACHE_MAX_AGE_SECONDS);\r\n }\r\n}\r\n"],"mappings":";;;;;AAKA,IAAa,mCAAb,cAAsD,MAAM;CAC1D,AAAO,YAAY,YAAoB;EACrC,MACE,0CAA0C,KAAK,UAAU,UAAU,EAAE,8FAGvE;EACA,KAAK,OAAO;CACd;AACF;AAEA,IAAa,mCAAb,cAAsD,MAAM;CAC1D,AAAO,YAAY,YAAoB,cAAsB;EAC3D,MACE,0CAA0C,KAAK,UAAU,UAAU,EAAE,mDAEhE,KAAK,UAAU,YAAY,EAAE,2FAEpC;EACA,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;;;AAgBA,MAAM,oCAAoC;AAE1C,SAAS,yBAAyB,YAA8B;CAC9D,MAAM,WAAW,WAAW,MAAM,GAAG;CAErC,IACE,WAAW,WAAW,KACtB,WAAW,WAAW,GAAG,KACzB,WAAW,SAAS,IAAI,KACxB,SAAS,MAAM,YAAY,QAAQ,WAAW,KAAK,YAAY,OAAO,YAAY,IAAI,GAEtF,MAAM,IAAI,iCAAiC,UAAU;CAGvD,OAAO;AACT;;;;;;;;;;AAWA,SAAS,yBACP,YACA,aACA,MACM;CACN,IAAI;CAEJ,IAAI;EACF,SAAS,uBAAuB,UAAU;CAC5C,QAAQ;EACN;CACF;CAEA,MAAM,WAAW,IAAI,IAAI,WAAW;CACpC,MAAM,aAAa,OAAO,QAAQ,SAAS,CAAC,SAAS,IAAI,IAAI,CAAC;CAE9D,IAAI,WAAW,WAAW,GAAG;CAI7B,KACE;EAHY,WAAW,KAAK,SAAS,OAAO,MAAM,CAAC,CAAC,KAAK,IAKhD,EAAE,+CAEb;AACF;;AAGA,SAAgB,8BACd,QACA,WACA,aACA,OAAkC,QAAQ,MACpC;CACN,MAAM,aAAa,KAAK,KAAK,WAAW,QAAQ;CAEhD,yBAAyB,YAAY,aAAa,IAAI;CAEtD,KAAK,MAAM,cAAc,aAAa;EACpC,MAAM,eAAe,KAAK,KAAK,YAAY,GAAG,yBAAyB,UAAU,CAAC;EAElF,IAAI;EAEJ,IAAI;GACF,OAAO,GAAG,SAAS,YAAY;EACjC,QAAQ;GACN,MAAM,IAAI,iCAAiC,YAAY,YAAY;EACrE;EAEA,IAAI,CAAC,KAAK,OAAO,GACf,MAAM,IAAI,iCAAiC,YAAY,YAAY;EAGrE,OAAO,KAAK,IAAI,cAAc,cAAc,iCAAiC;CAC/E;AACF"}
@@ -5,45 +5,11 @@ import { ErrorPageModuleLoader } from "./error-page.mjs";
5
5
  import { Request, Response } from "@warlock.js/core";
6
6
 
7
7
  //#region ../web/src/server/render-page.d.ts
8
- /**
9
- * Pipeline stages 9–10: RENDER the page tree from the
10
- * data bundle stages 1–8 produced, then return finalized { html, status,
11
- * headers }. Stage 10 happens at the CALL SITE in two halves —
12
- * 10a the caller applies status + headers (the single live-response write,
13
- * after render, before anything flushes), 10b it flushes
14
- * the document. Nothing in this module writes the live response. It never
15
- * re-runs any earlier stage — `renderPage` calls `executePageRequest` and
16
- * everything here consumes its bundle as-is.
17
- *
18
- * `renderPage` is deliberately double-duty (dx-differentiators.md §3): it is
19
- * the production orchestrator AND the test helper. Because a loader IS a
20
- * controller, `renderPage("products.details", { params: { id: "42" } })`
21
- * returns `{ html, status, headers, data }` in one call — asserting a page's
22
- * data and its response headers is a unit test, no browser, no server boot.
23
- */
24
- type PageRoutesRegistry = {
25
- routes: readonly PageRouteEntry[]; /** Same contract as ExecutePageRequestOptions["createHttp"]. */
26
- createHttp: ExecutePageRequestOptions["createHttp"];
27
- };
28
- /**
29
- * Boot-time wiring so `renderPage(name, options)` can resolve a route NAME
30
- * without each call site carrying the manifest. Returns the previous registry
31
- * so tests can restore it. A per-call `routes`/`createHttp` override wins.
32
- */
33
- declare function connectPageRoutes(registry: PageRoutesRegistry | undefined): PageRoutesRegistry | undefined;
34
- type RenderPageOptions = {
35
- params?: Record<string, string>;
36
- query?: Record<string, string>; /** Per-call overrides of the connected registry (tests, mostly). */
37
- routes?: readonly PageRouteEntry[];
38
- createHttp?: ExecutePageRequestOptions["createHttp"]; /** Loaded only after the ordinary boundary chain has been exhausted. */
8
+ type RenderPageRequestOptions = {
9
+ routes: readonly PageRouteEntry[];
10
+ createHttp: ExecutePageRequestOptions["createHttp"]; /** Loaded only after the ordinary boundary chain has been exhausted. */
39
11
  loadErrorPage?: ErrorPageModuleLoader;
40
12
  };
41
- /**
42
- * `renderPageRequest` takes the URL itself, so `params`/`query` (the
43
- * name-based sugar buildUrl consumes) have no meaning here — everything else
44
- * is the same seam.
45
- */
46
- type RenderPageRequestOptions = Omit<RenderPageOptions, "params" | "query">;
47
13
  type RenderedPage = {
48
14
  /** The full document ("" when the pipeline short-circuited before render). */html: string;
49
15
  status: number; /** Committed response headers, lowercased key → value. */
@@ -58,22 +24,19 @@ type RenderedPage = {
58
24
  * The full stages-1–8 bundle, for assertions beyond the page's own data.
59
25
  * Undefined ONLY on `renderPageRequest`'s no-match path: no route matched,
60
26
  * so no pipeline ran and there is no bundle — the 404 answer stands alone.
61
- * `renderPage` always carries one (its no-match throws instead).
62
27
  */
63
28
  bundle: PageDataBundle | undefined;
64
29
  };
65
- declare function renderPage(routeName: string, options?: RenderPageOptions): Promise<RenderedPage | Response>;
66
30
  /**
67
- * The URL-based sibling of `renderPage` the production render surface: a
68
- * real HTTP server has a URL, not a route name. The url goes STRAIGHT to
69
- * executePageRequest's stage-1 matcher (no buildUrl), then the same shared
70
- * tail renders and emits.
31
+ * The production render surface: a real HTTP server has a URL. The url goes
32
+ * STRAIGHT to executePageRequest's stage-1 matcher, then the shared tail
33
+ * (`finishRender`) renders and emits.
71
34
  *
72
- * No-match here is NOT the manifest bug renderPage throws on: an arbitrary
73
- * URL matching no route is a legitimate 404, and a server must ANSWER it —
74
- * `{ html: "", status: 404 }` with an undefined `bundle` (see RenderedPage).
35
+ * No-match is not thrown on: an arbitrary URL matching no route is a
36
+ * legitimate 404, and a server must ANSWER it — `{ html: "", status: 404 }`
37
+ * with an undefined `bundle` (see RenderedPage).
75
38
  */
76
- declare function renderPageRequest(url: string, options?: RenderPageRequestOptions): Promise<RenderedPage | Response>;
39
+ declare function renderPageRequest(url: string, options: RenderPageRequestOptions): Promise<RenderedPage | Response>;
77
40
  //#endregion
78
- export { PageRoutesRegistry, RenderPageOptions, RenderPageRequestOptions, RenderedPage, connectPageRoutes, renderPage, renderPageRequest };
41
+ export { RenderPageRequestOptions, RenderedPage, renderPageRequest };
79
42
  //# sourceMappingURL=render-page.d.mts.map
@@ -1,7 +1,7 @@
1
1
  import { LocaleProvider } from "../localization.mjs";
2
2
  import { DocumentContext, PAYLOAD_SCRIPT_ID, escapePayload } from "../components/document-context.mjs";
3
3
  import { markNonHydrating } from "./page-render-bundle.mjs";
4
- import { registerModules } from "../runtime/register-modules.mjs";
4
+ import { registerModules } from "../register-modules.mjs";
5
5
  import DefaultApp from "../components/default-app.mjs";
6
6
  import { buildHydrationPayload } from "./build-hydration-payload.mjs";
7
7
  import { ERROR_PAGE_METADATA } from "./resolve-page-metadata.mjs";
@@ -22,37 +22,14 @@ function committedHeaders(bundle) {
22
22
  function committedCookies(bundle) {
23
23
  return bundle.commit?.cookies ?? [];
24
24
  }
25
- let pageRoutesRegistry;
26
- /**
27
- * Boot-time wiring so `renderPage(name, options)` can resolve a route NAME
28
- * without each call site carrying the manifest. Returns the previous registry
29
- * so tests can restore it. A per-call `routes`/`createHttp` override wins.
30
- */
31
- function connectPageRoutes(registry) {
32
- const previous = pageRoutesRegistry;
33
- pageRoutesRegistry = registry;
34
- return previous;
35
- }
36
25
  function requireRegistry(options) {
37
- const routes = options.routes ?? pageRoutesRegistry?.routes;
38
- const createHttp = options.createHttp ?? pageRoutesRegistry?.createHttp;
39
- if (!routes || !createHttp) throw new Error("renderPage()/renderPageRequest() has no route registry connected (web/src/server/render-page.ts). Both resolve against the page manifest, which the server bootstrap owns. Fix: call connectPageRoutes({ routes, createHttp }) at boot (tests: in beforeAll), or pass { routes, createHttp } to this call.");
26
+ const { routes, createHttp } = options;
27
+ if (!routes || !createHttp) throw new Error("renderPageRequest() has no route registry (web/src/server/render-page.ts). It resolves against the page manifest, which the server bootstrap owns. Fix: pass { routes, createHttp } to this call.");
40
28
  return {
41
29
  routes,
42
30
  createHttp
43
31
  };
44
32
  }
45
- function buildUrl(entry, params, query) {
46
- const path = entry.path.split("/").map((segment) => {
47
- if (!segment.startsWith(":")) return segment;
48
- const name = segment.slice(1);
49
- const value = params[name];
50
- if (value === void 0) throw new Error(`renderPage("${entry.name}"): route path "${entry.path}" needs param "${name}" and the call did not provide it (web/src/server/render-page.ts). Fix: pass it in \`params: { ${name}: … }\`.`);
51
- return encodeURIComponent(value);
52
- }).join("/");
53
- const queryString = new URLSearchParams(query).toString();
54
- return queryString ? `${path}?${queryString}` : path;
55
- }
56
33
  /**
57
34
  * The framework-owned terminal boundary (P1 §4: designation falls back to
58
35
  * `app` even when no level exports one — "the framework owns a root
@@ -205,7 +182,7 @@ async function finishRender(triple, bundle, documentSlots, response, loadErrorPa
205
182
  if (!loadErrorPage) return void 0;
206
183
  const props = {
207
184
  error: thrown,
208
- status: 500
185
+ status: currentError?.statusCode ?? 500
209
186
  };
210
187
  const module = await loadErrorPage();
211
188
  registerModules([module]);
@@ -240,7 +217,7 @@ async function finishRender(triple, bundle, documentSlots, response, loadErrorPa
240
217
  }
241
218
  currentError = buildErrorRecord(thrown, designateBoundary(currentError?.boundary.boundaryLevel === "layout" ? "app" : currentError ? "layout" : "page", triple));
242
219
  }
243
- const status = currentError ? 500 : bundle.commit?.statusCode ?? 200;
220
+ const status = currentError ? currentError.statusCode ?? 500 : bundle.commit?.statusCode ?? 200;
244
221
  return {
245
222
  html: emitDocument(body),
246
223
  status,
@@ -329,35 +306,16 @@ async function renderPageFailure(options) {
329
306
  bundle
330
307
  };
331
308
  }
332
- async function renderPage(routeName, options = {}) {
333
- const registry = requireRegistry(options);
334
- const entry = registry.routes.find((candidate) => candidate.name === routeName);
335
- if (!entry) {
336
- const known = registry.routes.map((candidate) => `"${candidate.name}"`).join(", ");
337
- throw new Error(`renderPage("${routeName}"): no route with that name (web/src/server/render-page.ts). Known route names: ${known}. Fix: use a name from the manifest, or connect the manifest that declares this one.`);
338
- }
339
- const url = buildUrl(entry, options.params ?? {}, options.query ?? {});
340
- const { state, createHttp } = capturingCreateHttp(registry);
341
- const rendered = await executePageRequest({
342
- url,
343
- routes: registry.routes,
344
- createHttp,
345
- finish: (bundle) => finishRender(entry.triple, bundle, documentSlotsFrom(state.captured), state.captured.response, options.loadErrorPage)
346
- });
347
- if (!rendered) throw new Error(`renderPage("${routeName}"): the built URL "${url}" did not match stage 1 (web/src/server/render-page.ts). The name resolved but the matcher disagreed — that is a manifest bug, not a caller bug.`);
348
- return rendered;
349
- }
350
309
  /**
351
- * The URL-based sibling of `renderPage` the production render surface: a
352
- * real HTTP server has a URL, not a route name. The url goes STRAIGHT to
353
- * executePageRequest's stage-1 matcher (no buildUrl), then the same shared
354
- * tail renders and emits.
310
+ * The production render surface: a real HTTP server has a URL. The url goes
311
+ * STRAIGHT to executePageRequest's stage-1 matcher, then the shared tail
312
+ * (`finishRender`) renders and emits.
355
313
  *
356
- * No-match here is NOT the manifest bug renderPage throws on: an arbitrary
357
- * URL matching no route is a legitimate 404, and a server must ANSWER it —
358
- * `{ html: "", status: 404 }` with an undefined `bundle` (see RenderedPage).
314
+ * No-match is not thrown on: an arbitrary URL matching no route is a
315
+ * legitimate 404, and a server must ANSWER it — `{ html: "", status: 404 }`
316
+ * with an undefined `bundle` (see RenderedPage).
359
317
  */
360
- async function renderPageRequest(url, options = {}) {
318
+ async function renderPageRequest(url, options) {
361
319
  const registry = requireRegistry(options);
362
320
  const { state, createHttp } = capturingCreateHttp(registry);
363
321
  const rendered = await executePageRequest({
@@ -378,5 +336,5 @@ async function renderPageRequest(url, options = {}) {
378
336
  }
379
337
 
380
338
  //#endregion
381
- export { connectPageRoutes, renderPage, renderPageFailure, renderPageRequest };
339
+ export { renderPageFailure, renderPageRequest };
382
340
  //# sourceMappingURL=render-page.mjs.map