@warlock.js/web 5.2.2 → 5.2.4

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 (47) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/esm/build/contribution.mjs.map +1 -1
  3. package/esm/build/discover-pages.mjs.map +1 -1
  4. package/esm/build/generate-client-registry.mjs.map +1 -1
  5. package/esm/build/generate-pages-barrel.mjs.map +1 -1
  6. package/esm/build/page-default-export.mjs.map +1 -1
  7. package/esm/build/page-routes-manifest.mjs.map +1 -1
  8. package/esm/build/public-files.mjs.map +1 -1
  9. package/esm/build/read-route-exports.mjs.map +1 -1
  10. package/esm/client/build-hydrated-tree.mjs.map +1 -1
  11. package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
  12. package/esm/client/navigation/prefetch.mjs.map +1 -1
  13. package/esm/client/runtime/manifest.mjs.map +1 -1
  14. package/esm/client/runtime/matcher.mjs.map +1 -1
  15. package/esm/components/document-context.mjs.map +1 -1
  16. package/esm/components/link.mjs.map +1 -1
  17. package/esm/routing/filesystem-route.mjs.map +1 -1
  18. package/esm/routing/layout-policy.mjs.map +1 -1
  19. package/esm/routing/query-string.mjs.map +1 -1
  20. package/esm/routing/route-table.mjs.map +1 -1
  21. package/esm/server/create-page-route-handler.mjs.map +1 -1
  22. package/esm/server/execute-page-request.mjs.map +1 -1
  23. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  24. package/esm/server/install-page-routes.mjs.map +1 -1
  25. package/esm/server/match-page-route.mjs.map +1 -1
  26. package/esm/server/not-found-page.mjs.map +1 -1
  27. package/esm/server/page-file-change.mjs.map +1 -1
  28. package/esm/server/page-route-reload.mjs.map +1 -1
  29. package/esm/server/render-page.mjs.map +1 -1
  30. package/esm/server/settle-page-response.mjs.map +1 -1
  31. package/esm/server/stylesheet-urls.mjs.map +1 -1
  32. package/esm/server/unregistered-pages.mjs.map +1 -1
  33. package/esm/server/web-connector-factory.mjs.map +1 -1
  34. package/esm/server/web-connector.mjs.map +1 -1
  35. package/esm/shared.mjs.map +1 -1
  36. package/esm/vite/build-client.mjs.map +1 -1
  37. package/esm/vite/gate-a-resolve.mjs.map +1 -1
  38. package/esm/vite/gate-b-secrets.mjs.map +1 -1
  39. package/esm/vite/gate-c-verify.mjs.map +1 -1
  40. package/esm/vite/hydration-entries.mjs.map +1 -1
  41. package/esm/vite/index.mjs.map +1 -1
  42. package/esm/vite/page-registry-plugin.mjs.map +1 -1
  43. package/esm/vite/projection.mjs.map +1 -1
  44. package/llms-full.txt +33 -9
  45. package/llms.txt +1 -1
  46. package/package.json +3 -3
  47. package/skills/add-web-to-an-app/SKILL.md +7 -5
@@ -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 and serves the\r\n * global root (`<appSrcRoot>/web/**`) exactly as it serves a module's\r\n * (`<appSrcRoot>/app/<module>/web/**`). WHAT ROUTE A PAGE ANSWERS ON stays\r\n * 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 {\r\n deriveFilesystemRouteName,\r\n deriveFilesystemRoutePath,\r\n} from \"../routing/filesystem-route\";\r\nimport { NestedLayoutsNotSupportedError, selectPageLayout } from \"../routing/layout-policy\";\r\nimport { canonicalizeRouteExport } 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\";\nimport { isLoaderShortCircuit } from \"./settle-page-response\";\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 };\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: deriveFilesystemRouteName(filesystemPageFile),\r\n };\r\n }\r\n\r\n const route = canonicalizeRouteExport(routeExport);\r\n\r\n return {\r\n declaredPath: route.path,\r\n name: route.name ?? deriveFilesystemRouteName(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;\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>/app/*\\/web/**\" and \"<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 {\r\n router,\r\n vite,\r\n appSrcRoot,\r\n appFile,\r\n hydrationClientModuleUrl,\r\n } = 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(`Two error pages were found: ${errorPageFiles.map((page) => page.pageFile).join(\", \")}.`);\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 = 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 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 = 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 }),\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,\n statusForRenderedOk: 404,\n skipPageLoader: true,\n }),\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":[],"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 and serves the\r\n * global root (`<appSrcRoot>/web/**`) exactly as it serves a module's\r\n * (`<appSrcRoot>/app/<module>/web/**`). WHAT ROUTE A PAGE ANSWERS ON stays\r\n * 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 {\r\n deriveFilesystemRouteName,\r\n deriveFilesystemRoutePath,\r\n} from \"../routing/filesystem-route\";\r\nimport { NestedLayoutsNotSupportedError, selectPageLayout } from \"../routing/layout-policy\";\r\nimport { canonicalizeRouteExport } 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\";\nimport { isLoaderShortCircuit } from \"./settle-page-response\";\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 };\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: deriveFilesystemRouteName(filesystemPageFile),\r\n };\r\n }\r\n\r\n const route = canonicalizeRouteExport(routeExport);\r\n\r\n return {\r\n declaredPath: route.path,\r\n name: route.name ?? deriveFilesystemRouteName(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;\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>/app/*\\/web/**\" and \"<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 {\r\n router,\r\n vite,\r\n appSrcRoot,\r\n appFile,\r\n hydrationClientModuleUrl,\r\n } = 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(`Two error pages were found: ${errorPageFiles.map((page) => page.pageFile).join(\", \")}.`);\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 = 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 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 = 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 }),\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,\n statusForRenderedOk: 404,\n skipPageLoader: true,\n }),\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0FA,MAAa,0CAA0C;;;;;;AAOvD,SAAS,uBAAuB,UAAkB,YAA4B;CAC5E,OAAO,GAAG,KAAK,SAAS,UAAU,EAAE,GAAG,QAAQ,KAAK,SAAS,YAAY,QAAQ,CAAC;AACpF;AAEA,SAAS,sBAAsB,UAAkB,YAA4B;CAC3E,OAAO,QAAQ,KAAK,SAAS,KAAK,KAAK,YAAY,KAAK,GAAG,QAAQ,CAAC;AACtE;;;;;;;AAQA,SAAgB,yBACd,aACA,UACA,YACmD;CACnD,MAAM,qBAAqB,sBAAsB,UAAU,UAAU;CAErE,IAAI,gBAAgB,QAClB,OAAO;EACL,cAAc,0BAA0B,EAAE,UAAU,mBAAmB,CAAC;EACxE,MAAM,0BAA0B,kBAAkB;CACpD;CAGF,MAAM,QAAQ,wBAAwB,WAAW;CAEjD,OAAO;EACL,cAAc,MAAM;EACpB,MAAM,MAAM,QAAQ,0BAA0B,kBAAkB;CAClE;AACF;AA0DA,eAAe,mBACb,UACA,SACA,YACsB;CACtB,MAAM,QAAQ,eAAe,UAAU,OAAO;CAC9C,MAAM,UAAU,MAAM,QAAQ,IAAI,MAAM,IAAI,UAAU,CAAC;CACvD,MAAM,YAAY,iBAChB,MAAM,KAAK,QAAQ,WAAW;EAC5B;EACA,SAAS,OAAO,QAAQ,MAAM,CAAC,YAAY;CAC7C,EAAE,CACJ;CAEA,IAAI,UAAU,SAAS,YACrB,MAAM,IAAI,+BAA+B,UAAU,UAAU,OAAO;CAGtE,OAAO;EACL;EACA,YAAY,UAAU,SAAS,aAAa,UAAU,SAAS,MAAM,GAAG,EAAE;EAC1E,QAAQ,QAAQ,QACb,UAAU,iBAAiB,iBAAiB,UAAU,aAAa,UAAU,GAAG,GACjF,GACF;EACA,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,SAAQ,iBAAgB,CAAC,GAAI,aAAa,cAAc,CAAC,CAAE,CAAC;EAChF,QAAQ,OAAO,YAAY;GACzB,IAAI;GAEJ,KAAK,IAAI,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,SAAS;IACnD,MAAM,QAAQ,MAAM,QAAQ,MAAM,CAAC,SAAS,OAAO;IAEnD,IAAI,iBAAiB,YAAY,qBAAqB,KAAK,GAAG,OAAO;IACrE,IAAI,UAAU,WAAW,WAAW;GACtC;GAEA,OAAO;EACT;CACF;AACF;;;;;;;;;;AAgDA,eAAsB,kBACpB,SAC+B;CAC/B,MAAM,EACJ,QACA,MACA,YACA,SACA,6BACE;CAGJ,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,MAAM,+BAA+B,eAAe,KAAK,SAAS,KAAK,QAAQ,CAAC,CAAC,KAAK,IAAI,EAAE,EAAE;CAG1G,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,gBAAgB,kBAAkB,SACpC,eACM,KAAK,cAAc,aAAa;CAE1C,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,MAAM,aAAc,MAAM,KAAK,cAAc,QAAQ;EAErD,MAAM,aAAa,uBAAuB,UAAU,UAAU;EAG9D,MAAM,EAAE,cAAc,WAAW,SAAS,yBACxC,WAAW,OACX,UACA,UACF;EAEA,MAAM,cAAyB,eAC7B,KAAK,cAAc,UAAU;EAC/B,MAAM,cAAc,MAAM,mBAAmB,UAAU,SAAS,UAAU;EAC1E,MAAM,EAAE,YAAY,QAAQ,iBAAiB;EAE7C,MAAM,gBAAgB,WAAW,UAAU,SACvC,0BAA0B;GACxB,UAAU,sBAAsB,UAAU,UAAU;GACpD,gBAAgB,YAAY;EAC9B,CAAC,IACD,iBAAiB,cAAc,SAAS;EAE5C,MAAM,eAAe,WAAW,IAAI,aAAa;EAEjD,IAAI,cACF,MAAM,IAAI,MACR,2CAA2C,cAAc,oBAC5C,aAAa,kBAAkB,UAAU,yEACA,aAAa,SAC7D,SAAS,oDACjB;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,UAC3C,aACE,aAAa,aACT,mBAAmB;IAAE,GAAG;IAAa;GAAW,GAAG,UAAU,IAC7D,KAAK,cAAc,QAAQ,KACjC,aAAY,KAAK,cAAc,QAAQ;GAK7C,+BAA+B,QAAQ,IAAI,YAAY,MAAM,IAAI,UAAU,CAAC;GAC5E;GACA;GACA;EACF,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;EAClB,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":"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,EAAE,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\";\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 +1 @@
1
- {"version":3,"file":"not-found-page.mjs","names":[],"sources":["../../../../../../../web/src/server/not-found-page.ts"],"sourcesContent":["/**\n * THE NOT-FOUND PATH — the one route in the application that answers for URLs\n * nobody declared.\n *\n * An application gets it by writing `404.page.tsx` anywhere under a web root.\n * The file is named for the status it answers with, not for a concept\n * (\"not-found\"), because `404` is the string people actually search for, and\n * the `*.page.tsx` suffix is what makes it a page in the first place.\n *\n * ── THE RULE THIS FILE EXISTS FOR ────────────────────────────────────────────\n *\n * Pages and API routes share ONE route namespace and ONE router. So a catch-all\n * page route sees every unmatched request in the process, including\n * `GET /api/uzers` — and if it renders a document for that, a `fetch()` gets\n * `<!doctype html>` back and dies inside `response.json()` with a SyntaxError\n * pointing at the parser instead of at the typo. That failure is expensive\n * precisely because the error names nothing near its cause.\n *\n * Because pages and API share one namespace there is no path-prefix rule\n * available: `/anything` may legitimately be either. The discriminator is\n * therefore the `Accept` header, and it is stated as a narrow permission rather\n * than a guess — the document is the exception, JSON is the default:\n *\n * A request renders the not-found PAGE only if\n * 1. its method is GET or HEAD, and\n * 2. `text/html` appears EXPLICITLY in its `Accept` header.\n *\n * (1) is not a heuristic about browsers. Pages are registered with\n * `router.get` and nothing else — every page route in this codebase is\n * installed by `installPageRoutes` / `installPageRoutesFromManifest`, both\n * of which call `router.get`. A `POST` therefore cannot have been meant\n * for a page, by construction. HEAD rides along because Fastify answers it\n * from the GET route.\n *\n * (2) is EXPLICIT and the word is load-bearing. A wildcard does NOT count:\n * `* /*` — what a bare `fetch()` sends — is not a request for a document,\n * it is the absence of a preference, and `text/*` claims a family rather\n * than the type. Only the literal `text/html` media range, with a non-zero\n * `q`, opens the page path. A browser address-bar navigation always sends\n * an explicit `text/html`; a `fetch()` that has not asked for one never\n * does. So the mistyped `/api/...` in a `fetch()` keeps its JSON body and\n * dies at the typo rather than inside `response.json()`.\n *\n * WHAT THE RULE CANNOT DECIDE, and does not pretend to: a browser navigating to\n * a typo'd API URL asks for `text/html`, and so is answered with the document.\n * Nothing in that request distinguishes it from a typo'd page URL — same verb,\n * same header, same absence of a match — so the rule does not guess. The status\n * is 404 either way, which is the part machines read.\n *\n * ── DEPENDENCY NOTE ──────────────────────────────────────────────────────────\n * This module has NO runtime imports. `../build/discover-pages` imports the\n * filename constant and the identity helpers from here so build discovery and\n * both installers cannot disagree about what a not-found page is, and that edge\n * must not drag the render pipeline into the build.\n */\nimport type { HttpContext } from \"@warlock.js/core\";\nimport type { PageRouteHandler } from \"./create-page-route-handler\";\n\n/**\n * The one filename that makes a page THE not-found page.\n *\n * `404.page.tsx`, not `not-found.page.tsx`: it keeps the `*.page.tsx`\n * convention every other page follows, and `404` is the token a developer\n * greps for when a URL answers with one.\n */\nexport const NOT_FOUND_PAGE_FILENAME = \"404.page.tsx\";\n\n/**\n * The path the not-found route is registered on — find-my-way's and Fastify's\n * catch-all, and the same literal core's own dev dispatcher registers\n * (`core/src/router/router.ts`, `server.route({ url: \"*\" })`).\n *\n * A catch-all has the LOWEST matching priority in both routers, so every\n * declared page and every declared API route still wins on its own path; this\n * route is only ever reached because nothing else claimed the URL.\n */\nexport const NOT_FOUND_ROUTE_PATH = \"*\";\n\n/**\n * The reserved route name the not-found page is registered under.\n *\n * Namespaced under `warlock.` because it is the framework's route rather than\n * the application's, and because the router's name namespace is shared with API\n * routes — an application that takes this name gets core's duplicate-name error,\n * which is the loud answer, not a silent overwrite.\n *\n * It is deliberately NOT published into the route table (`href()` / `<Link>`):\n * the not-found page has no URL of its own to link to.\n */\nexport const NOT_FOUND_ROUTE_NAME = \"warlock.not-found\";\n\n/** True when `sourceFile`'s basename is exactly {@link NOT_FOUND_PAGE_FILENAME}. */\nexport function isNotFoundPageFile(sourceFile: string): boolean {\n const separator = Math.max(sourceFile.lastIndexOf(\"/\"), sourceFile.lastIndexOf(\"\\\\\"));\n\n return sourceFile.slice(separator + 1) === NOT_FOUND_PAGE_FILENAME;\n}\n\n/**\n * Raised when more than one `404.page.tsx` exists.\n *\n * There is exactly one not-found route in a process, so a second file is not a\n * per-module override — it is two files claiming one route, with the winner\n * decided by directory-walk order. Both are named because the fix is to delete\n * one and the operator has to know which two are in play.\n */\nexport class DuplicateNotFoundPageError extends Error {\n public constructor(public readonly pageFiles: readonly string[]) {\n super(\n `Two or more not-found pages were found: ${pageFiles.map((file) => `\"${file}\"`).join(\", \")}. ` +\n `An application has exactly one \\`${NOT_FOUND_PAGE_FILENAME}\\` — it answers every ` +\n \"unmatched page URL in the process, so a second one would silently never render. \" +\n \"Keep one and delete the rest.\",\n );\n this.name = \"DuplicateNotFoundPageError\";\n }\n}\n\n/**\n * Raised when `404.page.tsx` declares a `route` export.\n *\n * The not-found page has no URL of its own — it is reached by NOT matching. A\n * `route` export on it reads like a promise that `/404` is browsable, and it is\n * not: the installers register this file on the catch-all and nowhere else. So\n * the export is refused rather than ignored, because a declaration the framework\n * silently drops is worse than one it rejects.\n */\nexport class NotFoundPageDeclaresRouteError extends Error {\n public constructor(public readonly pageFile: string) {\n super(\n `\"${pageFile}\" is the not-found page but declares a \\`route\\` export. ` +\n `\\`${NOT_FOUND_PAGE_FILENAME}\\` has no URL of its own — it answers every page URL that ` +\n \"matched nothing, and is never registered at a path of its own. Remove the `route` \" +\n \"export; to serve a browsable page at a fixed path, use a normal `*.page.tsx`.\",\n );\n this.name = \"NotFoundPageDeclaresRouteError\";\n }\n}\n\n/** The shape this module reads off a registered route — core's `Route`, narrowed. */\nexport type RegisteredRouteShape = {\n path: string;\n isPage?: boolean;\n};\n\n/**\n * The media range the not-found PAGE is gated on. Compared literally: a request\n * either named this exact type or it did not.\n */\nconst HTML_MEDIA_TYPE = \"text/html\";\n\n/**\n * True when `text/html` is named EXPLICITLY in an `Accept` header — the whole\n * discriminator, in one predicate.\n *\n * Wildcards are refused on purpose. `* /*` is what `fetch()` and `curl` send\n * when the caller expressed no preference at all, and `text/*` names a family;\n * neither is a request for a document, and treating either as one is what makes\n * a mistyped `/api/...` answer HTML to a JSON parser.\n *\n * `q=0` is honoured because it is the header's own way of saying \"not this\n * one\" — `Accept: text/html;q=0, application/json` is a client refusing the\n * document, and reading it as a request for one would be reading the header\n * backwards. Any other `q`, present or absent, counts.\n */\nexport function acceptsHtmlExplicitly(accept: string | undefined): boolean {\n if (!accept) return false;\n\n for (const entry of accept.split(\",\")) {\n const [rawType, ...parameters] = entry.split(\";\");\n\n if (rawType.trim().toLowerCase() !== HTML_MEDIA_TYPE) continue;\n\n const quality = parameters\n .map((parameter) => parameter.trim().toLowerCase())\n .find((parameter) => parameter.startsWith(\"q=\"));\n\n // A malformed `q` is not a refusal — only an explicit zero is.\n if (quality !== undefined && Number.parseFloat(quality.slice(2)) === 0) continue;\n\n return true;\n }\n\n return false;\n}\n\nexport type UnmatchedRequestKind = \"page\" | \"api\";\n\n/**\n * The rule, in one function — see this file's header for why it is these two\n * conditions and why the second one refuses wildcards.\n */\nexport function classifyUnmatchedRequest(input: {\n method: string;\n accept: string | undefined;\n}): UnmatchedRequestKind {\n const method = input.method.toUpperCase();\n\n // Pages are installed with `router.get`, so only these two verbs can ever\n // have been asking for one.\n if (method !== \"GET\" && method !== \"HEAD\") return \"api\";\n\n return acceptsHtmlExplicitly(input.accept) ? \"page\" : \"api\";\n}\n\n/**\n * The document served when the application ships no `404.page.tsx`.\n *\n * Deliberately a STRING, not a React render: it must survive the case where the\n * application root, a layout or the page module is exactly what is broken, and\n * a default that can itself fail is not a default. It carries no stylesheet and\n * no hydration script for the same reason — nothing here can 500.\n *\n * It answers 404 like the real page does, because the status is the part that\n * search engines, caches and monitoring read; a framework default that soft-404s\n * would teach every un-customised application to lie.\n */\nexport function frameworkDefaultNotFoundDocument(): string {\n return (\n \"<!doctype html>\" +\n '<html lang=\"en\">' +\n \"<head>\" +\n '<meta charset=\"utf-8\">' +\n '<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">' +\n '<meta name=\"robots\" content=\"noindex\">' +\n \"<title>404 — Page not found</title>\" +\n \"</head>\" +\n \"<body>\" +\n \"<h1>404 — Page not found</h1>\" +\n \"<p>This URL does not match any page.</p>\" +\n `<p>To replace this page, add <code>${NOT_FOUND_PAGE_FILENAME}</code> to a web folder ` +\n \"(for example <code>src/web/404.page.tsx</code>).</p>\" +\n \"</body>\" +\n \"</html>\"\n );\n}\n\nexport type NotFoundRouteHandlerOptions = {\n /**\n * The application's `404.page.tsx`, already built into a page handler by\n * whichever installer owns module loading — `undefined` when the application\n * ships no such file, which is what selects\n * {@link frameworkDefaultNotFoundDocument}.\n *\n * Taking a built handler rather than a module keeps this file out of the\n * render pipeline entirely: dev hands over a Vite-backed handler, production a\n * manifest-backed one, and neither difference is visible here.\n */\n renderPage?: PageRouteHandler;\n};\n\n/**\n * The handler registered on the catch-all.\n *\n * Three answers, in this order, and the order is the safety property: the API\n * check runs BEFORE anything can render, so no request that the rule calls an\n * API request can reach a React render even if the page module is broken.\n */\nexport function createNotFoundRouteHandler(\n options: NotFoundRouteHandlerOptions,\n): PageRouteHandler {\n const { renderPage } = options;\n\n return async (context: HttpContext) => {\n const { request, response } = context;\n // Node lowercases header names and collapses a repeated `Accept` into an\n // array; both forms are read, so a duplicated header cannot silently mean\n // \"no preference\".\n const accept = request.header(\"accept\");\n\n if (\n classifyUnmatchedRequest({\n method: request.method,\n accept: Array.isArray(accept) ? accept.join(\",\") : accept,\n }) === \"api\"\n ) {\n // The same body core's own dev dispatcher writes for an unmatched route\n // (`core/src/router/router.ts`), so an API 404 reads identically whether\n // it fell through to core or was declined here — and identically in\n // development and in production, which it previously was not.\n await response.send(\n { error: \"Route not found\", path: request.path, method: request.method },\n 404,\n );\n\n return;\n }\n\n if (renderPage === undefined) {\n await response.html(frameworkDefaultNotFoundDocument(), 404);\n\n return;\n }\n\n return renderPage(context);\n };\n}\n"],"mappings":";;;;;;;;AAiEA,MAAa,0BAA0B;;;;;;;;;;AAWvC,MAAa,uBAAuB;;;;;;;;;;;;AAapC,MAAa,uBAAuB;;AAGpC,SAAgB,mBAAmB,YAA6B;CAC9D,MAAM,YAAY,KAAK,IAAI,WAAW,YAAY,GAAG,GAAG,WAAW,YAAY,IAAI,CAAC;CAEpF,OAAO,WAAW,MAAM,YAAY,CAAC,MAAM;AAC7C;;;;;;;;;AAUA,IAAa,6BAAb,cAAgD,MAAM;CACjB;CAAnC,AAAO,YAAY,AAAgB,WAA8B;EAC/D,MACE,2CAA2C,UAAU,KAAK,SAAS,IAAI,KAAK,EAAE,EAAE,KAAK,IAAI,EAAE,qCACrD,wBAAwB,oIAGhE;EANiC;EAOjC,KAAK,OAAO;CACd;AACF;;;;;;;;;;AAWA,IAAa,iCAAb,cAAoD,MAAM;CACrB;CAAnC,AAAO,YAAY,AAAgB,UAAkB;EACnD,MACE,IAAI,SAAS,6DACN,wBAAwB,8NAGjC;EANiC;EAOjC,KAAK,OAAO;CACd;AACF;;;;;AAYA,MAAM,kBAAkB;;;;;;;;;;;;;;;AAgBxB,SAAgB,sBAAsB,QAAqC;CACzE,IAAI,CAAC,QAAQ,OAAO;CAEpB,KAAK,MAAM,SAAS,OAAO,MAAM,GAAG,GAAG;EACrC,MAAM,CAAC,SAAS,GAAG,cAAc,MAAM,MAAM,GAAG;EAEhD,IAAI,QAAQ,KAAK,EAAE,YAAY,MAAM,iBAAiB;EAEtD,MAAM,UAAU,WACb,KAAK,cAAc,UAAU,KAAK,EAAE,YAAY,CAAC,EACjD,MAAM,cAAc,UAAU,WAAW,IAAI,CAAC;EAGjD,IAAI,YAAY,UAAa,OAAO,WAAW,QAAQ,MAAM,CAAC,CAAC,MAAM,GAAG;EAExE,OAAO;CACT;CAEA,OAAO;AACT;;;;;AAQA,SAAgB,yBAAyB,OAGhB;CACvB,MAAM,SAAS,MAAM,OAAO,YAAY;CAIxC,IAAI,WAAW,SAAS,WAAW,QAAQ,OAAO;CAElD,OAAO,sBAAsB,MAAM,MAAM,IAAI,SAAS;AACxD;;;;;;;;;;;;;AAcA,SAAgB,mCAA2C;CACzD,OACE,gUAWsC,wBAAwB;AAKlE;;;;;;;;AAuBA,SAAgB,2BACd,SACkB;CAClB,MAAM,EAAE,eAAe;CAEvB,OAAO,OAAO,YAAyB;EACrC,MAAM,EAAE,SAAS,aAAa;EAI9B,MAAM,SAAS,QAAQ,OAAO,QAAQ;EAEtC,IACE,yBAAyB;GACvB,QAAQ,QAAQ;GAChB,QAAQ,MAAM,QAAQ,MAAM,IAAI,OAAO,KAAK,GAAG,IAAI;EACrD,CAAC,MAAM,OACP;GAKA,MAAM,SAAS,KACb;IAAE,OAAO;IAAmB,MAAM,QAAQ;IAAM,QAAQ,QAAQ;GAAO,GACvE,GACF;GAEA;EACF;EAEA,IAAI,eAAe,QAAW;GAC5B,MAAM,SAAS,KAAK,iCAAiC,GAAG,GAAG;GAE3D;EACF;EAEA,OAAO,WAAW,OAAO;CAC3B;AACF"}
1
+ {"version":3,"file":"not-found-page.mjs","names":[],"sources":["../../../../../../../web/src/server/not-found-page.ts"],"sourcesContent":["/**\n * THE NOT-FOUND PATH — the one route in the application that answers for URLs\n * nobody declared.\n *\n * An application gets it by writing `404.page.tsx` anywhere under a web root.\n * The file is named for the status it answers with, not for a concept\n * (\"not-found\"), because `404` is the string people actually search for, and\n * the `*.page.tsx` suffix is what makes it a page in the first place.\n *\n * ── THE RULE THIS FILE EXISTS FOR ────────────────────────────────────────────\n *\n * Pages and API routes share ONE route namespace and ONE router. So a catch-all\n * page route sees every unmatched request in the process, including\n * `GET /api/uzers` — and if it renders a document for that, a `fetch()` gets\n * `<!doctype html>` back and dies inside `response.json()` with a SyntaxError\n * pointing at the parser instead of at the typo. That failure is expensive\n * precisely because the error names nothing near its cause.\n *\n * Because pages and API share one namespace there is no path-prefix rule\n * available: `/anything` may legitimately be either. The discriminator is\n * therefore the `Accept` header, and it is stated as a narrow permission rather\n * than a guess — the document is the exception, JSON is the default:\n *\n * A request renders the not-found PAGE only if\n * 1. its method is GET or HEAD, and\n * 2. `text/html` appears EXPLICITLY in its `Accept` header.\n *\n * (1) is not a heuristic about browsers. Pages are registered with\n * `router.get` and nothing else — every page route in this codebase is\n * installed by `installPageRoutes` / `installPageRoutesFromManifest`, both\n * of which call `router.get`. A `POST` therefore cannot have been meant\n * for a page, by construction. HEAD rides along because Fastify answers it\n * from the GET route.\n *\n * (2) is EXPLICIT and the word is load-bearing. A wildcard does NOT count:\n * `* /*` — what a bare `fetch()` sends — is not a request for a document,\n * it is the absence of a preference, and `text/*` claims a family rather\n * than the type. Only the literal `text/html` media range, with a non-zero\n * `q`, opens the page path. A browser address-bar navigation always sends\n * an explicit `text/html`; a `fetch()` that has not asked for one never\n * does. So the mistyped `/api/...` in a `fetch()` keeps its JSON body and\n * dies at the typo rather than inside `response.json()`.\n *\n * WHAT THE RULE CANNOT DECIDE, and does not pretend to: a browser navigating to\n * a typo'd API URL asks for `text/html`, and so is answered with the document.\n * Nothing in that request distinguishes it from a typo'd page URL — same verb,\n * same header, same absence of a match — so the rule does not guess. The status\n * is 404 either way, which is the part machines read.\n *\n * ── DEPENDENCY NOTE ──────────────────────────────────────────────────────────\n * This module has NO runtime imports. `../build/discover-pages` imports the\n * filename constant and the identity helpers from here so build discovery and\n * both installers cannot disagree about what a not-found page is, and that edge\n * must not drag the render pipeline into the build.\n */\nimport type { HttpContext } from \"@warlock.js/core\";\nimport type { PageRouteHandler } from \"./create-page-route-handler\";\n\n/**\n * The one filename that makes a page THE not-found page.\n *\n * `404.page.tsx`, not `not-found.page.tsx`: it keeps the `*.page.tsx`\n * convention every other page follows, and `404` is the token a developer\n * greps for when a URL answers with one.\n */\nexport const NOT_FOUND_PAGE_FILENAME = \"404.page.tsx\";\n\n/**\n * The path the not-found route is registered on — find-my-way's and Fastify's\n * catch-all, and the same literal core's own dev dispatcher registers\n * (`core/src/router/router.ts`, `server.route({ url: \"*\" })`).\n *\n * A catch-all has the LOWEST matching priority in both routers, so every\n * declared page and every declared API route still wins on its own path; this\n * route is only ever reached because nothing else claimed the URL.\n */\nexport const NOT_FOUND_ROUTE_PATH = \"*\";\n\n/**\n * The reserved route name the not-found page is registered under.\n *\n * Namespaced under `warlock.` because it is the framework's route rather than\n * the application's, and because the router's name namespace is shared with API\n * routes — an application that takes this name gets core's duplicate-name error,\n * which is the loud answer, not a silent overwrite.\n *\n * It is deliberately NOT published into the route table (`href()` / `<Link>`):\n * the not-found page has no URL of its own to link to.\n */\nexport const NOT_FOUND_ROUTE_NAME = \"warlock.not-found\";\n\n/** True when `sourceFile`'s basename is exactly {@link NOT_FOUND_PAGE_FILENAME}. */\nexport function isNotFoundPageFile(sourceFile: string): boolean {\n const separator = Math.max(sourceFile.lastIndexOf(\"/\"), sourceFile.lastIndexOf(\"\\\\\"));\n\n return sourceFile.slice(separator + 1) === NOT_FOUND_PAGE_FILENAME;\n}\n\n/**\n * Raised when more than one `404.page.tsx` exists.\n *\n * There is exactly one not-found route in a process, so a second file is not a\n * per-module override — it is two files claiming one route, with the winner\n * decided by directory-walk order. Both are named because the fix is to delete\n * one and the operator has to know which two are in play.\n */\nexport class DuplicateNotFoundPageError extends Error {\n public constructor(public readonly pageFiles: readonly string[]) {\n super(\n `Two or more not-found pages were found: ${pageFiles.map((file) => `\"${file}\"`).join(\", \")}. ` +\n `An application has exactly one \\`${NOT_FOUND_PAGE_FILENAME}\\` — it answers every ` +\n \"unmatched page URL in the process, so a second one would silently never render. \" +\n \"Keep one and delete the rest.\",\n );\n this.name = \"DuplicateNotFoundPageError\";\n }\n}\n\n/**\n * Raised when `404.page.tsx` declares a `route` export.\n *\n * The not-found page has no URL of its own — it is reached by NOT matching. A\n * `route` export on it reads like a promise that `/404` is browsable, and it is\n * not: the installers register this file on the catch-all and nowhere else. So\n * the export is refused rather than ignored, because a declaration the framework\n * silently drops is worse than one it rejects.\n */\nexport class NotFoundPageDeclaresRouteError extends Error {\n public constructor(public readonly pageFile: string) {\n super(\n `\"${pageFile}\" is the not-found page but declares a \\`route\\` export. ` +\n `\\`${NOT_FOUND_PAGE_FILENAME}\\` has no URL of its own — it answers every page URL that ` +\n \"matched nothing, and is never registered at a path of its own. Remove the `route` \" +\n \"export; to serve a browsable page at a fixed path, use a normal `*.page.tsx`.\",\n );\n this.name = \"NotFoundPageDeclaresRouteError\";\n }\n}\n\n/** The shape this module reads off a registered route — core's `Route`, narrowed. */\nexport type RegisteredRouteShape = {\n path: string;\n isPage?: boolean;\n};\n\n/**\n * The media range the not-found PAGE is gated on. Compared literally: a request\n * either named this exact type or it did not.\n */\nconst HTML_MEDIA_TYPE = \"text/html\";\n\n/**\n * True when `text/html` is named EXPLICITLY in an `Accept` header — the whole\n * discriminator, in one predicate.\n *\n * Wildcards are refused on purpose. `* /*` is what `fetch()` and `curl` send\n * when the caller expressed no preference at all, and `text/*` names a family;\n * neither is a request for a document, and treating either as one is what makes\n * a mistyped `/api/...` answer HTML to a JSON parser.\n *\n * `q=0` is honoured because it is the header's own way of saying \"not this\n * one\" — `Accept: text/html;q=0, application/json` is a client refusing the\n * document, and reading it as a request for one would be reading the header\n * backwards. Any other `q`, present or absent, counts.\n */\nexport function acceptsHtmlExplicitly(accept: string | undefined): boolean {\n if (!accept) return false;\n\n for (const entry of accept.split(\",\")) {\n const [rawType, ...parameters] = entry.split(\";\");\n\n if (rawType.trim().toLowerCase() !== HTML_MEDIA_TYPE) continue;\n\n const quality = parameters\n .map((parameter) => parameter.trim().toLowerCase())\n .find((parameter) => parameter.startsWith(\"q=\"));\n\n // A malformed `q` is not a refusal — only an explicit zero is.\n if (quality !== undefined && Number.parseFloat(quality.slice(2)) === 0) continue;\n\n return true;\n }\n\n return false;\n}\n\nexport type UnmatchedRequestKind = \"page\" | \"api\";\n\n/**\n * The rule, in one function — see this file's header for why it is these two\n * conditions and why the second one refuses wildcards.\n */\nexport function classifyUnmatchedRequest(input: {\n method: string;\n accept: string | undefined;\n}): UnmatchedRequestKind {\n const method = input.method.toUpperCase();\n\n // Pages are installed with `router.get`, so only these two verbs can ever\n // have been asking for one.\n if (method !== \"GET\" && method !== \"HEAD\") return \"api\";\n\n return acceptsHtmlExplicitly(input.accept) ? \"page\" : \"api\";\n}\n\n/**\n * The document served when the application ships no `404.page.tsx`.\n *\n * Deliberately a STRING, not a React render: it must survive the case where the\n * application root, a layout or the page module is exactly what is broken, and\n * a default that can itself fail is not a default. It carries no stylesheet and\n * no hydration script for the same reason — nothing here can 500.\n *\n * It answers 404 like the real page does, because the status is the part that\n * search engines, caches and monitoring read; a framework default that soft-404s\n * would teach every un-customised application to lie.\n */\nexport function frameworkDefaultNotFoundDocument(): string {\n return (\n \"<!doctype html>\" +\n '<html lang=\"en\">' +\n \"<head>\" +\n '<meta charset=\"utf-8\">' +\n '<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">' +\n '<meta name=\"robots\" content=\"noindex\">' +\n \"<title>404 — Page not found</title>\" +\n \"</head>\" +\n \"<body>\" +\n \"<h1>404 — Page not found</h1>\" +\n \"<p>This URL does not match any page.</p>\" +\n `<p>To replace this page, add <code>${NOT_FOUND_PAGE_FILENAME}</code> to a web folder ` +\n \"(for example <code>src/web/404.page.tsx</code>).</p>\" +\n \"</body>\" +\n \"</html>\"\n );\n}\n\nexport type NotFoundRouteHandlerOptions = {\n /**\n * The application's `404.page.tsx`, already built into a page handler by\n * whichever installer owns module loading — `undefined` when the application\n * ships no such file, which is what selects\n * {@link frameworkDefaultNotFoundDocument}.\n *\n * Taking a built handler rather than a module keeps this file out of the\n * render pipeline entirely: dev hands over a Vite-backed handler, production a\n * manifest-backed one, and neither difference is visible here.\n */\n renderPage?: PageRouteHandler;\n};\n\n/**\n * The handler registered on the catch-all.\n *\n * Three answers, in this order, and the order is the safety property: the API\n * check runs BEFORE anything can render, so no request that the rule calls an\n * API request can reach a React render even if the page module is broken.\n */\nexport function createNotFoundRouteHandler(\n options: NotFoundRouteHandlerOptions,\n): PageRouteHandler {\n const { renderPage } = options;\n\n return async (context: HttpContext) => {\n const { request, response } = context;\n // Node lowercases header names and collapses a repeated `Accept` into an\n // array; both forms are read, so a duplicated header cannot silently mean\n // \"no preference\".\n const accept = request.header(\"accept\");\n\n if (\n classifyUnmatchedRequest({\n method: request.method,\n accept: Array.isArray(accept) ? accept.join(\",\") : accept,\n }) === \"api\"\n ) {\n // The same body core's own dev dispatcher writes for an unmatched route\n // (`core/src/router/router.ts`), so an API 404 reads identically whether\n // it fell through to core or was declined here — and identically in\n // development and in production, which it previously was not.\n await response.send(\n { error: \"Route not found\", path: request.path, method: request.method },\n 404,\n );\n\n return;\n }\n\n if (renderPage === undefined) {\n await response.html(frameworkDefaultNotFoundDocument(), 404);\n\n return;\n }\n\n return renderPage(context);\n };\n}\n"],"mappings":";;;;;;;;AAiEA,MAAa,0BAA0B;;;;;;;;;;AAWvC,MAAa,uBAAuB;;;;;;;;;;;;AAapC,MAAa,uBAAuB;;AAGpC,SAAgB,mBAAmB,YAA6B;CAC9D,MAAM,YAAY,KAAK,IAAI,WAAW,YAAY,GAAG,GAAG,WAAW,YAAY,IAAI,CAAC;CAEpF,OAAO,WAAW,MAAM,YAAY,CAAC,MAAM;AAC7C;;;;;;;;;AAUA,IAAa,6BAAb,cAAgD,MAAM;CACjB;CAAnC,AAAO,YAAY,AAAgB,WAA8B;EAC/D,MACE,2CAA2C,UAAU,KAAK,SAAS,IAAI,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,qCACrD,wBAAwB,oIAGhE;EANiC;EAOjC,KAAK,OAAO;CACd;AACF;;;;;;;;;;AAWA,IAAa,iCAAb,cAAoD,MAAM;CACrB;CAAnC,AAAO,YAAY,AAAgB,UAAkB;EACnD,MACE,IAAI,SAAS,6DACN,wBAAwB,8NAGjC;EANiC;EAOjC,KAAK,OAAO;CACd;AACF;;;;;AAYA,MAAM,kBAAkB;;;;;;;;;;;;;;;AAgBxB,SAAgB,sBAAsB,QAAqC;CACzE,IAAI,CAAC,QAAQ,OAAO;CAEpB,KAAK,MAAM,SAAS,OAAO,MAAM,GAAG,GAAG;EACrC,MAAM,CAAC,SAAS,GAAG,cAAc,MAAM,MAAM,GAAG;EAEhD,IAAI,QAAQ,KAAK,CAAC,CAAC,YAAY,MAAM,iBAAiB;EAEtD,MAAM,UAAU,WACb,KAAK,cAAc,UAAU,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAClD,MAAM,cAAc,UAAU,WAAW,IAAI,CAAC;EAGjD,IAAI,YAAY,UAAa,OAAO,WAAW,QAAQ,MAAM,CAAC,CAAC,MAAM,GAAG;EAExE,OAAO;CACT;CAEA,OAAO;AACT;;;;;AAQA,SAAgB,yBAAyB,OAGhB;CACvB,MAAM,SAAS,MAAM,OAAO,YAAY;CAIxC,IAAI,WAAW,SAAS,WAAW,QAAQ,OAAO;CAElD,OAAO,sBAAsB,MAAM,MAAM,IAAI,SAAS;AACxD;;;;;;;;;;;;;AAcA,SAAgB,mCAA2C;CACzD,OACE,gUAWsC,wBAAwB;AAKlE;;;;;;;;AAuBA,SAAgB,2BACd,SACkB;CAClB,MAAM,EAAE,eAAe;CAEvB,OAAO,OAAO,YAAyB;EACrC,MAAM,EAAE,SAAS,aAAa;EAI9B,MAAM,SAAS,QAAQ,OAAO,QAAQ;EAEtC,IACE,yBAAyB;GACvB,QAAQ,QAAQ;GAChB,QAAQ,MAAM,QAAQ,MAAM,IAAI,OAAO,KAAK,GAAG,IAAI;EACrD,CAAC,MAAM,OACP;GAKA,MAAM,SAAS,KACb;IAAE,OAAO;IAAmB,MAAM,QAAQ;IAAM,QAAQ,QAAQ;GAAO,GACvE,GACF;GAEA;EACF;EAEA,IAAI,eAAe,QAAW;GAC5B,MAAM,SAAS,KAAK,iCAAiC,GAAG,GAAG;GAE3D;EACF;EAEA,OAAO,WAAW,OAAO;CAC3B;AACF"}
@@ -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,EAAE,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,EAAE,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,EAAE,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,SAAS,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 ||\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 +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,EAAE,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,EACxF,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,EAClD,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 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 +1 @@
1
- {"version":3,"file":"render-page.mjs","names":[],"sources":["../../../../../../../web/src/server/render-page.ts"],"sourcesContent":["import { createElement, type ComponentType, type ReactNode } from \"react\";\nimport { Response, type Request } from \"@warlock.js/core\";\nimport DefaultApp from \"../components/default-app\";\nimport {\n DocumentContext,\n escapePayload,\n PAYLOAD_SCRIPT_ID,\n type DocumentContextValue,\n} from \"../components/document-context\";\nimport type { SharedContext } from \"../index\";\nimport { buildHydrationPayload } from \"./build-hydration-payload\";\nimport {\n hydrationErrorPageProps,\n resolveErrorPageMetadata,\n type ErrorPageModule,\n type ErrorPageModuleLoader,\n} from \"./error-page\";\nimport { ERROR_PAGE_METADATA } from \"./resolve-page-metadata\";\nimport {\n registerModules,\n type RegisterableModuleNamespace,\n} from \"../runtime/register-modules\";\nimport { markNonHydrating } from \"./page-render-bundle\";\nimport type { ServerErrorPageProps } from \"../props\";\nimport {\n buildErrorRecord,\n designateBoundary,\n executePageRequest,\n type BufferedCookie,\n type ExecutePageRequestOptions,\n type PageDataBundle,\n type PageErrorRecord,\n type PageLevelName,\n type PageResponseCommit,\n type PageRouteEntry,\n type PageRouteMatch,\n type PageTripleModule,\n} from \"./execute-page-request\";\n\nexport { escapePayload, PAYLOAD_SCRIPT_ID };\nexport type { BufferedCookie };\n\n/** Widens `PageDataBundle` with the stage 7 commit record — see `execute-page-request.ts`. */\ntype Bundle = PageDataBundle & { commit?: PageResponseCommit };\n\n/** Reads the stage 7 commit into the lowercased header map `RenderedPage` carries. */\nfunction committedHeaders(bundle: PageDataBundle): Record<string, string> {\n const headers: Record<string, string> = {};\n\n for (const header of (bundle as Bundle).commit?.headers ?? []) {\n headers[header.key.toLowerCase()] = header.value;\n }\n\n return headers;\n}\n\n/** Reads the stage 7 commit into the cookie list `RenderedPage` carries. */\nfunction committedCookies(bundle: PageDataBundle): BufferedCookie[] {\n return (bundle as Bundle).commit?.cookies ?? [];\n}\n\n/**\n * Pipeline stages 9–10: RENDER the page tree from the\n * data bundle stages 1–8 produced, then return finalized { html, status,\n * headers }. Stage 10 happens at the CALL SITE in two halves —\n * 10a the caller applies status + headers (the single live-response write,\n * after render, before anything flushes), 10b it flushes\n * the document. Nothing in this module writes the live response. It never\n * re-runs any earlier stage — `renderPage` calls `executePageRequest` and\n * everything here consumes its bundle as-is.\n *\n * `renderPage` is deliberately double-duty (dx-differentiators.md §3): it is\n * the production orchestrator AND the test helper. Because a loader IS a\n * controller, `renderPage(\"products.details\", { params: { id: \"42\" } })`\n * returns `{ html, status, headers, data }` in one call — asserting a page's\n * data and its response headers is a unit test, no browser, no server boot.\n */\n\n// ---------------------------------------------------------------------------\n// The routes seam (same pattern as connectPageContext: boot wiring, once)\n// ---------------------------------------------------------------------------\n\nexport type PageRoutesRegistry = {\n routes: readonly PageRouteEntry[];\n /** Same contract as ExecutePageRequestOptions[\"createHttp\"]. */\n createHttp: ExecutePageRequestOptions[\"createHttp\"];\n};\n\nlet pageRoutesRegistry: PageRoutesRegistry | undefined;\n\n/**\n * Boot-time wiring so `renderPage(name, options)` can resolve a route NAME\n * without each call site carrying the manifest. Returns the previous registry\n * so tests can restore it. A per-call `routes`/`createHttp` override wins.\n */\nexport function connectPageRoutes(\n registry: PageRoutesRegistry | undefined,\n): PageRoutesRegistry | undefined {\n const previous = pageRoutesRegistry;\n pageRoutesRegistry = registry;\n return previous;\n}\n\n// ---------------------------------------------------------------------------\n// renderPage surface\n// ---------------------------------------------------------------------------\n\nexport type RenderPageOptions = {\n params?: Record<string, string>;\n query?: Record<string, string>;\n /**\n * Impersonation for tests: assigned to `request.user` right after the\n * request pair is constructed — `user` is a plain public property on core's\n * Request (core/src/http/request.ts:92) and this is exactly the write auth\n * middleware would have performed.\n */\n as?: unknown;\n /** Per-call overrides of the connected registry (tests, mostly). */\n routes?: readonly PageRouteEntry[];\n createHttp?: ExecutePageRequestOptions[\"createHttp\"];\n /** Loaded only after the ordinary boundary chain has been exhausted. */\n loadErrorPage?: ErrorPageModuleLoader;\n};\n\n/**\n * `renderPageRequest` takes the URL itself, so `params`/`query` (the\n * name-based sugar buildUrl consumes) have no meaning here — everything else\n * is the same seam.\n */\nexport type RenderPageRequestOptions = Omit<\n RenderPageOptions,\n \"params\" | \"query\"\n>;\n\nexport type RenderedPage = {\n /** The full document (\"\" when the pipeline short-circuited before render). */\n html: string;\n status: number;\n /** Committed response headers, lowercased key → value. */\n headers: Record<string, string>;\n /** Committed response cookies, in commit order — stage 7's `bundle.commit.cookies`. */\n cookies: BufferedCookie[];\n /**\n * The PAGE loader's data — `data.product.name` reads as the dx story\n * writes it. `unknown`: the pipeline never checks a loader's return shape.\n */\n data: unknown;\n /**\n * The full stages-1–8 bundle, for assertions beyond the page's own data.\n * Undefined ONLY on `renderPageRequest`'s no-match path: no route matched,\n * so no pipeline ran and there is no bundle — the 404 answer stands alone.\n * `renderPage` always carries one (its no-match throws instead).\n */\n bundle: PageDataBundle | undefined;\n};\n\nexport type RenderPageFailureOptions = {\n name: string;\n path: string;\n request: Request;\n response: Response;\n thrown: unknown;\n loadErrorPage?: ErrorPageModuleLoader;\n};\n\nfunction requireRegistry(\n options: Pick<RenderPageOptions, \"routes\" | \"createHttp\">,\n): PageRoutesRegistry {\n const routes = options.routes ?? pageRoutesRegistry?.routes;\n const createHttp = options.createHttp ?? pageRoutesRegistry?.createHttp;\n\n if (!routes || !createHttp) {\n throw new Error(\n \"renderPage()/renderPageRequest() has no route registry connected \" +\n \"(web/src/server/render-page.ts). Both resolve against the page \" +\n \"manifest, which the server bootstrap owns. Fix: \" +\n \"call connectPageRoutes({ routes, createHttp }) at boot (tests: in \" +\n \"beforeAll), or pass { routes, createHttp } to this call.\",\n );\n }\n\n return { routes, createHttp };\n}\n\nfunction buildUrl(\n entry: PageRouteEntry,\n params: Record<string, string>,\n query: Record<string, string>,\n): string {\n const path = entry.path\n .split(\"/\")\n .map((segment) => {\n if (!segment.startsWith(\":\")) return segment;\n\n const name = segment.slice(1);\n const value = params[name];\n\n if (value === undefined) {\n throw new Error(\n `renderPage(\"${entry.name}\"): route path \"${entry.path}\" needs ` +\n `param \"${name}\" and the call did not provide it ` +\n \"(web/src/server/render-page.ts). Fix: pass it in \" +\n `\\`params: { ${name}: … }\\`.`,\n );\n }\n\n return encodeURIComponent(value);\n })\n .join(\"/\");\n\n const queryString = new URLSearchParams(query).toString();\n\n return queryString ? `${path}?${queryString}` : path;\n}\n\n// ---------------------------------------------------------------------------\n// Stage 9 — RENDER\n// ---------------------------------------------------------------------------\n\n/**\n * The framework-owned terminal boundary (P1 §4: designation falls back to\n * `app` even when no level exports one — \"the framework owns a root\n * boundary\"). Deliberately generic: the error itself is server knowledge and\n * never serialized into the document.\n */\nfunction FrameworkRootBoundary(): ReactNode {\n return createElement(\"main\", { role: \"alert\" }, \"Something went wrong.\");\n}\n\nfunction errorPageElement(\n module: ErrorPageModule,\n props: ServerErrorPageProps,\n): ReactNode {\n const ErrorPage = module.default as\n ((input: ServerErrorPageProps) => ReactNode) | undefined;\n if (!ErrorPage) {\n throw new Error(\n \"The application error.page.tsx module has no default export.\",\n );\n }\n return createElement(ErrorPage, props);\n}\n\ntype LevelProps = {\n data: unknown;\n shared: Readonly<SharedContext> | undefined;\n children?: ReactNode;\n};\n\n/** The ordinary page leaf alone receives the route match's params. */\ntype PageLevelProps = {\n data: unknown;\n shared: Readonly<SharedContext> | undefined;\n params: Readonly<Record<string, string>>;\n};\n\nconst DATA_KEYS: Record<PageLevelName, \"appData\" | \"layoutData\" | \"pageData\"> =\n {\n app: \"appData\",\n layout: \"layoutData\",\n page: \"pageData\",\n };\n\n/**\n * Compose the tree root→leaf: `<App><Layout><Page/></Layout></App>`, each\n * level receiving ITS OWN loader data and the same sealed `shared` — the\n * exact props the M1 contract declares (web/src/props.ts) and never\n * request/response (the component also renders on a machine where neither\n * exists, props.ts:19-22).\n *\n * A level with no default export contributes no DOM and passes children\n * through — that is `layout.tsx` omitting its default export to be a guard\n * with no DOM.\n */\nfunction buildPageElement(\n triple: Record<PageLevelName, PageTripleModule>,\n bundle: PageDataBundle,\n): ReactNode {\n return wrapRootward(triple, bundle, \"page\", buildLeaf(triple.page, bundle));\n}\n\n/**\n * The error path renders the DESIGNATED boundary in place of the level it\n * covers, still wrapped by every level rootward of it — a page-level throw\n * keeps its App and Layout chrome, whose data survived the settle rules\n * (P1 §4: fulfilled sibling data stays in the bundle).\n *\n * `record` is explicit rather than read from `bundle.error` — a render-time\n * throw (`finishRender`'s stage 9 escalation loop) designates a NEW boundary on the fly that the stage 1-8 bundle never saw.\n */\nfunction buildBoundaryElement(\n triple: Record<PageLevelName, PageTripleModule>,\n bundle: PageDataBundle,\n record: PageErrorRecord,\n): ReactNode {\n const { boundary, error } = record;\n const Boundary = triple[boundary.boundaryLevel].ErrorBoundary as\n ((props: { error: unknown }) => ReactNode) | undefined;\n\n const element = Boundary\n ? createElement(Boundary, { error })\n : createElement(FrameworkRootBoundary, {});\n\n const wrapped = wrapRootward(triple, bundle, boundary.boundaryLevel, element);\n\n // \"App\" has no level rootward of it, so `wrapRootward` returns `wrapped`\n // unwrapped when the boundary covers the app level itself — but the\n // pipeline always emits a complete document, so the\n // framework default supplies the shell here even though the app's own\n // (broken) root is what's being bypassed.\n return boundary.boundaryLevel === \"app\"\n ? createElement(DefaultApp, { children: wrapped })\n : wrapped;\n}\n\nfunction buildLeaf(\n module: PageTripleModule,\n bundle: PageDataBundle,\n): ReactNode {\n const Component = module.default as\n ((props: PageLevelProps) => ReactNode) | undefined;\n\n if (!Component) return null;\n\n return createElement(Component as ComponentType<PageLevelProps>, {\n data: bundle.pageData,\n shared: bundle.shared,\n params: bundle.route.params,\n });\n}\n\nfunction wrapRootward(\n triple: Record<PageLevelName, PageTripleModule>,\n bundle: PageDataBundle,\n from: PageLevelName,\n leaf: ReactNode,\n): ReactNode {\n const wrappers: PageLevelName[] =\n from === \"page\" ? [\"layout\", \"app\"] : from === \"layout\" ? [\"app\"] : [];\n\n let element = leaf;\n\n for (const level of wrappers) {\n const Component = triple[level].default as\n ((props: LevelProps) => ReactNode) | undefined;\n\n if (!Component) {\n // \"App\" is the root: no App export means no custom document, but the\n // pipeline always emits a complete one — the\n // framework default App supplies it. Layout has no such fallback: an\n // omitted layout default export stays a no-DOM passthrough,\n // unchanged from before.\n if (level === \"app\") {\n element = createElement(DefaultApp, { children: element });\n }\n\n continue;\n }\n\n element = createElement(Component as ComponentType<LevelProps>, {\n data: bundle[DATA_KEYS[level]],\n shared: bundle.shared,\n children: element,\n });\n }\n\n return element;\n}\n\n// ---------------------------------------------------------------------------\n// Document assembly — stage 10 (10a apply + 10b flush) lives at the call site\n// ---------------------------------------------------------------------------\n\n/**\n * The root (App or the framework default) now ALWAYS renders a complete\n * `<html>…</html>` document itself — `<Head/>`/\n * `<Scripts/>` read the metadata/payload from `DocumentContext` (provided\n * around the element in `finishRender`, below) and emit real elements.\n * There is nothing left for this stage to assemble by string surgery; it\n * only prepends the doctype `renderToString` never includes.\n */\nfunction emitDocument(body: string): string {\n return \"<!DOCTYPE html>\" + body;\n}\n\n// ---------------------------------------------------------------------------\n// The shared tail (stages 9–10) — both orchestrators end here\n// ---------------------------------------------------------------------------\n\n/**\n * The real request/response pair `capturingCreateHttp` captured for this\n * call. It is used at\n * the two orchestrator call sites for the `as` impersonation write\n * (`state.captured.request.user = as`, below) and to read the document\n * slots (`documentSlotsFrom`, below).\n */\ntype CapturedHttp = {\n request: Request;\n response: Response;\n};\n\n/**\n * Wrap the caller's createHttp to capture the real pair (for the document\n * slots, `documentSlotsFrom` below), the matched entry (the only place a\n * URL-based caller learns which triple to render), and to apply `as` —\n * `user` is a plain public property on core's Request\n * (core/src/http/request.ts:92), exactly the write auth middleware performs.\n */\nfunction capturingCreateHttp(\n registry: PageRoutesRegistry,\n as: unknown,\n): {\n state: { captured?: CapturedHttp; match?: PageRouteMatch };\n createHttp: ExecutePageRequestOptions[\"createHttp\"];\n} {\n const state: { captured?: CapturedHttp; match?: PageRouteMatch } = {};\n\n return {\n state,\n createHttp(match) {\n state.match = match;\n state.captured = registry.createHttp(match);\n\n // `!= null` (not just `!== undefined`): `Request.user` is `RequestUser\n // | undefined` (core/src/http/request.ts:93) — it has no `null` member,\n // so an explicit `as: null` is treated the same as \"no impersonation\"\n // rather than written through.\n if (as != null) state.captured.request.user = as;\n\n return state.captured;\n },\n };\n}\n\n/**\n * The two request-derived document slots (`nonce`/`lang` on\n * `DocumentContextValue`), extracted at the orchestrator call sites\n * because `finishRender` no longer carries `captured` (D1). `dir` is not\n * here: core's Request has no dir-like field (checked\n * core/src/http/request.ts — only `nonce` at :177 and `locale` at :343\n * exist) — an app supplies `dir` via its own convention.\n */\ntype DocumentSlots = {\n nonce?: string;\n lang?: string;\n};\n\n/** Reads document slots directly from core's Request. */\nfunction documentSlotsFrom(captured: CapturedHttp | undefined): DocumentSlots {\n const request = captured?.request;\n\n return { nonce: request?.nonce, lang: request?.locale };\n}\n\nasync function finishRender(\n triple: PageRouteEntry[\"triple\"],\n bundle: PageDataBundle,\n documentSlots: DocumentSlots,\n response: Response,\n loadErrorPage: ErrorPageModuleLoader | undefined,\n): Promise<RenderedPage> {\n // Read from the stage 7 commit, never live off `response` — this function\n // writes (and now reads) the live response zero times. A bundle with no\n // commit (no loader ran at all) simply has no headers/cookies to report.\n const headers = committedHeaders(bundle);\n const cookies = committedCookies(bundle);\n\n // Middleware and validation short-circuits emit no document. Loader-returned\n // Response instances never reach this function.\n if (bundle.shortCircuit) {\n const status =\n bundle.shortCircuit.stage === \"validation\"\n ? bundle.shortCircuit.status\n : (bundle.shortCircuit.statusCode ?? 200);\n return {\n html: \"\",\n status,\n headers,\n cookies,\n data: bundle.pageData,\n bundle,\n };\n }\n\n // The framework's closed-by-default answer (README rule 8): every document\n // is `Cache-Control: private` unless a loader's committed headers already\n // answered for the key. Map-only — the caller applies the returned headers.\n if (headers[\"cache-control\"] === undefined) {\n headers[\"cache-control\"] = \"private\";\n }\n\n // ── stage 9 · RENDER ─────────────────────────────────────────────────────\n // Lazy import: react-dom is a peer used only on this path, so merely\n // loading the server barrel never requires it.\n const { renderToString } = await import(\"react-dom/server\");\n\n // JSON.stringify omits object properties whose value is undefined. Loader\n // `<Head/>`/`<Scripts/>` read this context — metadata and the payload are\n // both already final by this point (stages 1-8 are done), so there is\n // nothing left for the root to await.\n //\n // The payload comes from `buildHydrationPayload` rather than being assembled\n // here, so that this document and the `_loader` route hand the browser the\n // SAME object. See that module for why the two must not drift.\n let documentValue: DocumentContextValue = {\n metadata: bundle.metadata,\n payload: buildHydrationPayload(bundle),\n nonce: documentSlots.nonce,\n lang: documentSlots.lang,\n };\n\n const renderWithContext = (element: ReactNode): string =>\n renderToString(\n createElement(DocumentContext.Provider, {\n value: documentValue,\n children: element,\n }),\n );\n\n // A boundary that throws while rendering escalates to\n // the next enclosing boundary rootward; if none survives, the framework's\n // last-resort terminal renders. `currentError` starts as whatever stage\n // 1-8 already designated (`bundle.error`, undefined for a normal page\n // render) and is replaced by each escalation — `bundle.error` itself is\n // never mutated, staying a truthful stage 1-8 record.\n let currentError = bundle.error;\n let renderTimeThrow = false;\n let body: string;\n\n const renderFrameworkRoot = (): string =>\n renderWithContext(\n createElement(DefaultApp, {\n children: createElement(FrameworkRootBoundary, {}),\n }),\n );\n const renderFrameworkAfterErrorPageFailure = (): string => {\n bundle.errorPage = undefined;\n bundle.metadata = ERROR_PAGE_METADATA;\n documentValue = {\n ...documentValue,\n metadata: bundle.metadata,\n payload: buildHydrationPayload(bundle),\n };\n return renderFrameworkRoot();\n };\n\n const renderErrorPage = async (\n thrown: unknown,\n serializableError: unknown = thrown,\n ): Promise<string | undefined> => {\n if (!loadErrorPage) return undefined;\n\n const props: ServerErrorPageProps = { error: thrown, status: 500 };\n const module = await loadErrorPage();\n registerModules([module as RegisterableModuleNamespace]);\n const errorPage = hydrationErrorPageProps(props, serializableError);\n bundle.errorPage = errorPage;\n bundle.metadata = resolveErrorPageMetadata(module, props);\n documentValue = {\n ...documentValue,\n metadata: bundle.metadata,\n payload: buildHydrationPayload(bundle),\n };\n return renderWithContext(\n wrapRootward(triple, bundle, \"page\", errorPageElement(module, props)),\n );\n };\n\n for (;;) {\n try {\n // The application error page is the framework terminal, never a rival\n // to an authored boundary. It is reached only after no app boundary\n // exists (or after that boundary has itself thrown below).\n if (\n currentError?.boundary.boundaryLevel === \"app\" &&\n !triple.app.ErrorBoundary\n ) {\n try {\n body =\n (await renderErrorPage(\n currentError.originalError ?? currentError.error,\n currentError.error,\n )) ?? renderFrameworkRoot();\n } catch {\n body = renderFrameworkAfterErrorPageFailure();\n }\n renderTimeThrow = true;\n break;\n }\n\n const element = currentError\n ? buildBoundaryElement(triple, bundle, currentError)\n : buildPageElement(triple, bundle);\n\n body = renderWithContext(element);\n break;\n } catch (thrown) {\n renderTimeThrow = true;\n\n if (currentError?.boundary.boundaryLevel === \"app\") {\n // The floor: the app-level boundary's own render just threw, so\n // there is nothing rootward of `app` to escalate to (§2's \"none\n // survives\"). Render the framework's trivial boundary directly —\n // bypassing the app's ErrorBoundary/App component, since that is\n // what just failed — wrapped in DefaultApp so the response is still\n // a complete `<html>` document (default-app.tsx:22-46) rather than\n // a bare `<main>` fragment.\n try {\n body = (await renderErrorPage(thrown)) ?? renderFrameworkRoot();\n } catch {\n body = renderFrameworkAfterErrorPageFailure();\n }\n break;\n }\n\n // Escalate from the level rootward of whatever just threw — searching\n // from the SAME level would re-select the boundary that just failed.\n // A throw not yet attributable to a level (a normal page render, no\n // prior designation) starts the search at `page`.\n const throwingLevel: PageLevelName =\n currentError?.boundary.boundaryLevel === \"layout\"\n ? \"app\"\n : currentError\n ? \"layout\"\n : \"page\";\n\n currentError = buildErrorRecord(\n thrown,\n designateBoundary(throwingLevel, triple),\n );\n }\n }\n\n // Status is chosen after render — the last thing that can change the\n // outcome — and RETURNED, never applied: `finishRender` writes the live\n // response zero times. The caller applies status + headers at one site and\n // flushes immediately after (stage 10a/10b). \"The framework owns the status\n // whenever a boundary renders\" (design/request-lifecycle.md stage 7): ANY\n // boundary — nested or app-level, discovered pre-render or escalated during\n // render — forces 500. The boundary's LEVEL only decides which component\n // renders, never the status; the committed status from stage 7\n // (`bundle.commit.statusCode`) stands only for a page with no error at all\n // — read off the commit, never off the live `response`, same as `headers`\n // above. No committed status (no loader called `setStatusCode`) is the\n // ordinary 200.\n const status = currentError\n ? 500\n : ((bundle as Bundle).commit?.statusCode ?? 200);\n\n const html = emitDocument(body);\n\n return { html, status, headers, cookies, data: bundle.pageData, bundle };\n}\n\n/**\n * Render failures that happen before the page pipeline has a triple (notably a\n * module-load or registration throw). This deliberately owns one terminal\n * attempt: an error-page failure falls straight to FrameworkRootBoundary.\n *\n * There is no triple yet, so there is no trustworthy server composition for\n * the browser to hydrate against — every response this function produces is\n * marked `markNonHydrating` (page-render-bundle.ts), on both the bundle and\n * the document payload, whether or not it managed to render the app's own\n * `error.page.tsx`. A normal app error page reached through `finishRender`\n * renders inside a real triple and stays hydratable; this path never does.\n */\nexport async function renderPageFailure(\n options: RenderPageFailureOptions,\n): Promise<RenderedPage> {\n const { request, response, name, path, thrown, loadErrorPage } = options;\n const bundle: PageDataBundle = markNonHydrating({\n route: { name, path, params: {}, query: {} },\n });\n // No pipeline ran (there is no triple), so there is no commit to read —\n // never a live `response.getHeaders()` read either; see `finishRender`.\n const headers: Record<string, string> = { \"cache-control\": \"private\" };\n\n const { renderToString } = await import(\"react-dom/server\");\n const slots = documentSlotsFrom({ request, response });\n const frameworkPayload = markNonHydrating(buildHydrationPayload(bundle));\n let value: DocumentContextValue = {\n metadata: undefined,\n payload: frameworkPayload,\n nonce: slots.nonce,\n lang: slots.lang,\n };\n const renderWithContext = (element: ReactNode): string =>\n renderToString(\n createElement(DocumentContext.Provider, { value, children: element }),\n );\n let body: string;\n\n try {\n if (!loadErrorPage)\n throw new Error(\"No application error page is configured.\");\n const props: ServerErrorPageProps = { error: thrown, status: 500 };\n const module = await loadErrorPage();\n registerModules([module as RegisterableModuleNamespace]);\n const errorPage = hydrationErrorPageProps(props);\n bundle.errorPage = errorPage;\n value = {\n ...value,\n metadata: resolveErrorPageMetadata(module, props),\n payload: markNonHydrating({ ...frameworkPayload, errorPage }),\n };\n body = renderWithContext(\n createElement(DefaultApp, { children: errorPageElement(module, props) }),\n );\n } catch {\n bundle.errorPage = undefined;\n bundle.metadata = ERROR_PAGE_METADATA;\n value = {\n ...value,\n metadata: bundle.metadata,\n payload: markNonHydrating(buildHydrationPayload(bundle)),\n };\n body = renderWithContext(\n createElement(DefaultApp, {\n children: createElement(FrameworkRootBoundary, {}),\n }),\n );\n }\n\n return {\n html: emitDocument(body),\n status: 500,\n headers,\n cookies: [],\n data: undefined,\n bundle,\n };\n}\n\n// ---------------------------------------------------------------------------\n// The orchestrators\n// ---------------------------------------------------------------------------\n\nexport async function renderPage(\n routeName: string,\n options: RenderPageOptions = {},\n): Promise<RenderedPage | Response> {\n const registry = requireRegistry(options);\n const entry = registry.routes.find(\n (candidate) => candidate.name === routeName,\n );\n\n if (!entry) {\n const known = registry.routes\n .map((candidate) => `\"${candidate.name}\"`)\n .join(\", \");\n\n throw new Error(\n `renderPage(\"${routeName}\"): no route with that name ` +\n `(web/src/server/render-page.ts). Known route names: ${known}. ` +\n \"Fix: use a name from the manifest, or connect the manifest that \" +\n \"declares this one.\",\n );\n }\n\n const url = buildUrl(entry, options.params ?? {}, options.query ?? {});\n const { state, createHttp } = capturingCreateHttp(registry, options.as);\n\n const rendered = await executePageRequest({\n url,\n routes: registry.routes,\n createHttp,\n finish: (bundle) =>\n finishRender(\n entry.triple,\n bundle,\n documentSlotsFrom(state.captured),\n state.captured!.response,\n options.loadErrorPage,\n ),\n });\n\n if (!rendered) {\n throw new Error(\n `renderPage(\"${routeName}\"): the built URL \"${url}\" did not match ` +\n \"stage 1 (web/src/server/render-page.ts). The name resolved but the \" +\n \"matcher disagreed — that is a manifest bug, not a caller bug.\",\n );\n }\n\n return rendered;\n}\n\n/**\n * The URL-based sibling of `renderPage` — the production render surface: a\n * real HTTP server has a URL, not a route name. The url goes STRAIGHT to\n * executePageRequest's stage-1 matcher (no buildUrl), then the same shared\n * tail renders and emits.\n *\n * No-match here is NOT the manifest bug renderPage throws on: an arbitrary\n * URL matching no route is a legitimate 404, and a server must ANSWER it —\n * `{ html: \"\", status: 404 }` with an undefined `bundle` (see RenderedPage).\n */\nexport async function renderPageRequest(\n url: string,\n options: RenderPageRequestOptions = {},\n): Promise<RenderedPage | Response> {\n const registry = requireRegistry(options);\n const { state, createHttp } = capturingCreateHttp(registry, options.as);\n\n const rendered = await executePageRequest({\n url,\n routes: registry.routes,\n createHttp,\n finish: (bundle) =>\n finishRender(\n state.match!.entry.triple,\n bundle,\n documentSlotsFrom(state.captured),\n state.captured!.response,\n options.loadErrorPage,\n ),\n });\n\n if (!rendered) {\n return {\n html: \"\",\n status: 404,\n headers: {},\n cookies: [],\n data: undefined,\n bundle: undefined,\n };\n }\n\n // executePageRequest only produces a bundle after createHttp ran for the\n // match, so the captured entry is present whenever the bundle is.\n return rendered;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"render-page.mjs","names":[],"sources":["../../../../../../../web/src/server/render-page.ts"],"sourcesContent":["import { createElement, type ComponentType, type ReactNode } from \"react\";\nimport { Response, type Request } from \"@warlock.js/core\";\nimport DefaultApp from \"../components/default-app\";\nimport {\n DocumentContext,\n escapePayload,\n PAYLOAD_SCRIPT_ID,\n type DocumentContextValue,\n} from \"../components/document-context\";\nimport type { SharedContext } from \"../index\";\nimport { buildHydrationPayload } from \"./build-hydration-payload\";\nimport {\n hydrationErrorPageProps,\n resolveErrorPageMetadata,\n type ErrorPageModule,\n type ErrorPageModuleLoader,\n} from \"./error-page\";\nimport { ERROR_PAGE_METADATA } from \"./resolve-page-metadata\";\nimport {\n registerModules,\n type RegisterableModuleNamespace,\n} from \"../runtime/register-modules\";\nimport { markNonHydrating } from \"./page-render-bundle\";\nimport type { ServerErrorPageProps } from \"../props\";\nimport {\n buildErrorRecord,\n designateBoundary,\n executePageRequest,\n type BufferedCookie,\n type ExecutePageRequestOptions,\n type PageDataBundle,\n type PageErrorRecord,\n type PageLevelName,\n type PageResponseCommit,\n type PageRouteEntry,\n type PageRouteMatch,\n type PageTripleModule,\n} from \"./execute-page-request\";\n\nexport { escapePayload, PAYLOAD_SCRIPT_ID };\nexport type { BufferedCookie };\n\n/** Widens `PageDataBundle` with the stage 7 commit record — see `execute-page-request.ts`. */\ntype Bundle = PageDataBundle & { commit?: PageResponseCommit };\n\n/** Reads the stage 7 commit into the lowercased header map `RenderedPage` carries. */\nfunction committedHeaders(bundle: PageDataBundle): Record<string, string> {\n const headers: Record<string, string> = {};\n\n for (const header of (bundle as Bundle).commit?.headers ?? []) {\n headers[header.key.toLowerCase()] = header.value;\n }\n\n return headers;\n}\n\n/** Reads the stage 7 commit into the cookie list `RenderedPage` carries. */\nfunction committedCookies(bundle: PageDataBundle): BufferedCookie[] {\n return (bundle as Bundle).commit?.cookies ?? [];\n}\n\n/**\n * Pipeline stages 9–10: RENDER the page tree from the\n * data bundle stages 1–8 produced, then return finalized { html, status,\n * headers }. Stage 10 happens at the CALL SITE in two halves —\n * 10a the caller applies status + headers (the single live-response write,\n * after render, before anything flushes), 10b it flushes\n * the document. Nothing in this module writes the live response. It never\n * re-runs any earlier stage — `renderPage` calls `executePageRequest` and\n * everything here consumes its bundle as-is.\n *\n * `renderPage` is deliberately double-duty (dx-differentiators.md §3): it is\n * the production orchestrator AND the test helper. Because a loader IS a\n * controller, `renderPage(\"products.details\", { params: { id: \"42\" } })`\n * returns `{ html, status, headers, data }` in one call — asserting a page's\n * data and its response headers is a unit test, no browser, no server boot.\n */\n\n// ---------------------------------------------------------------------------\n// The routes seam (same pattern as connectPageContext: boot wiring, once)\n// ---------------------------------------------------------------------------\n\nexport type PageRoutesRegistry = {\n routes: readonly PageRouteEntry[];\n /** Same contract as ExecutePageRequestOptions[\"createHttp\"]. */\n createHttp: ExecutePageRequestOptions[\"createHttp\"];\n};\n\nlet pageRoutesRegistry: PageRoutesRegistry | undefined;\n\n/**\n * Boot-time wiring so `renderPage(name, options)` can resolve a route NAME\n * without each call site carrying the manifest. Returns the previous registry\n * so tests can restore it. A per-call `routes`/`createHttp` override wins.\n */\nexport function connectPageRoutes(\n registry: PageRoutesRegistry | undefined,\n): PageRoutesRegistry | undefined {\n const previous = pageRoutesRegistry;\n pageRoutesRegistry = registry;\n return previous;\n}\n\n// ---------------------------------------------------------------------------\n// renderPage surface\n// ---------------------------------------------------------------------------\n\nexport type RenderPageOptions = {\n params?: Record<string, string>;\n query?: Record<string, string>;\n /**\n * Impersonation for tests: assigned to `request.user` right after the\n * request pair is constructed — `user` is a plain public property on core's\n * Request (core/src/http/request.ts:92) and this is exactly the write auth\n * middleware would have performed.\n */\n as?: unknown;\n /** Per-call overrides of the connected registry (tests, mostly). */\n routes?: readonly PageRouteEntry[];\n createHttp?: ExecutePageRequestOptions[\"createHttp\"];\n /** Loaded only after the ordinary boundary chain has been exhausted. */\n loadErrorPage?: ErrorPageModuleLoader;\n};\n\n/**\n * `renderPageRequest` takes the URL itself, so `params`/`query` (the\n * name-based sugar buildUrl consumes) have no meaning here — everything else\n * is the same seam.\n */\nexport type RenderPageRequestOptions = Omit<\n RenderPageOptions,\n \"params\" | \"query\"\n>;\n\nexport type RenderedPage = {\n /** The full document (\"\" when the pipeline short-circuited before render). */\n html: string;\n status: number;\n /** Committed response headers, lowercased key → value. */\n headers: Record<string, string>;\n /** Committed response cookies, in commit order — stage 7's `bundle.commit.cookies`. */\n cookies: BufferedCookie[];\n /**\n * The PAGE loader's data — `data.product.name` reads as the dx story\n * writes it. `unknown`: the pipeline never checks a loader's return shape.\n */\n data: unknown;\n /**\n * The full stages-1–8 bundle, for assertions beyond the page's own data.\n * Undefined ONLY on `renderPageRequest`'s no-match path: no route matched,\n * so no pipeline ran and there is no bundle — the 404 answer stands alone.\n * `renderPage` always carries one (its no-match throws instead).\n */\n bundle: PageDataBundle | undefined;\n};\n\nexport type RenderPageFailureOptions = {\n name: string;\n path: string;\n request: Request;\n response: Response;\n thrown: unknown;\n loadErrorPage?: ErrorPageModuleLoader;\n};\n\nfunction requireRegistry(\n options: Pick<RenderPageOptions, \"routes\" | \"createHttp\">,\n): PageRoutesRegistry {\n const routes = options.routes ?? pageRoutesRegistry?.routes;\n const createHttp = options.createHttp ?? pageRoutesRegistry?.createHttp;\n\n if (!routes || !createHttp) {\n throw new Error(\n \"renderPage()/renderPageRequest() has no route registry connected \" +\n \"(web/src/server/render-page.ts). Both resolve against the page \" +\n \"manifest, which the server bootstrap owns. Fix: \" +\n \"call connectPageRoutes({ routes, createHttp }) at boot (tests: in \" +\n \"beforeAll), or pass { routes, createHttp } to this call.\",\n );\n }\n\n return { routes, createHttp };\n}\n\nfunction buildUrl(\n entry: PageRouteEntry,\n params: Record<string, string>,\n query: Record<string, string>,\n): string {\n const path = entry.path\n .split(\"/\")\n .map((segment) => {\n if (!segment.startsWith(\":\")) return segment;\n\n const name = segment.slice(1);\n const value = params[name];\n\n if (value === undefined) {\n throw new Error(\n `renderPage(\"${entry.name}\"): route path \"${entry.path}\" needs ` +\n `param \"${name}\" and the call did not provide it ` +\n \"(web/src/server/render-page.ts). Fix: pass it in \" +\n `\\`params: { ${name}: … }\\`.`,\n );\n }\n\n return encodeURIComponent(value);\n })\n .join(\"/\");\n\n const queryString = new URLSearchParams(query).toString();\n\n return queryString ? `${path}?${queryString}` : path;\n}\n\n// ---------------------------------------------------------------------------\n// Stage 9 — RENDER\n// ---------------------------------------------------------------------------\n\n/**\n * The framework-owned terminal boundary (P1 §4: designation falls back to\n * `app` even when no level exports one — \"the framework owns a root\n * boundary\"). Deliberately generic: the error itself is server knowledge and\n * never serialized into the document.\n */\nfunction FrameworkRootBoundary(): ReactNode {\n return createElement(\"main\", { role: \"alert\" }, \"Something went wrong.\");\n}\n\nfunction errorPageElement(\n module: ErrorPageModule,\n props: ServerErrorPageProps,\n): ReactNode {\n const ErrorPage = module.default as\n ((input: ServerErrorPageProps) => ReactNode) | undefined;\n if (!ErrorPage) {\n throw new Error(\n \"The application error.page.tsx module has no default export.\",\n );\n }\n return createElement(ErrorPage, props);\n}\n\ntype LevelProps = {\n data: unknown;\n shared: Readonly<SharedContext> | undefined;\n children?: ReactNode;\n};\n\n/** The ordinary page leaf alone receives the route match's params. */\ntype PageLevelProps = {\n data: unknown;\n shared: Readonly<SharedContext> | undefined;\n params: Readonly<Record<string, string>>;\n};\n\nconst DATA_KEYS: Record<PageLevelName, \"appData\" | \"layoutData\" | \"pageData\"> =\n {\n app: \"appData\",\n layout: \"layoutData\",\n page: \"pageData\",\n };\n\n/**\n * Compose the tree root→leaf: `<App><Layout><Page/></Layout></App>`, each\n * level receiving ITS OWN loader data and the same sealed `shared` — the\n * exact props the M1 contract declares (web/src/props.ts) and never\n * request/response (the component also renders on a machine where neither\n * exists, props.ts:19-22).\n *\n * A level with no default export contributes no DOM and passes children\n * through — that is `layout.tsx` omitting its default export to be a guard\n * with no DOM.\n */\nfunction buildPageElement(\n triple: Record<PageLevelName, PageTripleModule>,\n bundle: PageDataBundle,\n): ReactNode {\n return wrapRootward(triple, bundle, \"page\", buildLeaf(triple.page, bundle));\n}\n\n/**\n * The error path renders the DESIGNATED boundary in place of the level it\n * covers, still wrapped by every level rootward of it — a page-level throw\n * keeps its App and Layout chrome, whose data survived the settle rules\n * (P1 §4: fulfilled sibling data stays in the bundle).\n *\n * `record` is explicit rather than read from `bundle.error` — a render-time\n * throw (`finishRender`'s stage 9 escalation loop) designates a NEW boundary on the fly that the stage 1-8 bundle never saw.\n */\nfunction buildBoundaryElement(\n triple: Record<PageLevelName, PageTripleModule>,\n bundle: PageDataBundle,\n record: PageErrorRecord,\n): ReactNode {\n const { boundary, error } = record;\n const Boundary = triple[boundary.boundaryLevel].ErrorBoundary as\n ((props: { error: unknown }) => ReactNode) | undefined;\n\n const element = Boundary\n ? createElement(Boundary, { error })\n : createElement(FrameworkRootBoundary, {});\n\n const wrapped = wrapRootward(triple, bundle, boundary.boundaryLevel, element);\n\n // \"App\" has no level rootward of it, so `wrapRootward` returns `wrapped`\n // unwrapped when the boundary covers the app level itself — but the\n // pipeline always emits a complete document, so the\n // framework default supplies the shell here even though the app's own\n // (broken) root is what's being bypassed.\n return boundary.boundaryLevel === \"app\"\n ? createElement(DefaultApp, { children: wrapped })\n : wrapped;\n}\n\nfunction buildLeaf(\n module: PageTripleModule,\n bundle: PageDataBundle,\n): ReactNode {\n const Component = module.default as\n ((props: PageLevelProps) => ReactNode) | undefined;\n\n if (!Component) return null;\n\n return createElement(Component as ComponentType<PageLevelProps>, {\n data: bundle.pageData,\n shared: bundle.shared,\n params: bundle.route.params,\n });\n}\n\nfunction wrapRootward(\n triple: Record<PageLevelName, PageTripleModule>,\n bundle: PageDataBundle,\n from: PageLevelName,\n leaf: ReactNode,\n): ReactNode {\n const wrappers: PageLevelName[] =\n from === \"page\" ? [\"layout\", \"app\"] : from === \"layout\" ? [\"app\"] : [];\n\n let element = leaf;\n\n for (const level of wrappers) {\n const Component = triple[level].default as\n ((props: LevelProps) => ReactNode) | undefined;\n\n if (!Component) {\n // \"App\" is the root: no App export means no custom document, but the\n // pipeline always emits a complete one — the\n // framework default App supplies it. Layout has no such fallback: an\n // omitted layout default export stays a no-DOM passthrough,\n // unchanged from before.\n if (level === \"app\") {\n element = createElement(DefaultApp, { children: element });\n }\n\n continue;\n }\n\n element = createElement(Component as ComponentType<LevelProps>, {\n data: bundle[DATA_KEYS[level]],\n shared: bundle.shared,\n children: element,\n });\n }\n\n return element;\n}\n\n// ---------------------------------------------------------------------------\n// Document assembly — stage 10 (10a apply + 10b flush) lives at the call site\n// ---------------------------------------------------------------------------\n\n/**\n * The root (App or the framework default) now ALWAYS renders a complete\n * `<html>…</html>` document itself — `<Head/>`/\n * `<Scripts/>` read the metadata/payload from `DocumentContext` (provided\n * around the element in `finishRender`, below) and emit real elements.\n * There is nothing left for this stage to assemble by string surgery; it\n * only prepends the doctype `renderToString` never includes.\n */\nfunction emitDocument(body: string): string {\n return \"<!DOCTYPE html>\" + body;\n}\n\n// ---------------------------------------------------------------------------\n// The shared tail (stages 9–10) — both orchestrators end here\n// ---------------------------------------------------------------------------\n\n/**\n * The real request/response pair `capturingCreateHttp` captured for this\n * call. It is used at\n * the two orchestrator call sites for the `as` impersonation write\n * (`state.captured.request.user = as`, below) and to read the document\n * slots (`documentSlotsFrom`, below).\n */\ntype CapturedHttp = {\n request: Request;\n response: Response;\n};\n\n/**\n * Wrap the caller's createHttp to capture the real pair (for the document\n * slots, `documentSlotsFrom` below), the matched entry (the only place a\n * URL-based caller learns which triple to render), and to apply `as` —\n * `user` is a plain public property on core's Request\n * (core/src/http/request.ts:92), exactly the write auth middleware performs.\n */\nfunction capturingCreateHttp(\n registry: PageRoutesRegistry,\n as: unknown,\n): {\n state: { captured?: CapturedHttp; match?: PageRouteMatch };\n createHttp: ExecutePageRequestOptions[\"createHttp\"];\n} {\n const state: { captured?: CapturedHttp; match?: PageRouteMatch } = {};\n\n return {\n state,\n createHttp(match) {\n state.match = match;\n state.captured = registry.createHttp(match);\n\n // `!= null` (not just `!== undefined`): `Request.user` is `RequestUser\n // | undefined` (core/src/http/request.ts:93) — it has no `null` member,\n // so an explicit `as: null` is treated the same as \"no impersonation\"\n // rather than written through.\n if (as != null) state.captured.request.user = as;\n\n return state.captured;\n },\n };\n}\n\n/**\n * The two request-derived document slots (`nonce`/`lang` on\n * `DocumentContextValue`), extracted at the orchestrator call sites\n * because `finishRender` no longer carries `captured` (D1). `dir` is not\n * here: core's Request has no dir-like field (checked\n * core/src/http/request.ts — only `nonce` at :177 and `locale` at :343\n * exist) — an app supplies `dir` via its own convention.\n */\ntype DocumentSlots = {\n nonce?: string;\n lang?: string;\n};\n\n/** Reads document slots directly from core's Request. */\nfunction documentSlotsFrom(captured: CapturedHttp | undefined): DocumentSlots {\n const request = captured?.request;\n\n return { nonce: request?.nonce, lang: request?.locale };\n}\n\nasync function finishRender(\n triple: PageRouteEntry[\"triple\"],\n bundle: PageDataBundle,\n documentSlots: DocumentSlots,\n response: Response,\n loadErrorPage: ErrorPageModuleLoader | undefined,\n): Promise<RenderedPage> {\n // Read from the stage 7 commit, never live off `response` — this function\n // writes (and now reads) the live response zero times. A bundle with no\n // commit (no loader ran at all) simply has no headers/cookies to report.\n const headers = committedHeaders(bundle);\n const cookies = committedCookies(bundle);\n\n // Middleware and validation short-circuits emit no document. Loader-returned\n // Response instances never reach this function.\n if (bundle.shortCircuit) {\n const status =\n bundle.shortCircuit.stage === \"validation\"\n ? bundle.shortCircuit.status\n : (bundle.shortCircuit.statusCode ?? 200);\n return {\n html: \"\",\n status,\n headers,\n cookies,\n data: bundle.pageData,\n bundle,\n };\n }\n\n // The framework's closed-by-default answer (README rule 8): every document\n // is `Cache-Control: private` unless a loader's committed headers already\n // answered for the key. Map-only — the caller applies the returned headers.\n if (headers[\"cache-control\"] === undefined) {\n headers[\"cache-control\"] = \"private\";\n }\n\n // ── stage 9 · RENDER ─────────────────────────────────────────────────────\n // Lazy import: react-dom is a peer used only on this path, so merely\n // loading the server barrel never requires it.\n const { renderToString } = await import(\"react-dom/server\");\n\n // JSON.stringify omits object properties whose value is undefined. Loader\n // `<Head/>`/`<Scripts/>` read this context — metadata and the payload are\n // both already final by this point (stages 1-8 are done), so there is\n // nothing left for the root to await.\n //\n // The payload comes from `buildHydrationPayload` rather than being assembled\n // here, so that this document and the `_loader` route hand the browser the\n // SAME object. See that module for why the two must not drift.\n let documentValue: DocumentContextValue = {\n metadata: bundle.metadata,\n payload: buildHydrationPayload(bundle),\n nonce: documentSlots.nonce,\n lang: documentSlots.lang,\n };\n\n const renderWithContext = (element: ReactNode): string =>\n renderToString(\n createElement(DocumentContext.Provider, {\n value: documentValue,\n children: element,\n }),\n );\n\n // A boundary that throws while rendering escalates to\n // the next enclosing boundary rootward; if none survives, the framework's\n // last-resort terminal renders. `currentError` starts as whatever stage\n // 1-8 already designated (`bundle.error`, undefined for a normal page\n // render) and is replaced by each escalation — `bundle.error` itself is\n // never mutated, staying a truthful stage 1-8 record.\n let currentError = bundle.error;\n let renderTimeThrow = false;\n let body: string;\n\n const renderFrameworkRoot = (): string =>\n renderWithContext(\n createElement(DefaultApp, {\n children: createElement(FrameworkRootBoundary, {}),\n }),\n );\n const renderFrameworkAfterErrorPageFailure = (): string => {\n bundle.errorPage = undefined;\n bundle.metadata = ERROR_PAGE_METADATA;\n documentValue = {\n ...documentValue,\n metadata: bundle.metadata,\n payload: buildHydrationPayload(bundle),\n };\n return renderFrameworkRoot();\n };\n\n const renderErrorPage = async (\n thrown: unknown,\n serializableError: unknown = thrown,\n ): Promise<string | undefined> => {\n if (!loadErrorPage) return undefined;\n\n const props: ServerErrorPageProps = { error: thrown, status: 500 };\n const module = await loadErrorPage();\n registerModules([module as RegisterableModuleNamespace]);\n const errorPage = hydrationErrorPageProps(props, serializableError);\n bundle.errorPage = errorPage;\n bundle.metadata = resolveErrorPageMetadata(module, props);\n documentValue = {\n ...documentValue,\n metadata: bundle.metadata,\n payload: buildHydrationPayload(bundle),\n };\n return renderWithContext(\n wrapRootward(triple, bundle, \"page\", errorPageElement(module, props)),\n );\n };\n\n for (;;) {\n try {\n // The application error page is the framework terminal, never a rival\n // to an authored boundary. It is reached only after no app boundary\n // exists (or after that boundary has itself thrown below).\n if (\n currentError?.boundary.boundaryLevel === \"app\" &&\n !triple.app.ErrorBoundary\n ) {\n try {\n body =\n (await renderErrorPage(\n currentError.originalError ?? currentError.error,\n currentError.error,\n )) ?? renderFrameworkRoot();\n } catch {\n body = renderFrameworkAfterErrorPageFailure();\n }\n renderTimeThrow = true;\n break;\n }\n\n const element = currentError\n ? buildBoundaryElement(triple, bundle, currentError)\n : buildPageElement(triple, bundle);\n\n body = renderWithContext(element);\n break;\n } catch (thrown) {\n renderTimeThrow = true;\n\n if (currentError?.boundary.boundaryLevel === \"app\") {\n // The floor: the app-level boundary's own render just threw, so\n // there is nothing rootward of `app` to escalate to (§2's \"none\n // survives\"). Render the framework's trivial boundary directly —\n // bypassing the app's ErrorBoundary/App component, since that is\n // what just failed — wrapped in DefaultApp so the response is still\n // a complete `<html>` document (default-app.tsx:22-46) rather than\n // a bare `<main>` fragment.\n try {\n body = (await renderErrorPage(thrown)) ?? renderFrameworkRoot();\n } catch {\n body = renderFrameworkAfterErrorPageFailure();\n }\n break;\n }\n\n // Escalate from the level rootward of whatever just threw — searching\n // from the SAME level would re-select the boundary that just failed.\n // A throw not yet attributable to a level (a normal page render, no\n // prior designation) starts the search at `page`.\n const throwingLevel: PageLevelName =\n currentError?.boundary.boundaryLevel === \"layout\"\n ? \"app\"\n : currentError\n ? \"layout\"\n : \"page\";\n\n currentError = buildErrorRecord(\n thrown,\n designateBoundary(throwingLevel, triple),\n );\n }\n }\n\n // Status is chosen after render — the last thing that can change the\n // outcome — and RETURNED, never applied: `finishRender` writes the live\n // response zero times. The caller applies status + headers at one site and\n // flushes immediately after (stage 10a/10b). \"The framework owns the status\n // whenever a boundary renders\" (design/request-lifecycle.md stage 7): ANY\n // boundary — nested or app-level, discovered pre-render or escalated during\n // render — forces 500. The boundary's LEVEL only decides which component\n // renders, never the status; the committed status from stage 7\n // (`bundle.commit.statusCode`) stands only for a page with no error at all\n // — read off the commit, never off the live `response`, same as `headers`\n // above. No committed status (no loader called `setStatusCode`) is the\n // ordinary 200.\n const status = currentError\n ? 500\n : ((bundle as Bundle).commit?.statusCode ?? 200);\n\n const html = emitDocument(body);\n\n return { html, status, headers, cookies, data: bundle.pageData, bundle };\n}\n\n/**\n * Render failures that happen before the page pipeline has a triple (notably a\n * module-load or registration throw). This deliberately owns one terminal\n * attempt: an error-page failure falls straight to FrameworkRootBoundary.\n *\n * There is no triple yet, so there is no trustworthy server composition for\n * the browser to hydrate against — every response this function produces is\n * marked `markNonHydrating` (page-render-bundle.ts), on both the bundle and\n * the document payload, whether or not it managed to render the app's own\n * `error.page.tsx`. A normal app error page reached through `finishRender`\n * renders inside a real triple and stays hydratable; this path never does.\n */\nexport async function renderPageFailure(\n options: RenderPageFailureOptions,\n): Promise<RenderedPage> {\n const { request, response, name, path, thrown, loadErrorPage } = options;\n const bundle: PageDataBundle = markNonHydrating({\n route: { name, path, params: {}, query: {} },\n });\n // No pipeline ran (there is no triple), so there is no commit to read —\n // never a live `response.getHeaders()` read either; see `finishRender`.\n const headers: Record<string, string> = { \"cache-control\": \"private\" };\n\n const { renderToString } = await import(\"react-dom/server\");\n const slots = documentSlotsFrom({ request, response });\n const frameworkPayload = markNonHydrating(buildHydrationPayload(bundle));\n let value: DocumentContextValue = {\n metadata: undefined,\n payload: frameworkPayload,\n nonce: slots.nonce,\n lang: slots.lang,\n };\n const renderWithContext = (element: ReactNode): string =>\n renderToString(\n createElement(DocumentContext.Provider, { value, children: element }),\n );\n let body: string;\n\n try {\n if (!loadErrorPage)\n throw new Error(\"No application error page is configured.\");\n const props: ServerErrorPageProps = { error: thrown, status: 500 };\n const module = await loadErrorPage();\n registerModules([module as RegisterableModuleNamespace]);\n const errorPage = hydrationErrorPageProps(props);\n bundle.errorPage = errorPage;\n value = {\n ...value,\n metadata: resolveErrorPageMetadata(module, props),\n payload: markNonHydrating({ ...frameworkPayload, errorPage }),\n };\n body = renderWithContext(\n createElement(DefaultApp, { children: errorPageElement(module, props) }),\n );\n } catch {\n bundle.errorPage = undefined;\n bundle.metadata = ERROR_PAGE_METADATA;\n value = {\n ...value,\n metadata: bundle.metadata,\n payload: markNonHydrating(buildHydrationPayload(bundle)),\n };\n body = renderWithContext(\n createElement(DefaultApp, {\n children: createElement(FrameworkRootBoundary, {}),\n }),\n );\n }\n\n return {\n html: emitDocument(body),\n status: 500,\n headers,\n cookies: [],\n data: undefined,\n bundle,\n };\n}\n\n// ---------------------------------------------------------------------------\n// The orchestrators\n// ---------------------------------------------------------------------------\n\nexport async function renderPage(\n routeName: string,\n options: RenderPageOptions = {},\n): Promise<RenderedPage | Response> {\n const registry = requireRegistry(options);\n const entry = registry.routes.find(\n (candidate) => candidate.name === routeName,\n );\n\n if (!entry) {\n const known = registry.routes\n .map((candidate) => `\"${candidate.name}\"`)\n .join(\", \");\n\n throw new Error(\n `renderPage(\"${routeName}\"): no route with that name ` +\n `(web/src/server/render-page.ts). Known route names: ${known}. ` +\n \"Fix: use a name from the manifest, or connect the manifest that \" +\n \"declares this one.\",\n );\n }\n\n const url = buildUrl(entry, options.params ?? {}, options.query ?? {});\n const { state, createHttp } = capturingCreateHttp(registry, options.as);\n\n const rendered = await executePageRequest({\n url,\n routes: registry.routes,\n createHttp,\n finish: (bundle) =>\n finishRender(\n entry.triple,\n bundle,\n documentSlotsFrom(state.captured),\n state.captured!.response,\n options.loadErrorPage,\n ),\n });\n\n if (!rendered) {\n throw new Error(\n `renderPage(\"${routeName}\"): the built URL \"${url}\" did not match ` +\n \"stage 1 (web/src/server/render-page.ts). The name resolved but the \" +\n \"matcher disagreed — that is a manifest bug, not a caller bug.\",\n );\n }\n\n return rendered;\n}\n\n/**\n * The URL-based sibling of `renderPage` — the production render surface: a\n * real HTTP server has a URL, not a route name. The url goes STRAIGHT to\n * executePageRequest's stage-1 matcher (no buildUrl), then the same shared\n * tail renders and emits.\n *\n * No-match here is NOT the manifest bug renderPage throws on: an arbitrary\n * URL matching no route is a legitimate 404, and a server must ANSWER it —\n * `{ html: \"\", status: 404 }` with an undefined `bundle` (see RenderedPage).\n */\nexport async function renderPageRequest(\n url: string,\n options: RenderPageRequestOptions = {},\n): Promise<RenderedPage | Response> {\n const registry = requireRegistry(options);\n const { state, createHttp } = capturingCreateHttp(registry, options.as);\n\n const rendered = await executePageRequest({\n url,\n routes: registry.routes,\n createHttp,\n finish: (bundle) =>\n finishRender(\n state.match!.entry.triple,\n bundle,\n documentSlotsFrom(state.captured),\n state.captured!.response,\n options.loadErrorPage,\n ),\n });\n\n if (!rendered) {\n return {\n html: \"\",\n status: 404,\n headers: {},\n cookies: [],\n data: undefined,\n bundle: undefined,\n };\n }\n\n // executePageRequest only produces a bundle after createHttp ran for the\n // match, so the captured entry is present whenever the bundle is.\n return rendered;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"settle-page-response.mjs","names":[],"sources":["../../../../../../../web/src/server/settle-page-response.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { Response } from \"@warlock.js/core\";\nimport type {\n PageBoundaryDesignation,\n PageErrorRecord,\n PageLevelName,\n PageRouteEntry,\n} from \"./execute-page-request.types\";\n\nexport const LEVEL_ORDER: readonly PageLevelName[] = [\"app\", \"layout\", \"page\"];\n\nexport function designateBoundary(\n throwingLevel: PageLevelName,\n triple: PageRouteEntry[\"triple\"],\n): PageBoundaryDesignation {\n const throwingIndex = LEVEL_ORDER.indexOf(throwingLevel);\n\n for (let index = throwingIndex; index >= 0; index--) {\n const level = LEVEL_ORDER[index];\n\n if (triple[level].ErrorBoundary) {\n return { throwingLevel, boundaryLevel: level };\n }\n }\n\n return { throwingLevel, boundaryLevel: \"app\" };\n}\n\nexport function buildErrorRecord(\n thrown: unknown,\n boundary: PageBoundaryDesignation,\n requestPath?: string,\n): PageErrorRecord {\n const digest = randomUUID();\n\n console.error(\"[warlock] page error\", digest, ...(requestPath ? [requestPath] : []), thrown);\n\n if (process.env.NODE_ENV === \"production\") {\n const surrogate = new Error(\"An unexpected error occurred.\");\n\n (surrogate as Error & { digest: string }).digest = digest;\n\n return { originalError: thrown, error: surrogate, boundary, digest, scrubbed: true };\n }\n\n // `error` already IS the real thrown value here — `originalError` only ever\n // needs to diverge from it on the scrubbed (production) path above. Leaving\n // it `undefined` rather than a redundant second reference to the same object\n // keeps the record's `toEqual` shape honest (undefined properties compare as\n // absent) and readers still get the real error via\n // `record.originalError ?? record.error`.\n return { originalError: undefined, error: thrown, boundary, digest, scrubbed: false };\n}\n\n// ---------------------------------------------------------------------------\n// Stage 6/7 — buffered per-level responses, and the root→leaf commit\n// ---------------------------------------------------------------------------\n\n/** A single committed response header, in application order. */\nexport type BufferedHeader = { key: string; value: string };\n\n/** A single committed response cookie — the shape `applyBufferedCookie` replays. */\nexport type BufferedCookie = {\n name: string;\n value: unknown;\n options?: Record<string, unknown>;\n};\n\n/** The two loader short-circuit kinds a buffered response can signal. */\nexport type LoaderShortCircuitKind = \"redirect\" | \"notFound\";\n\nconst LOADER_SHORT_CIRCUIT = Symbol(\"warlock.page.loaderShortCircuit\");\n\n/**\n * What `response.redirect()` / `response.permanentRedirect()` / `response.notFound()`\n * return from inside a loader — a branded value the stage 7 settle scan\n * recognises by symbol, never by shape (so an app returning an\n * accidentally-similar plain object can't be mistaken for one).\n */\nexport type LoaderShortCircuitSignal = {\n readonly [LOADER_SHORT_CIRCUIT]: true;\n kind: LoaderShortCircuitKind;\n statusCode: number;\n url?: string;\n body?: unknown;\n};\n\nexport function isLoaderShortCircuit(value: unknown): value is LoaderShortCircuitSignal {\n return Boolean(value) && typeof value === \"object\" && LOADER_SHORT_CIRCUIT in (value as object);\n}\n\n/** One level's scratch buffer — what `response.header()`/`.cookie()` write into. */\nexport type LevelBuffer = {\n headers: BufferedHeader[];\n cookies: BufferedCookie[];\n statusCode?: number;\n};\n\nexport function createLevelBuffer(): LevelBuffer {\n return { headers: [], cookies: [] };\n}\n\n/**\n * The response surface a LOADER sees — never the live core `Response`.\n * `header()`/`cookie()` queue into the level's own buffer; nothing here\n * touches the real reply. `redirect()`/`permanentRedirect()`/`notFound()`\n * queue the buffer's own status (+ `Location`, for the two redirects) AND\n * return the branded signal stage 7 detects — the loader is expected to\n * `return response.redirect(...)`.\n */\nexport type BufferedResponse = {\n header(key: string, value: unknown): BufferedResponse;\n headers(bag: Record<string, unknown>): BufferedResponse;\n cookie(name: string, value: unknown, options?: Record<string, unknown>): BufferedResponse;\n setStatusCode(statusCode: number): BufferedResponse;\n redirect(url: string, statusCode?: number): LoaderShortCircuitSignal;\n permanentRedirect(url: string): LoaderShortCircuitSignal;\n notFound(body?: unknown): LoaderShortCircuitSignal;\n};\n\nexport function createBufferedResponse(buffer: LevelBuffer): BufferedResponse {\n const bufferedResponse: BufferedResponse = {\n header(key, value) {\n buffer.headers.push({ key, value: String(value) });\n return bufferedResponse;\n },\n headers(bag) {\n for (const [key, value] of Object.entries(bag)) bufferedResponse.header(key, value);\n return bufferedResponse;\n },\n cookie(name, value, options) {\n buffer.cookies.push({ name, value, options });\n return bufferedResponse;\n },\n setStatusCode(statusCode) {\n buffer.statusCode = statusCode;\n return bufferedResponse;\n },\n redirect(url, statusCode = 302) {\n buffer.statusCode = statusCode;\n buffer.headers.push({ key: \"Location\", value: url });\n return { [LOADER_SHORT_CIRCUIT]: true, kind: \"redirect\", statusCode, url, body: undefined };\n },\n permanentRedirect(url) {\n return bufferedResponse.redirect(url, 301);\n },\n notFound(body) {\n buffer.statusCode = 404;\n return { [LOADER_SHORT_CIRCUIT]: true, kind: \"notFound\", statusCode: 404, url: undefined, body };\n },\n };\n\n return bufferedResponse;\n}\n\n/** Stage 7's folded, applied result — what `bundle.commit` carries. */\nexport type PageResponseCommit = {\n committedLevels: PageLevelName[];\n headers: BufferedHeader[];\n cookies: BufferedCookie[];\n statusCode?: number;\n};\n\n/**\n * Fold every surviving buffer root→leaf into ONE map per key (header key\n * case-insensitively, cookie by name) — leafward wins, insertion position\n * stays where the key FIRST appeared. Applies the folded headers and status\n * to the REAL response (`header()`/`setStatusCode()` are idempotent keyed\n * sets, so this is safe even though `commitBuffers` can run before render\n * changes its mind about the status later). Cookies are NOT applied to the\n * real response here — `cookie()` APPENDS, so mirroring it here and again at\n * the wire emit would duplicate every `Set-Cookie`. The single application\n * site is the emit (`create-page-route-handler.ts`, via `applyBufferedCookie`\n * over `bundle.commit.cookies`).\n */\nexport function commitBuffers(\n response: Response,\n buffers: Record<PageLevelName, LevelBuffer>,\n committedLevels: PageLevelName[],\n): PageResponseCommit {\n const headerOrder: string[] = [];\n const headerMap = new Map<string, BufferedHeader>();\n const cookieOrder: string[] = [];\n const cookieMap = new Map<string, BufferedCookie>();\n let statusCode: number | undefined;\n\n for (const level of committedLevels) {\n const buffer = buffers[level];\n\n for (const header of buffer.headers) {\n const key = header.key.toLowerCase();\n if (!headerMap.has(key)) headerOrder.push(key);\n headerMap.set(key, header);\n }\n\n for (const cookie of buffer.cookies) {\n if (!cookieMap.has(cookie.name)) cookieOrder.push(cookie.name);\n cookieMap.set(cookie.name, cookie);\n }\n\n if (buffer.statusCode !== undefined) statusCode = buffer.statusCode;\n }\n\n const headers = headerOrder.map(key => headerMap.get(key)!);\n const cookies = cookieOrder.map(name => cookieMap.get(name)!);\n\n for (const header of headers) response.header(header.key, header.value);\n if (statusCode !== undefined) response.setStatusCode(statusCode);\n\n return { committedLevels, headers, cookies, statusCode };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"settle-page-response.mjs","names":[],"sources":["../../../../../../../web/src/server/settle-page-response.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { Response } from \"@warlock.js/core\";\nimport type {\n PageBoundaryDesignation,\n PageErrorRecord,\n PageLevelName,\n PageRouteEntry,\n} from \"./execute-page-request.types\";\n\nexport const LEVEL_ORDER: readonly PageLevelName[] = [\"app\", \"layout\", \"page\"];\n\nexport function designateBoundary(\n throwingLevel: PageLevelName,\n triple: PageRouteEntry[\"triple\"],\n): PageBoundaryDesignation {\n const throwingIndex = LEVEL_ORDER.indexOf(throwingLevel);\n\n for (let index = throwingIndex; index >= 0; index--) {\n const level = LEVEL_ORDER[index];\n\n if (triple[level].ErrorBoundary) {\n return { throwingLevel, boundaryLevel: level };\n }\n }\n\n return { throwingLevel, boundaryLevel: \"app\" };\n}\n\nexport function buildErrorRecord(\n thrown: unknown,\n boundary: PageBoundaryDesignation,\n requestPath?: string,\n): PageErrorRecord {\n const digest = randomUUID();\n\n console.error(\"[warlock] page error\", digest, ...(requestPath ? [requestPath] : []), thrown);\n\n if (process.env.NODE_ENV === \"production\") {\n const surrogate = new Error(\"An unexpected error occurred.\");\n\n (surrogate as Error & { digest: string }).digest = digest;\n\n return { originalError: thrown, error: surrogate, boundary, digest, scrubbed: true };\n }\n\n // `error` already IS the real thrown value here — `originalError` only ever\n // needs to diverge from it on the scrubbed (production) path above. Leaving\n // it `undefined` rather than a redundant second reference to the same object\n // keeps the record's `toEqual` shape honest (undefined properties compare as\n // absent) and readers still get the real error via\n // `record.originalError ?? record.error`.\n return { originalError: undefined, error: thrown, boundary, digest, scrubbed: false };\n}\n\n// ---------------------------------------------------------------------------\n// Stage 6/7 — buffered per-level responses, and the root→leaf commit\n// ---------------------------------------------------------------------------\n\n/** A single committed response header, in application order. */\nexport type BufferedHeader = { key: string; value: string };\n\n/** A single committed response cookie — the shape `applyBufferedCookie` replays. */\nexport type BufferedCookie = {\n name: string;\n value: unknown;\n options?: Record<string, unknown>;\n};\n\n/** The two loader short-circuit kinds a buffered response can signal. */\nexport type LoaderShortCircuitKind = \"redirect\" | \"notFound\";\n\nconst LOADER_SHORT_CIRCUIT = Symbol(\"warlock.page.loaderShortCircuit\");\n\n/**\n * What `response.redirect()` / `response.permanentRedirect()` / `response.notFound()`\n * return from inside a loader — a branded value the stage 7 settle scan\n * recognises by symbol, never by shape (so an app returning an\n * accidentally-similar plain object can't be mistaken for one).\n */\nexport type LoaderShortCircuitSignal = {\n readonly [LOADER_SHORT_CIRCUIT]: true;\n kind: LoaderShortCircuitKind;\n statusCode: number;\n url?: string;\n body?: unknown;\n};\n\nexport function isLoaderShortCircuit(value: unknown): value is LoaderShortCircuitSignal {\n return Boolean(value) && typeof value === \"object\" && LOADER_SHORT_CIRCUIT in (value as object);\n}\n\n/** One level's scratch buffer — what `response.header()`/`.cookie()` write into. */\nexport type LevelBuffer = {\n headers: BufferedHeader[];\n cookies: BufferedCookie[];\n statusCode?: number;\n};\n\nexport function createLevelBuffer(): LevelBuffer {\n return { headers: [], cookies: [] };\n}\n\n/**\n * The response surface a LOADER sees — never the live core `Response`.\n * `header()`/`cookie()` queue into the level's own buffer; nothing here\n * touches the real reply. `redirect()`/`permanentRedirect()`/`notFound()`\n * queue the buffer's own status (+ `Location`, for the two redirects) AND\n * return the branded signal stage 7 detects — the loader is expected to\n * `return response.redirect(...)`.\n */\nexport type BufferedResponse = {\n header(key: string, value: unknown): BufferedResponse;\n headers(bag: Record<string, unknown>): BufferedResponse;\n cookie(name: string, value: unknown, options?: Record<string, unknown>): BufferedResponse;\n setStatusCode(statusCode: number): BufferedResponse;\n redirect(url: string, statusCode?: number): LoaderShortCircuitSignal;\n permanentRedirect(url: string): LoaderShortCircuitSignal;\n notFound(body?: unknown): LoaderShortCircuitSignal;\n};\n\nexport function createBufferedResponse(buffer: LevelBuffer): BufferedResponse {\n const bufferedResponse: BufferedResponse = {\n header(key, value) {\n buffer.headers.push({ key, value: String(value) });\n return bufferedResponse;\n },\n headers(bag) {\n for (const [key, value] of Object.entries(bag)) bufferedResponse.header(key, value);\n return bufferedResponse;\n },\n cookie(name, value, options) {\n buffer.cookies.push({ name, value, options });\n return bufferedResponse;\n },\n setStatusCode(statusCode) {\n buffer.statusCode = statusCode;\n return bufferedResponse;\n },\n redirect(url, statusCode = 302) {\n buffer.statusCode = statusCode;\n buffer.headers.push({ key: \"Location\", value: url });\n return { [LOADER_SHORT_CIRCUIT]: true, kind: \"redirect\", statusCode, url, body: undefined };\n },\n permanentRedirect(url) {\n return bufferedResponse.redirect(url, 301);\n },\n notFound(body) {\n buffer.statusCode = 404;\n return { [LOADER_SHORT_CIRCUIT]: true, kind: \"notFound\", statusCode: 404, url: undefined, body };\n },\n };\n\n return bufferedResponse;\n}\n\n/** Stage 7's folded, applied result — what `bundle.commit` carries. */\nexport type PageResponseCommit = {\n committedLevels: PageLevelName[];\n headers: BufferedHeader[];\n cookies: BufferedCookie[];\n statusCode?: number;\n};\n\n/**\n * Fold every surviving buffer root→leaf into ONE map per key (header key\n * case-insensitively, cookie by name) — leafward wins, insertion position\n * stays where the key FIRST appeared. Applies the folded headers and status\n * to the REAL response (`header()`/`setStatusCode()` are idempotent keyed\n * sets, so this is safe even though `commitBuffers` can run before render\n * changes its mind about the status later). Cookies are NOT applied to the\n * real response here — `cookie()` APPENDS, so mirroring it here and again at\n * the wire emit would duplicate every `Set-Cookie`. The single application\n * site is the emit (`create-page-route-handler.ts`, via `applyBufferedCookie`\n * over `bundle.commit.cookies`).\n */\nexport function commitBuffers(\n response: Response,\n buffers: Record<PageLevelName, LevelBuffer>,\n committedLevels: PageLevelName[],\n): PageResponseCommit {\n const headerOrder: string[] = [];\n const headerMap = new Map<string, BufferedHeader>();\n const cookieOrder: string[] = [];\n const cookieMap = new Map<string, BufferedCookie>();\n let statusCode: number | undefined;\n\n for (const level of committedLevels) {\n const buffer = buffers[level];\n\n for (const header of buffer.headers) {\n const key = header.key.toLowerCase();\n if (!headerMap.has(key)) headerOrder.push(key);\n headerMap.set(key, header);\n }\n\n for (const cookie of buffer.cookies) {\n if (!cookieMap.has(cookie.name)) cookieOrder.push(cookie.name);\n cookieMap.set(cookie.name, cookie);\n }\n\n if (buffer.statusCode !== undefined) statusCode = buffer.statusCode;\n }\n\n const headers = headerOrder.map(key => headerMap.get(key)!);\n const cookies = cookieOrder.map(name => cookieMap.get(name)!);\n\n for (const header of headers) response.header(header.key, header.value);\n if (statusCode !== undefined) response.setStatusCode(statusCode);\n\n return { committedLevels, headers, cookies, statusCode };\n}\n"],"mappings":"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"}