@warlock.js/web 5.6.0 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/esm/build/contribution.mjs +1 -1
  3. package/esm/build/discover-pages.mjs +3 -3
  4. package/esm/build/generate-pages-barrel.mjs +2 -2
  5. package/esm/build/page-routes-manifest.mjs +1 -1
  6. package/esm/build/public-files.mjs +1 -1
  7. package/esm/client/navigation/fetch-page-data.mjs +3 -10
  8. package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
  9. package/esm/client/navigation/navigation-root.mjs +1 -1
  10. package/esm/hydration-payload.mjs +19 -10
  11. package/esm/hydration-payload.mjs.map +1 -1
  12. package/esm/loaders.d.mts +12 -7
  13. package/esm/route.d.mts +8 -32
  14. package/esm/server/create-page-route-handler.mjs +12 -6
  15. package/esm/server/create-page-route-handler.mjs.map +1 -1
  16. package/esm/server/execute-page-request.mjs +29 -37
  17. package/esm/server/execute-page-request.mjs.map +1 -1
  18. package/esm/server/execute-page-request.types.d.mts +15 -4
  19. package/esm/server/hydration-client-url.mjs +1 -1
  20. package/esm/server/index.d.mts +2 -1
  21. package/esm/server/index.mjs +4 -4
  22. package/esm/server/install-page-routes-from-manifest.d.mts +9 -0
  23. package/esm/server/install-page-routes-from-manifest.mjs +20 -20
  24. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  25. package/esm/server/install-page-routes.d.mts +2 -24
  26. package/esm/server/install-page-routes.mjs +48 -25
  27. package/esm/server/install-page-routes.mjs.map +1 -1
  28. package/esm/server/match-page-route.mjs +6 -10
  29. package/esm/server/match-page-route.mjs.map +1 -1
  30. package/esm/server/not-found-page.d.mts +1 -0
  31. package/esm/server/not-found-page.mjs +8 -1
  32. package/esm/server/not-found-page.mjs.map +1 -1
  33. package/esm/server/page-file-change.mjs +1 -1
  34. package/esm/server/page-module-shapes.d.mts +24 -0
  35. package/esm/server/page-route-reload.mjs +1 -1
  36. package/esm/server/register-production-public-files.mjs +1 -1
  37. package/esm/server/render-page.d.mts +3 -2
  38. package/esm/server/render-page.mjs +1 -0
  39. package/esm/server/render-page.mjs.map +1 -1
  40. package/esm/server/resolve-route-validation-input.mjs +2 -2
  41. package/esm/server/resolve-route-validation-input.mjs.map +1 -1
  42. package/esm/server/resolve-validation-data.mjs +1 -1
  43. package/esm/server/resolve-validation-data.mjs.map +1 -1
  44. package/esm/server/stylesheet-urls.mjs +78 -19
  45. package/esm/server/stylesheet-urls.mjs.map +1 -1
  46. package/esm/server/web-connector.mjs +2 -2
  47. package/esm/server/web-connector.mjs.map +1 -1
  48. package/esm/validation.d.mts +12 -1
  49. package/esm/vite/build-client.mjs +1 -1
  50. package/esm/vite/gate-a-resolve.mjs +1 -1
  51. package/esm/vite/hydration-entries.mjs +1 -1
  52. package/llms-full.txt +25 -19
  53. package/package.json +3 -3
  54. package/skills/create-a-page/SKILL.md +25 -19
  55. package/esm/server/route-validation-error.mjs +0 -32
  56. package/esm/server/route-validation-error.mjs.map +0 -1
@@ -1,17 +1,17 @@
1
1
  import { publishRouteTable } from "../routing/route-table.mjs";
2
2
  import { composeRoutePath } from "../routing/compose-route-path.mjs";
3
- import { duplicateRoutePathMessage } from "../routing/duplicate-route-path.mjs";
3
+ import { DuplicateNotFoundPageError, NOT_FOUND_ROUTE_NAME, NOT_FOUND_ROUTE_PATH, NotFoundPageDeclaresRouteError, isNotFoundPageFile, registerNotFoundPageRoute } from "./not-found-page.mjs";
4
4
  import { PageFileSegmentNotSupportedError } from "../routing/page-file-segment.mjs";
5
5
  import { deriveFilesystemRoutePath } from "../routing/filesystem-route.mjs";
6
- import { resolveLayoutLevel as resolveLayoutLevel$1 } from "../routing/layout-level.mjs";
7
6
  import { resolvePageRouteCache, resolvePageRouteIdentity, resolvePageRouteName } from "../routing/route-identity.mjs";
8
- import { createPageRouteHandler } from "./create-page-route-handler.mjs";
9
- import { foldLayoutLoaders } from "./fold-layout-loaders.mjs";
10
- import { devHandlerStylesheetUrls } from "./stylesheet-urls.mjs";
11
- import { DuplicateNotFoundPageError, NOT_FOUND_ROUTE_NAME, NOT_FOUND_ROUTE_PATH, NotFoundPageDeclaresRouteError, createNotFoundRouteHandler, isNotFoundPageFile } from "./not-found-page.mjs";
12
7
  import { NonLiteralRouteExportError, readRouteExports } from "../build/read-route-exports.mjs";
13
8
  import { toPosix } from "../shared/to-posix.mjs";
14
9
  import { ErrorPageDeclaresRouteError, discoverPageFiles, isErrorPageFile, layoutChainFor } from "../build/discover-pages.mjs";
10
+ import { duplicateRoutePathMessage } from "../routing/duplicate-route-path.mjs";
11
+ import { resolveLayoutLevel as resolveLayoutLevel$1 } from "../routing/layout-level.mjs";
12
+ import { createPageRouteHandler } from "./create-page-route-handler.mjs";
13
+ import { foldLayoutLoaders } from "./fold-layout-loaders.mjs";
14
+ import { devHandlerStylesheetUrls } from "./stylesheet-urls.mjs";
15
15
  import "@warlock.js/core";
16
16
  import path from "node:path";
17
17
 
@@ -48,6 +48,24 @@ import path from "node:path";
48
48
  * through Vite (`vite.ssrLoadModule`), never read statically, because a dev
49
49
  * page module must be the one Vite serves, warm cache and all.
50
50
  */
51
+ /** Raised at boot when a page still uses the withdrawn `route.middleware` export. */
52
+ var RouteMiddlewareRemovedError = class extends Error {
53
+ pageFile;
54
+ constructor(pageFile) {
55
+ super(`"${pageFile}" declares \`route.middleware\`, which no longer runs — it was withdrawn after 5.6.0. Move it to the page's own top-level \`middleware\` export instead: \`export const middleware = [...]\`.`);
56
+ this.pageFile = pageFile;
57
+ this.name = "RouteMiddlewareRemovedError";
58
+ }
59
+ };
60
+ /** Raised at boot when a page still uses the withdrawn `route.validate` export. */
61
+ var RouteValidationRemovedError = class extends Error {
62
+ pageFile;
63
+ constructor(pageFile) {
64
+ super(`"${pageFile}" declares \`route.validate\`, which no longer runs — it was withdrawn after 5.6.0. Move it to the page's top-level \`validation\` export instead: \`export const validation = { params: ..., query: ... }\`.`);
65
+ this.pageFile = pageFile;
66
+ this.name = "RouteValidationRemovedError";
67
+ }
68
+ };
51
69
  /**
52
70
  * Ownership key for the framework's fallback 404 route. A NUL-prefixed value
53
71
  * cannot be a real filesystem path, so it cannot collide with an app page's
@@ -209,6 +227,11 @@ async function installPageRoutes(options) {
209
227
  });
210
228
  continue;
211
229
  }
230
+ const route = pageModule.route;
231
+ if (typeof route === "object" && route !== null) {
232
+ if ("middleware" in route) throw new RouteMiddlewareRemovedError(pageFile);
233
+ if ("validate" in route) throw new RouteValidationRemovedError(pageFile);
234
+ }
212
235
  const sourceFile = canonicalSourceFileFor(pageFile, appSrcRoot);
213
236
  const { path: routePath, name } = resolvePageRouteIdentity(pageModule.route, filesystemPageFileFor(pageFile, appSrcRoot), pageFile);
214
237
  const cache = resolvePageRouteCache(pageModule.route, pageFile);
@@ -234,7 +257,7 @@ async function installPageRoutes(options) {
234
257
  appFile,
235
258
  ...layoutLevel.chain,
236
259
  pageFile
237
- ]);
260
+ ], vite.moduleGraph);
238
261
  await router.withSourceFile(sourceFile, () => router.get(effectivePath, createPageRouteHandler({
239
262
  path: effectivePath,
240
263
  name,
@@ -268,23 +291,23 @@ async function installPageRoutes(options) {
268
291
  if (notFoundPageFile !== void 0) {
269
292
  if ((await vite.ssrLoadModule(notFoundPageFile)).route !== void 0) throw new NotFoundPageDeclaresRouteError(notFoundPageFile);
270
293
  }
271
- const registerNotFoundRoute = () => router.get("*", createNotFoundRouteHandler({ renderPage: notFoundPageFile === void 0 ? void 0 : createPageRouteHandler({
272
- path: "*",
273
- name: NOT_FOUND_ROUTE_NAME,
274
- appFile,
275
- pageFile: notFoundPageFile,
276
- layoutFile: void 0,
277
- loadModule: (moduleId) => vite.ssrLoadModule(moduleId),
278
- hydrationClientModuleUrl,
279
- loadErrorPage,
280
- stylesheetUrls: devHandlerStylesheetUrls(stylesheetRoot, [appFile, notFoundPageFile]),
281
- matchPath: (requestPath) => requestPath,
282
- statusForRenderedOk: 404,
283
- skipPageLoader: true,
284
- ...httpServerOption
285
- }) }), {
286
- name: NOT_FOUND_ROUTE_NAME,
287
- isPage: true
294
+ const registerNotFoundRoute = () => registerNotFoundPageRoute({
295
+ router,
296
+ renderPage: notFoundPageFile === void 0 ? void 0 : createPageRouteHandler({
297
+ path: "*",
298
+ name: NOT_FOUND_ROUTE_NAME,
299
+ appFile,
300
+ pageFile: notFoundPageFile,
301
+ layoutFile: void 0,
302
+ loadModule: (moduleId) => vite.ssrLoadModule(moduleId),
303
+ hydrationClientModuleUrl,
304
+ loadErrorPage,
305
+ stylesheetUrls: devHandlerStylesheetUrls(stylesheetRoot, [appFile, notFoundPageFile], vite.moduleGraph),
306
+ matchPath: (requestPath) => requestPath,
307
+ statusForRenderedOk: 404,
308
+ skipPageLoader: true,
309
+ ...httpServerOption
310
+ })
288
311
  });
289
312
  if (notFoundPageFile === void 0) await router.withSourceFile(FRAMEWORK_DEFAULT_NOT_FOUND_SOURCE_FILE, registerNotFoundRoute);
290
313
  else await router.withSourceFile(canonicalSourceFileFor(notFoundPageFile, appSrcRoot), registerNotFoundRoute);
@@ -294,5 +317,5 @@ async function installPageRoutes(options) {
294
317
  }
295
318
 
296
319
  //#endregion
297
- export { filesystemPageFileFor, installPageRoutes };
320
+ export { RouteMiddlewareRemovedError, filesystemPageFileFor, installPageRoutes };
298
321
  //# sourceMappingURL=install-page-routes.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"install-page-routes.mjs","names":["resolveComposedLayoutLevel"],"sources":["../../../../../../../web/src/server/install-page-routes.ts"],"sourcesContent":["/**\r\n * Registers every page {@link discoverPageFiles} finds under `<appSrcRoot>`\r\n * into Warlock's router (`router.get`, `core/src/router/router.ts:359-361`)\r\n * so `router.scanDevServer(fastify)` —\r\n * the sanctioned dev-server dispatch path (server matching is Warlock's\r\n * router; there is no second server matcher) — picks\r\n * it up. Replaces the two hand-rolled `fastify.get()` calls this file's\r\n * sibling, `dev-error-transport.ts`, used to make directly.\r\n *\r\n * DELIBERATE EXCEPTION to \"web has no core dependency\", same\r\n * reasoning `dev-error-transport.ts`'s own header comment records: this module is not\r\n * exported from either package barrel and is not part of `web/package.json`'s\r\n * dependency graph — dev/CLI bootstrap only.\r\n *\r\n * Scope note: a page's\r\n * `route.path` is now composed with the `prefix` export of EVERY `layout.tsx`\r\n * on its path — outermost first (`composeRoutePath` below) — before\r\n * registration and before the collision check, so `home.page.tsx`\r\n * (`path: \"/\"`, main layout `prefix: \"/\"`) resolves to `/` and\r\n * `products.page.tsx` (`path: \"/\"`, products layout `prefix: \"/products\"`)\r\n * resolves to `/products` — no collision. A page with no `layout.tsx` on its\r\n * path composes against the implicit root prefix `\"/\"` (e.g. `/contact-us`,\r\n * `/hydration-demo`, both unaffected by composition).\r\n *\r\n * WHICH PAGES EXIST is answered by {@link discoverPageFiles}\r\n * (`web/src/build/discover-pages.ts`) — the same walk production's build\r\n * shares — so this file owns no directory-walking of its own; it serves the\r\n * page root (`<appSrcRoot>/web/**`) exactly as discovery enumerates it. WHAT\r\n * ROUTE A PAGE ANSWERS ON stays this file's own job: each page and its nearest layout are still evaluated\r\n * through Vite (`vite.ssrLoadModule`), never read statically, because a dev\r\n * page module must be the one Vite serves, warm cache and all.\r\n */\r\nimport path from \"node:path\";\r\nimport type { ViteDevServer } from \"vite\";\r\nimport {\r\n discoverPageFiles,\r\n ErrorPageDeclaresRouteError,\r\n isErrorPageFile,\r\n layoutChainFor,\r\n} from \"../build/discover-pages\";\r\nimport { NonLiteralRouteExportError, readRouteExports } from \"../build/read-route-exports\";\r\nimport { composeRoutePath } from \"../routing/compose-route-path\";\r\nimport { duplicateRoutePathMessage } from \"../routing/duplicate-route-path\";\r\nimport { deriveFilesystemRoutePath } from \"../routing/filesystem-route\";\r\nimport { resolveLayoutLevel as resolveComposedLayoutLevel } from \"../routing/layout-level\";\r\nimport { PageFileSegmentNotSupportedError } from \"../routing/page-file-segment\";\r\nimport { toPosix } from \"../shared/to-posix\";\r\nimport {\r\n resolvePageRouteCache,\r\n resolvePageRouteIdentity,\r\n resolvePageRouteName,\r\n type PageCacheOptIn,\r\n} from \"../routing/route-identity\";\r\nimport { publishRouteTable } from \"../routing/route-table\";\r\nimport { type FastifyInstance, type Router } from \"@warlock.js/core\";\r\nimport { createPageRouteHandler } from \"./create-page-route-handler\";\r\nimport type { ErrorPageModule } from \"./error-page\";\r\nimport type { PipelineLoader, PipelineMiddleware } from \"./execute-page-request\";\r\nimport { foldLayoutLoaders } from \"./fold-layout-loaders\";\r\nimport { devHandlerStylesheetUrls } from \"./stylesheet-urls\";\r\nimport {\r\n createNotFoundRouteHandler,\r\n DuplicateNotFoundPageError,\r\n isNotFoundPageFile,\r\n NotFoundPageDeclaresRouteError,\r\n NOT_FOUND_ROUTE_NAME,\r\n NOT_FOUND_ROUTE_PATH,\r\n} from \"./not-found-page\";\r\n\r\n/** Re-exported so `web/src/server/index.ts`'s existing barrel export keeps resolving. */\r\nexport { composeRoutePath };\r\n\r\nexport type PageRouteExport = string | { path: string; name?: string; cache?: PageCacheOptIn };\r\n\r\nexport type PageModuleShape = {\r\n route?: PageRouteExport;\r\n};\r\n\r\nexport type InstalledPageRoute = {\r\n /** The canonical declared route path, before layout-prefix composition. */\r\n declaredPath: string;\r\n path: string;\r\n name: string;\r\n file: string;\r\n layoutFile: string | undefined;\r\n};\r\n\r\n/**\r\n * Ownership key for the framework's fallback 404 route. A NUL-prefixed value\r\n * cannot be a real filesystem path, so it cannot collide with an app page's\r\n * canonical source-file key.\r\n */\r\nexport const FRAMEWORK_DEFAULT_NOT_FOUND_SOURCE_FILE = \"\\0warlock:framework-default-404\";\r\n\r\n/**\r\n * The page's application-source-relative POSIX source path used as the router's\r\n * stable ownership key. `appSrcRoot`'s own basename preserves the existing\r\n * `src/web/...` source-file convention.\r\n */\r\nfunction canonicalSourceFileFor(pageFile: string, appSrcRoot: string): string {\r\n return `${path.basename(appSrcRoot)}/${toPosix(path.relative(appSrcRoot, pageFile))}`;\r\n}\r\n\r\nexport function filesystemPageFileFor(pageFile: string, appSrcRoot: string): string {\r\n return toPosix(path.relative(path.join(appSrcRoot, \"web\"), pageFile));\r\n}\r\n\r\n/**\r\n * Raised when a page's own module fails to load in dev (`vite.ssrLoadModule`\r\n * rejects on the page file itself — never a layout, which this class does not\r\n * cover).\r\n *\r\n * Before this class existed, that rejection propagated straight out of\r\n * `installPageRoutes`'s loop and failed the WHOLE install: every other page's\r\n * route went unregistered along with the broken one, and the failure named\r\n * neither the page file nor which module actually threw. One bad page taking\r\n * every other page down with it is a worse outage than the bad page alone, so\r\n * this error is what the broken page's own route now fails with instead —\r\n * named, with its cause attached, while the rest of the application keeps\r\n * serving.\r\n */\r\nexport class PageModuleLoadError extends Error {\r\n public constructor(\r\n public readonly pageFile: string,\r\n cause: unknown,\r\n ) {\r\n const rawCause = cause instanceof Error ? cause.message : String(cause);\r\n // The cause is someone else's text — a bundler's, a module resolver's — so\r\n // it may or may not end in punctuation. Without this it runs straight into\r\n // the next sentence, and the seam is exactly where a reader stops trusting\r\n // the message.\r\n const causeMessage = /[.!?]$/.test(rawCause.trim()) ? rawCause.trim() : `${rawCause.trim()}.`;\r\n\r\n super(\r\n `\"${pageFile}\" failed to load: ${causeMessage} Every other page still installed and is ` +\r\n \"still serving; fix the error in this page's module and it will start serving again.\",\r\n { cause },\r\n );\r\n this.name = \"PageModuleLoadError\";\r\n }\r\n}\r\n\r\n/**\r\n * Registers a page whose OWN module failed to load at its filesystem-derived\r\n * URL, so a request there reports {@link PageModuleLoadError} — naming the\r\n * page file and the underlying cause — instead of a bare 404 that explains\r\n * nothing.\r\n *\r\n * Only the filesystem-derived path is attempted: the page's `route` export\r\n * cannot be read (that requires the very module that failed to load), and its\r\n * layout chain is not resolved either, so no layout prefix composes into this\r\n * path. When the filesystem path itself is not derivable\r\n * (`deriveFilesystemRoutePath` rejects a segment), there is no path left to\r\n * register a route at, so nothing is registered — the caller reports both\r\n * failures loudly at boot and moves on.\r\n */\r\nasync function registerFailedPageRoute(input: {\r\n router: Router;\r\n pageFile: string;\r\n appSrcRoot: string;\r\n loadError: unknown;\r\n fileByPath: Map<string, string>;\r\n}): Promise<void> {\r\n const { router, pageFile, appSrcRoot, loadError, fileByPath } = input;\r\n const attributed = new PageModuleLoadError(pageFile, loadError);\r\n const filesystemPageFile = filesystemPageFileFor(pageFile, appSrcRoot);\r\n\r\n let effectivePath: string;\r\n\r\n try {\r\n effectivePath = deriveFilesystemRoutePath({ pageFile: filesystemPageFile });\r\n } catch (segmentError) {\r\n if (!(segmentError instanceof PageFileSegmentNotSupportedError)) throw segmentError;\r\n\r\n // No filesystem path to register a route at, so this page cannot answer\r\n // its own URL with the attributed error — the request that would have hit\r\n // it 404s instead, unregistered, and the existing unregistered-page\r\n // reporter (`web/src/server/unregistered-pages.ts`) is what explains that\r\n // 404 to whoever is looking. Reported loudly here so the underlying load\r\n // failure is not lost as well.\r\n console.error(attributed.message);\r\n console.error(segmentError.message);\r\n\r\n return;\r\n }\r\n\r\n const existingFile = fileByPath.get(effectivePath);\r\n\r\n if (existingFile) {\r\n throw new Error(duplicateRoutePathMessage({ effectivePath, existingFile, newFile: pageFile }));\r\n }\r\n\r\n fileByPath.set(effectivePath, pageFile);\r\n\r\n await router.withSourceFile(canonicalSourceFileFor(pageFile, appSrcRoot), () =>\r\n router.get(\r\n effectivePath,\r\n async () => {\r\n throw attributed;\r\n },\r\n { name: resolvePageRouteName(undefined, filesystemPageFile), isPage: true },\r\n ),\r\n );\r\n}\r\n\r\nexport type LayoutModuleShape = {\r\n /** Universal registration hook; invoked on this real namespace, never a composed wrapper. */\r\n register?: () => unknown;\r\n prefix?: string;\r\n /**\r\n * The default export — the thing that puts an element in the document, and\r\n * therefore the ONLY export that decides whether a layout counts against the\r\n * single-rendering-layout rule (`../routing/layout-policy.ts`). In dev the\r\n * module is loaded, so this is a fact rather than a guess.\r\n */\r\n default?: unknown;\r\n /** The layout's guards, in the order it declared them. */\r\n middleware?: readonly PipelineMiddleware[];\r\n loader?: PipelineLoader;\r\n};\r\n\r\n/** How this module gets a layout module namespace — `vite.ssrLoadModule`, in practice. */\r\ntype LoadLayout = (layoutFile: string) => Promise<LayoutModuleShape>;\r\n\r\n/**\r\n * The page's layout LEVEL, resolved from its whole chain rather than from the\r\n * one layout nearest to it.\r\n *\r\n * The render pipeline has exactly one layout slot per page\r\n * (`execute-page-request.ts`'s `PageRouteEntry[\"triple\"]`), so the chain has to\r\n * be collapsed into one module before it reaches a handler. Two things collapse\r\n * differently and both matter:\r\n *\r\n * - RENDERING is a selection: at most one layout on the chain may render, and\r\n * the policy picks it. `renders` is read off the loaded module\r\n * (`typeof module.default !== \"undefined\"`), never off the filename — a\r\n * `middleware`-only layout has no default export and is not a wrapper, and\r\n * passing a bare path to the policy would have it read as a rendering one,\r\n * which is the conservative default and the wrong answer here.\r\n * - MIDDLEWARE and PREFIX are compositions: every layout on the path\r\n * contributes, outermost first. A guard on an outer layout that the page's\r\n * own directory knows nothing about is exactly the guard that must still run,\r\n * and a prefix nobody composed is a URL nobody wrote down.\r\n *\r\n * The selection and prefix composition rules themselves do not depend on\r\n * where a layout's module came from, so they are not re-derived here — see\r\n * `../routing/layout-level.ts`'s `resolveLayoutLevel`, which production's\r\n * installer calls against the same rules. This function's own job is\r\n * everything that DOES depend on the source: walking the chain\r\n * (`layoutChainFor`), loading each module (`loadLayout`) and keying declared\r\n * prefixes by filesystem directory for {@link deriveFilesystemRoutePath}.\r\n */\r\ntype LayoutLevel = {\r\n /** Every `layout.tsx` from the web root down to the page's directory, outermost first. */\r\n chain: string[];\r\n /**\r\n * The module id the handler's layout slot is registered under, or `undefined`\r\n * when the page has no layout at all: the layout that RENDERS, or — when none\r\n * does — the nearest one, which is the slot dev has always used and so the\r\n * choice that changes nothing but the middleware for a chain with no wrapper\r\n * in it.\r\n */\r\n layoutFile: string | undefined;\r\n /** Every layout's `prefix`, composed outermost first — `discoverPages`' own reduction. */\r\n prefix: string;\r\n /** Declared prefixes keyed by layout directory relative to this page's web root. */\r\n prefixesByDirectory: Readonly<Record<string, string>>;\r\n};\r\n\r\nasync function resolveLayoutLevel(\r\n pageFile: string,\r\n webRoot: string,\r\n loadLayout: LoadLayout,\r\n): Promise<LayoutLevel> {\r\n const chain = layoutChainFor(pageFile, webRoot);\r\n const modules = await Promise.all(chain.map(loadLayout));\r\n const level = resolveComposedLayoutLevel(\r\n pageFile,\r\n chain.map((layoutFile, index) => ({\r\n id: layoutFile,\r\n renders: typeof modules[index].default !== \"undefined\",\r\n prefix: modules[index].prefix,\r\n })),\r\n );\r\n\r\n return {\r\n chain,\r\n layoutFile: level.hostId,\r\n prefix: level.prefix,\r\n prefixesByDirectory: Object.fromEntries(\r\n chain.flatMap((layoutFile, index) => {\r\n const prefix = modules[index].prefix;\r\n\r\n return prefix === undefined\r\n ? []\r\n : [[toPosix(path.relative(webRoot, path.dirname(layoutFile))), prefix]];\r\n }),\r\n ),\r\n };\r\n}\r\n\r\n/**\r\n * The layout slot's module for ONE request: the slot host's own namespace, with\r\n * the whole chain's middleware in place of its own — outermost first, which is\r\n * the order stage 3 runs the array in (`execute-page-request.ts:519-524`) and\r\n * the order an outer `optionalAuth` needs in order to have resolved an identity\r\n * before an inner `gate()` checks it.\r\n *\r\n * Loaded per call, not once at install time: a dev layout module must be the\r\n * one Vite is currently serving, edits and all.\r\n */\r\nasync function composeLayoutLevel(\r\n level: LayoutLevel & { layoutFile: string },\r\n loadLayout: LoadLayout,\r\n): Promise<LayoutModuleShape> {\r\n const modules = await Promise.all(level.chain.map(loadLayout));\r\n const hostIndex = level.chain.indexOf(level.layoutFile);\r\n const host = modules[hostIndex];\r\n\r\n return {\r\n ...host,\r\n middleware: modules.flatMap((layoutModule) => [...(layoutModule.middleware ?? [])]),\r\n loader: foldLayoutLoaders(\r\n modules.map((layoutModule) => layoutModule.loader),\r\n hostIndex,\r\n ),\r\n };\r\n}\r\n\r\nexport type InstallPageRoutesOptions = {\r\n router: Router;\r\n vite: ViteDevServer;\r\n /** v5/app/src — pages live under \"<appSrcRoot>/web/**\". */\r\n appSrcRoot: string;\r\n /** v5/app/src/web/root.tsx — the single global app-root file. */\r\n appFile: string;\r\n /**\r\n * The application root Vite's dev server serves from — `dev-error-transport.ts`'s\r\n * `paths.appRoot`, i.e. `<appRoot>/src === appSrcRoot` by default. Every\r\n * handler's stylesheet URLs are expressed relative to THIS, because that is\r\n * the root Vite's dev server actually resolves `/…` URLs against\r\n * (`stylesheet-urls.ts`'s `devStylesheetUrls`) — not `appSrcRoot`, which is\r\n * one directory level in.\r\n *\r\n * OPTIONAL and defaulted to `path.dirname(appSrcRoot)`: the caller that\r\n * wires dev boot (`web-connector.ts`) does not pass this field today, and\r\n * that default is exactly the relationship it constructs `appSrcRoot` from\r\n * (`appSrcRoot = path.join(appRoot, \"src\")`) — correct for every actual\r\n * deployment, and overridable by a caller with a non-default layout.\r\n */\r\n appRoot?: string;\r\n /** Browser module loaded after the server-rendered application and payload. */\r\n hydrationClientModuleUrl?: string;\r\n /**\r\n * UNUSED. Retained on this type only because `web-connector.ts` still builds\r\n * an options object naming it (`devStylesheetUrls(paths.appRoot,\r\n * paths.appFile)`, computed once for the whole application). Each handler\r\n * now computes its OWN stylesheet chain — `[root, ...outer-to-inner matched\r\n * layouts, page]`, via `devHandlerStylesheetUrls` — inside the registration\r\n * loop below, because a single application-wide list cannot express \"this\r\n * page's own CSS\" without also carrying every other page's.\r\n */\r\n stylesheetUrls?: readonly string[];\r\n /**\r\n * The Fastify instance `HttpConnector.boot()` published, resolved by the\r\n * caller on the NODE side (`web-connector.ts`) and forwarded to every\r\n * `createPageRouteHandler` call below instead of letting that factory read\r\n * `container.get(\"http.server\")` for itself.\r\n *\r\n * Load-bearing, not a convenience: Vite's SSR module runner\r\n * (`vite.ssrLoadModule`, used throughout this file) evaluates\r\n * `create-page-route-handler.ts` as a SECOND copy of `@warlock.js/core`, with\r\n * its own `container` that `HttpConnector.boot()` never wrote to. Reading the\r\n * container from inside that SSR graph therefore always misses, however\r\n * early or late this file calls it — the value has to arrive as a plain\r\n * argument from a caller on the Node side, where the real container lives.\r\n *\r\n * OPTIONAL so existing callers (and this file's own unit tests, which seed\r\n * `container.set(\"http.server\", …)` instead) keep resolving through the\r\n * container exactly as before — omitted here means \"not supplied\", the same\r\n * distinction `createPageRouteHandler` itself draws from `PageRouteHandlerOptions.httpServer`.\r\n */\r\n httpServer?: FastifyInstance;\r\n};\r\n\r\n/**\r\n * Registers every discoverable page into `options.router`. Throws\r\n * IMMEDIATELY, naming both files, the moment two pages declare the same\r\n * `route.path` — a registration-time failure, not a runtime 404 one of them\r\n * silently loses.\r\n *\r\n * Pages with no `route` export derive their path and name from their location\r\n * below `src/web`, using the same pure filesystem-routing helper as the build.\r\n */\r\nexport async function installPageRoutes(\r\n options: InstallPageRoutesOptions,\r\n): Promise<InstalledPageRoute[]> {\r\n const { router, vite, appSrcRoot, appFile, hydrationClientModuleUrl, httpServer } = options;\r\n // Spread conditionally, never as a bare `httpServer,` property: an explicit\r\n // `httpServer: undefined` key is its own signal to `createPageRouteHandler`\r\n // (`\"httpServer\" in options\"`, `create-page-route-handler.ts`) — \"no server,\r\n // on purpose\" — and this file must not manufacture that signal on behalf of\r\n // a caller (this file's own unit tests, most callers) that never supplied\r\n // one and means to fall back to the container instead.\r\n const httpServerOption = httpServer === undefined ? {} : { httpServer };\r\n // See `InstallPageRoutesOptions.appRoot` for why this default, not\r\n // `appSrcRoot` itself, is the root every handler's CSS is resolved against.\r\n const stylesheetRoot = options.appRoot ?? path.dirname(appSrcRoot);\r\n const discovered = [...discoverPageFiles(appSrcRoot)].sort((left, right) =>\r\n left.pageFile < right.pageFile ? -1 : left.pageFile > right.pageFile ? 1 : 0,\r\n );\r\n\r\n // THE NOT-FOUND PAGE IS TAKEN OUT OF THE ORDINARY LOOP, not filtered inside\r\n // it. It has no `route` export to read, no path to compose and no collision\r\n // to check — every step below is about a page with a URL, and `404.page.tsx`\r\n // does not have one. Registering it here would put it at `/404`, which is not\r\n // a page anybody asked to be able to visit.\r\n const errorPageFiles = discovered.filter((page) => isErrorPageFile(page.pageFile));\r\n const notFoundPageFiles = discovered.filter((page) => isNotFoundPageFile(page.pageFile));\r\n const pageFiles = discovered.filter(\r\n (page) => !isNotFoundPageFile(page.pageFile) && !isErrorPageFile(page.pageFile),\r\n );\r\n\r\n if (errorPageFiles.length > 1) {\r\n throw new Error(\r\n `Two error pages were found: ${errorPageFiles.map((page) => page.pageFile).join(\", \")}.`,\r\n );\r\n }\r\n\r\n const errorPageFile = errorPageFiles[0]?.pageFile;\r\n\r\n // Parse only: the error boundary must remain lazy until a request actually\r\n // fails, while a route export is still rejected at install time.\r\n if (errorPageFile !== undefined) {\r\n const declarations = readRouteExports(errorPageFile);\r\n if (!declarations.ok) throw new NonLiteralRouteExportError(declarations.rejection);\r\n if (declarations.route !== undefined) throw new ErrorPageDeclaresRouteError(errorPageFile);\r\n }\r\n const loadErrorPage =\r\n errorPageFile === undefined\r\n ? undefined\r\n : () => vite.ssrLoadModule(errorPageFile) as Promise<ErrorPageModule>;\r\n\r\n if (notFoundPageFiles.length > 1) {\r\n throw new DuplicateNotFoundPageError(notFoundPageFiles.map((page) => page.pageFile));\r\n }\r\n\r\n const installed: InstalledPageRoute[] = [];\r\n const fileByPath = new Map<string, string>();\r\n\r\n for (const { pageFile, webRoot } of pageFiles) {\r\n let pageModule: PageModuleShape;\r\n\r\n try {\r\n pageModule = (await vite.ssrLoadModule(pageFile)) as PageModuleShape;\r\n } catch (loadError) {\r\n // THE PAGE ITSELF MUST NOT ABORT THE INSTALL: every other page still\r\n // needs to install and serve. Layout loading is deliberately NOT\r\n // wrapped here — a broken layout is a different failure with a\r\n // different blast radius (it can affect more than one page) and is out\r\n // of this card's scope.\r\n await registerFailedPageRoute({ router, pageFile, appSrcRoot, loadError, fileByPath });\r\n\r\n continue;\r\n }\r\n\r\n const sourceFile = canonicalSourceFileFor(pageFile, appSrcRoot);\r\n\r\n // Route identity is explicit when declared and filesystem-derived otherwise.\r\n const { path: routePath, name } = resolvePageRouteIdentity(\r\n pageModule.route,\r\n filesystemPageFileFor(pageFile, appSrcRoot),\r\n pageFile,\r\n );\r\n\r\n // Validated at INSTALL time, with everything else — a malformed `cache`\r\n // opt-in fails boot, not the first request that would have served it.\r\n const cache = resolvePageRouteCache(pageModule.route, pageFile);\r\n\r\n const loadLayout: LoadLayout = (layoutFile) =>\r\n vite.ssrLoadModule(layoutFile) as Promise<LayoutModuleShape>;\r\n const layoutLevel = await resolveLayoutLevel(pageFile, webRoot, loadLayout);\r\n const { layoutFile, prefix: layoutPrefix } = layoutLevel;\r\n\r\n const effectivePath =\r\n pageModule.route === undefined\r\n ? deriveFilesystemRoutePath({\r\n pageFile: filesystemPageFileFor(pageFile, appSrcRoot),\r\n layoutPrefixes: layoutLevel.prefixesByDirectory,\r\n })\r\n : composeRoutePath(layoutPrefix, routePath);\r\n\r\n const existingFile = fileByPath.get(effectivePath);\r\n\r\n if (existingFile) {\r\n throw new Error(\r\n duplicateRoutePathMessage({\r\n effectivePath,\r\n existingFile,\r\n newFile: pageFile,\r\n composition: { layoutPrefix, routePath },\r\n }),\r\n );\r\n }\r\n\r\n fileByPath.set(effectivePath, pageFile);\r\n\r\n // Every registered handler gets ITS OWN immutable, ordered, deduped CSS\r\n // chain: root, then every matched layout outer to inner\r\n // (`layoutLevel.chain`), then the page — the same order the render\r\n // pipeline loads that chain in, so cascade order matches load order.\r\n // Computed once here, at registration, not per request: dev re-registers\r\n // on every restart, so a stale chain cannot outlive the source edit that\r\n // changed it.\r\n const stylesheetUrls = devHandlerStylesheetUrls(stylesheetRoot, [\r\n appFile,\r\n ...layoutLevel.chain,\r\n pageFile,\r\n ]);\r\n\r\n await router.withSourceFile(sourceFile, () =>\r\n router.get(\r\n effectivePath,\r\n // The handler itself is `createPageRouteHandler`\r\n // (`web/src/server/create-page-route-handler.ts`) — a named seam a\r\n // future `type: \"page\"` route can bind to, and testable without a Vite\r\n // server. Vite appears here only as the dev answer to \"how do I load a\r\n // module\"; the handler takes that as an input and knows nothing else\r\n // about it.\r\n createPageRouteHandler({\r\n path: effectivePath,\r\n name,\r\n appFile,\r\n pageFile,\r\n layoutFile,\r\n // The layout slot's id resolves to the COMPOSED level — every layout's\r\n // middleware, in chain order — and every other id goes straight to\r\n // Vite. A one-layout chain has nothing to compose, so it is left to\r\n // resolve as the exact module Vite hands back, untouched.\r\n loadModule:\r\n layoutLevel.chain.length > 1 && layoutFile !== undefined\r\n ? (moduleId) =>\r\n moduleId === layoutFile\r\n ? composeLayoutLevel({ ...layoutLevel, layoutFile }, loadLayout)\r\n : vite.ssrLoadModule(moduleId)\r\n : (moduleId) => vite.ssrLoadModule(moduleId),\r\n // Registration tracks real module namespaces, not the composed\r\n // layout wrapper above. Loading the raw chain per request also lets\r\n // Vite hand over a replacement namespace after an HMR update; the\r\n // helper's WeakSet then gives that new identity its one invocation.\r\n loadRegistrationLayouts: () => Promise.all(layoutLevel.chain.map(loadLayout)),\r\n hydrationClientModuleUrl,\r\n loadErrorPage,\r\n stylesheetUrls,\r\n cache,\r\n ...httpServerOption,\r\n }),\r\n // `isPage` marks this route as SSR-served. Pages and API routes share one\r\n // router and one route-name namespace, so the router's duplicate-name\r\n // error reads this flag to say which claimant is the page.\r\n { name, isPage: true },\r\n ),\r\n );\r\n\r\n installed.push({\r\n declaredPath: routePath,\r\n path: effectivePath,\r\n name,\r\n file: pageFile,\r\n layoutFile,\r\n });\r\n }\r\n\r\n /*\r\n THE CATCH-ALL, registered LAST and only when this application has a page\r\n surface at all. \"Configured with web, no pages yet\" is a legal state, and an\r\n application serving no pages has no page 404 to answer with — its unmatched\r\n URLs stay core's to answer, exactly as they are today.\r\n\r\n Registered even when the application ships no `404.page.tsx`: the framework\r\n default still answers 404, so an application that has not written one yet\r\n gets the right STATUS from the first request, and adding the file later\r\n changes the body and nothing else.\r\n */\r\n if (pageFiles.length > 0 || notFoundPageFiles.length > 0) {\r\n const notFoundPageFile = notFoundPageFiles[0]?.pageFile;\r\n\r\n // Read at INSTALL time, so a `route` export on the not-found page is\r\n // refused at boot with everything else — not on the first request that\r\n // misses, which is the one request nobody is watching.\r\n if (notFoundPageFile !== undefined) {\r\n const notFoundModule = (await vite.ssrLoadModule(notFoundPageFile)) as PageModuleShape;\r\n\r\n if (notFoundModule.route !== undefined) {\r\n throw new NotFoundPageDeclaresRouteError(notFoundPageFile);\r\n }\r\n }\r\n\r\n const registerNotFoundRoute = () =>\r\n router.get(\r\n NOT_FOUND_ROUTE_PATH,\r\n createNotFoundRouteHandler({\r\n renderPage:\r\n notFoundPageFile === undefined\r\n ? undefined\r\n : createPageRouteHandler({\r\n path: NOT_FOUND_ROUTE_PATH,\r\n name: NOT_FOUND_ROUTE_NAME,\r\n appFile,\r\n pageFile: notFoundPageFile,\r\n // NO LAYOUT, deliberately, and it is the same trade as \"no\r\n // loader on the 404 page\": a layout brings its whole chain's\r\n // middleware with it, and a guard that redirects or throws on\r\n // the not-found path turns a missing page into an incident. The\r\n // page renders inside the application root and nothing else.\r\n layoutFile: undefined,\r\n loadModule: (moduleId) => vite.ssrLoadModule(moduleId),\r\n hydrationClientModuleUrl,\r\n loadErrorPage,\r\n // NO LAYOUT means no layout CSS either — just root and the\r\n // not-found page's own stylesheets, same reasoning as above.\r\n stylesheetUrls: devHandlerStylesheetUrls(stylesheetRoot, [\r\n appFile,\r\n notFoundPageFile,\r\n ]),\r\n // The URL that missed IS this route's pattern for this request.\r\n matchPath: (requestPath) => requestPath,\r\n statusForRenderedOk: 404,\r\n skipPageLoader: true,\r\n ...httpServerOption,\r\n }),\r\n }),\r\n // `isPage` for the same reason every other page route carries it: the\r\n // router's duplicate-name error reads the flag to say which claimant is\r\n // the page.\r\n { name: NOT_FOUND_ROUTE_NAME, isPage: true },\r\n );\r\n\r\n if (notFoundPageFile === undefined) {\r\n await router.withSourceFile(FRAMEWORK_DEFAULT_NOT_FOUND_SOURCE_FILE, registerNotFoundRoute);\r\n } else {\r\n await router.withSourceFile(\r\n canonicalSourceFileFor(notFoundPageFile, appSrcRoot),\r\n registerNotFoundRoute,\r\n );\r\n }\r\n }\r\n\r\n /*\r\n Published from the SAME loop that registered the routes, so `href()` and the\r\n router cannot disagree about where a name points. It happens here rather\r\n than in the caller because a caller that forgets leaves every `<Link>` on\r\n the server throwing at render — and dev republishes on every restart, which\r\n is why the table replaces wholesale instead of merging: a deleted page's\r\n name has to stop resolving.\r\n */\r\n publishRouteTable(installed, \"installPageRoutes (dev)\");\r\n\r\n return installed;\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"install-page-routes.mjs","names":["resolveComposedLayoutLevel"],"sources":["../../../../../../../web/src/server/install-page-routes.ts"],"sourcesContent":["/**\r\n * Registers every page {@link discoverPageFiles} finds under `<appSrcRoot>`\r\n * into Warlock's router (`router.get`, `core/src/router/router.ts:359-361`)\r\n * so `router.scanDevServer(fastify)` —\r\n * the sanctioned dev-server dispatch path (server matching is Warlock's\r\n * router; there is no second server matcher) — picks\r\n * it up. Replaces the two hand-rolled `fastify.get()` calls this file's\r\n * sibling, `dev-error-transport.ts`, used to make directly.\r\n *\r\n * DELIBERATE EXCEPTION to \"web has no core dependency\", same\r\n * reasoning `dev-error-transport.ts`'s own header comment records: this module is not\r\n * exported from either package barrel and is not part of `web/package.json`'s\r\n * dependency graph — dev/CLI bootstrap only.\r\n *\r\n * Scope note: a page's\r\n * `route.path` is now composed with the `prefix` export of EVERY `layout.tsx`\r\n * on its path — outermost first (`composeRoutePath` below) — before\r\n * registration and before the collision check, so `home.page.tsx`\r\n * (`path: \"/\"`, main layout `prefix: \"/\"`) resolves to `/` and\r\n * `products.page.tsx` (`path: \"/\"`, products layout `prefix: \"/products\"`)\r\n * resolves to `/products` — no collision. A page with no `layout.tsx` on its\r\n * path composes against the implicit root prefix `\"/\"` (e.g. `/contact-us`,\r\n * `/hydration-demo`, both unaffected by composition).\r\n *\r\n * WHICH PAGES EXIST is answered by {@link discoverPageFiles}\r\n * (`web/src/build/discover-pages.ts`) — the same walk production's build\r\n * shares — so this file owns no directory-walking of its own; it serves the\r\n * page root (`<appSrcRoot>/web/**`) exactly as discovery enumerates it. WHAT\r\n * ROUTE A PAGE ANSWERS ON stays this file's own job: each page and its nearest layout are still evaluated\r\n * through Vite (`vite.ssrLoadModule`), never read statically, because a dev\r\n * page module must be the one Vite serves, warm cache and all.\r\n */\r\nimport path from \"node:path\";\r\nimport type { ViteDevServer } from \"vite\";\r\nimport {\r\n discoverPageFiles,\r\n ErrorPageDeclaresRouteError,\r\n isErrorPageFile,\r\n layoutChainFor,\r\n} from \"../build/discover-pages\";\r\nimport { NonLiteralRouteExportError, readRouteExports } from \"../build/read-route-exports\";\r\nimport { composeRoutePath } from \"../routing/compose-route-path\";\r\nimport { duplicateRoutePathMessage } from \"../routing/duplicate-route-path\";\r\nimport { deriveFilesystemRoutePath } from \"../routing/filesystem-route\";\r\nimport { resolveLayoutLevel as resolveComposedLayoutLevel } from \"../routing/layout-level\";\r\nimport { PageFileSegmentNotSupportedError } from \"../routing/page-file-segment\";\r\nimport { toPosix } from \"../shared/to-posix\";\r\nimport {\r\n resolvePageRouteCache,\r\n resolvePageRouteIdentity,\r\n resolvePageRouteName,\r\n} from \"../routing/route-identity\";\r\nimport { publishRouteTable } from \"../routing/route-table\";\r\nimport { type FastifyInstance, type Router } from \"@warlock.js/core\";\r\nimport { createPageRouteHandler } from \"./create-page-route-handler\";\r\nimport type { ErrorPageModule } from \"./error-page\";\r\nimport type { PipelineLoader, PipelineMiddleware } from \"./execute-page-request\";\r\nimport { foldLayoutLoaders } from \"./fold-layout-loaders\";\r\nimport { devHandlerStylesheetUrls } from \"./stylesheet-urls\";\r\nimport {\r\n DuplicateNotFoundPageError,\r\n isNotFoundPageFile,\r\n NotFoundPageDeclaresRouteError,\r\n NOT_FOUND_ROUTE_NAME,\r\n NOT_FOUND_ROUTE_PATH,\r\n registerNotFoundPageRoute,\r\n} from \"./not-found-page\";\r\nimport type { LayoutModuleShape, PageModuleShape, PageRouteExport } from \"./page-module-shapes\";\r\n\r\nexport type { LayoutModuleShape, PageModuleShape, PageRouteExport } from \"./page-module-shapes\";\r\n\r\n/** Re-exported so `web/src/server/index.ts`'s existing barrel export keeps resolving. */\r\nexport { composeRoutePath };\r\n\r\n/** Raised at boot when a page still uses the withdrawn `route.middleware` export. */\r\nexport class RouteMiddlewareRemovedError extends Error {\r\n public constructor(public readonly pageFile: string) {\r\n super(\r\n `\"${pageFile}\" declares \\`route.middleware\\`, which no longer runs — it was withdrawn after 5.6.0. ` +\r\n \"Move it to the page's own top-level `middleware` export instead: `export const middleware = [...]`.\",\r\n );\r\n this.name = \"RouteMiddlewareRemovedError\";\r\n }\r\n}\r\n\r\n/** Raised at boot when a page still uses the withdrawn `route.validate` export. */\r\nexport class RouteValidationRemovedError extends Error {\r\n public constructor(public readonly pageFile: string) {\r\n super(\r\n `\"${pageFile}\" declares \\`route.validate\\`, which no longer runs — it was withdrawn after 5.6.0. ` +\r\n \"Move it to the page's top-level `validation` export instead: `export const validation = { params: ..., query: ... }`.\",\r\n );\r\n this.name = \"RouteValidationRemovedError\";\r\n }\r\n}\r\n\r\nexport type InstalledPageRoute = {\r\n /** The canonical declared route path, before layout-prefix composition. */\r\n declaredPath: string;\r\n path: string;\r\n name: string;\r\n file: string;\r\n layoutFile: string | undefined;\r\n};\r\n\r\n/**\r\n * Ownership key for the framework's fallback 404 route. A NUL-prefixed value\r\n * cannot be a real filesystem path, so it cannot collide with an app page's\r\n * canonical source-file key.\r\n */\r\nexport const FRAMEWORK_DEFAULT_NOT_FOUND_SOURCE_FILE = \"\\0warlock:framework-default-404\";\r\n\r\n/**\r\n * The page's application-source-relative POSIX source path used as the router's\r\n * stable ownership key. `appSrcRoot`'s own basename preserves the existing\r\n * `src/web/...` source-file convention.\r\n */\r\nfunction canonicalSourceFileFor(pageFile: string, appSrcRoot: string): string {\r\n return `${path.basename(appSrcRoot)}/${toPosix(path.relative(appSrcRoot, pageFile))}`;\r\n}\r\n\r\nexport function filesystemPageFileFor(pageFile: string, appSrcRoot: string): string {\r\n return toPosix(path.relative(path.join(appSrcRoot, \"web\"), pageFile));\r\n}\r\n\r\n/**\r\n * Raised when a page's own module fails to load in dev (`vite.ssrLoadModule`\r\n * rejects on the page file itself — never a layout, which this class does not\r\n * cover).\r\n *\r\n * Before this class existed, that rejection propagated straight out of\r\n * `installPageRoutes`'s loop and failed the WHOLE install: every other page's\r\n * route went unregistered along with the broken one, and the failure named\r\n * neither the page file nor which module actually threw. One bad page taking\r\n * every other page down with it is a worse outage than the bad page alone, so\r\n * this error is what the broken page's own route now fails with instead —\r\n * named, with its cause attached, while the rest of the application keeps\r\n * serving.\r\n */\r\nexport class PageModuleLoadError extends Error {\r\n public constructor(\r\n public readonly pageFile: string,\r\n cause: unknown,\r\n ) {\r\n const rawCause = cause instanceof Error ? cause.message : String(cause);\r\n // The cause is someone else's text — a bundler's, a module resolver's — so\r\n // it may or may not end in punctuation. Without this it runs straight into\r\n // the next sentence, and the seam is exactly where a reader stops trusting\r\n // the message.\r\n const causeMessage = /[.!?]$/.test(rawCause.trim()) ? rawCause.trim() : `${rawCause.trim()}.`;\r\n\r\n super(\r\n `\"${pageFile}\" failed to load: ${causeMessage} Every other page still installed and is ` +\r\n \"still serving; fix the error in this page's module and it will start serving again.\",\r\n { cause },\r\n );\r\n this.name = \"PageModuleLoadError\";\r\n }\r\n}\r\n\r\n/**\r\n * Registers a page whose OWN module failed to load at its filesystem-derived\r\n * URL, so a request there reports {@link PageModuleLoadError} — naming the\r\n * page file and the underlying cause — instead of a bare 404 that explains\r\n * nothing.\r\n *\r\n * Only the filesystem-derived path is attempted: the page's `route` export\r\n * cannot be read (that requires the very module that failed to load), and its\r\n * layout chain is not resolved either, so no layout prefix composes into this\r\n * path. When the filesystem path itself is not derivable\r\n * (`deriveFilesystemRoutePath` rejects a segment), there is no path left to\r\n * register a route at, so nothing is registered — the caller reports both\r\n * failures loudly at boot and moves on.\r\n */\r\nasync function registerFailedPageRoute(input: {\r\n router: Router;\r\n pageFile: string;\r\n appSrcRoot: string;\r\n loadError: unknown;\r\n fileByPath: Map<string, string>;\r\n}): Promise<void> {\r\n const { router, pageFile, appSrcRoot, loadError, fileByPath } = input;\r\n const attributed = new PageModuleLoadError(pageFile, loadError);\r\n const filesystemPageFile = filesystemPageFileFor(pageFile, appSrcRoot);\r\n\r\n let effectivePath: string;\r\n\r\n try {\r\n effectivePath = deriveFilesystemRoutePath({ pageFile: filesystemPageFile });\r\n } catch (segmentError) {\r\n if (!(segmentError instanceof PageFileSegmentNotSupportedError)) throw segmentError;\r\n\r\n // No filesystem path to register a route at, so this page cannot answer\r\n // its own URL with the attributed error — the request that would have hit\r\n // it 404s instead, unregistered, and the existing unregistered-page\r\n // reporter (`web/src/server/unregistered-pages.ts`) is what explains that\r\n // 404 to whoever is looking. Reported loudly here so the underlying load\r\n // failure is not lost as well.\r\n console.error(attributed.message);\r\n console.error(segmentError.message);\r\n\r\n return;\r\n }\r\n\r\n const existingFile = fileByPath.get(effectivePath);\r\n\r\n if (existingFile) {\r\n throw new Error(duplicateRoutePathMessage({ effectivePath, existingFile, newFile: pageFile }));\r\n }\r\n\r\n fileByPath.set(effectivePath, pageFile);\r\n\r\n await router.withSourceFile(canonicalSourceFileFor(pageFile, appSrcRoot), () =>\r\n router.get(\r\n effectivePath,\r\n async () => {\r\n throw attributed;\r\n },\r\n { name: resolvePageRouteName(undefined, filesystemPageFile), isPage: true },\r\n ),\r\n );\r\n}\r\n\r\n/* Moved to page-module-shapes.ts so both installers consume one contract.\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 declaration order.\r\n middleware?: readonly PipelineMiddleware[];\r\n loader?: PipelineLoader;\r\n*/\r\n\r\n/** How this module gets a layout module namespace — `vite.ssrLoadModule`, in practice. */\r\ntype LoadLayout = (layoutFile: string) => Promise<LayoutModuleShape>;\r\n\r\n/**\r\n * The page's layout LEVEL, resolved from its whole chain rather than from the\r\n * one layout nearest to it.\r\n *\r\n * The render pipeline has exactly one layout slot per page\r\n * (`execute-page-request.ts`'s `PageRouteEntry[\"triple\"]`), so the chain has to\r\n * be collapsed into one module before it reaches a handler. Two things collapse\r\n * differently and both matter:\r\n *\r\n * - RENDERING is a selection: at most one layout on the chain may render, and\r\n * the policy picks it. `renders` is read off the loaded module\r\n * (`typeof module.default !== \"undefined\"`), never off the filename — a\r\n * `middleware`-only layout has no default export and is not a wrapper, and\r\n * passing a bare path to the policy would have it read as a rendering one,\r\n * which is the conservative default and the wrong answer here.\r\n * - MIDDLEWARE and PREFIX are compositions: every layout on the path\r\n * contributes, outermost first. A guard on an outer layout that the page's\r\n * own directory knows nothing about is exactly the guard that must still run,\r\n * and a prefix nobody composed is a URL nobody wrote down.\r\n *\r\n * The selection and prefix composition rules themselves do not depend on\r\n * where a layout's module came from, so they are not re-derived here — see\r\n * `../routing/layout-level.ts`'s `resolveLayoutLevel`, which production's\r\n * installer calls against the same rules. This function's own job is\r\n * everything that DOES depend on the source: walking the chain\r\n * (`layoutChainFor`), loading each module (`loadLayout`) and keying declared\r\n * prefixes by filesystem directory for {@link deriveFilesystemRoutePath}.\r\n */\r\ntype LayoutLevel = {\r\n /** Every `layout.tsx` from the web root down to the page's directory, outermost first. */\r\n chain: string[];\r\n /**\r\n * The module id the handler's layout slot is registered under, or `undefined`\r\n * when the page has no layout at all: the layout that RENDERS, or — when none\r\n * does — the nearest one, which is the slot dev has always used and so the\r\n * choice that changes nothing but the middleware for a chain with no wrapper\r\n * in it.\r\n */\r\n layoutFile: string | undefined;\r\n /** Every layout's `prefix`, composed outermost first — `discoverPages`' own reduction. */\r\n prefix: string;\r\n /** Declared prefixes keyed by layout directory relative to this page's web root. */\r\n prefixesByDirectory: Readonly<Record<string, string>>;\r\n};\r\n\r\nasync function resolveLayoutLevel(\r\n pageFile: string,\r\n webRoot: string,\r\n loadLayout: LoadLayout,\r\n): Promise<LayoutLevel> {\r\n const chain = layoutChainFor(pageFile, webRoot);\r\n const modules = await Promise.all(chain.map(loadLayout));\r\n const level = resolveComposedLayoutLevel(\r\n pageFile,\r\n chain.map((layoutFile, index) => ({\r\n id: layoutFile,\r\n renders: typeof modules[index].default !== \"undefined\",\r\n prefix: modules[index].prefix,\r\n })),\r\n );\r\n\r\n return {\r\n chain,\r\n layoutFile: level.hostId,\r\n prefix: level.prefix,\r\n prefixesByDirectory: Object.fromEntries(\r\n chain.flatMap((layoutFile, index) => {\r\n const prefix = modules[index].prefix;\r\n\r\n return prefix === undefined\r\n ? []\r\n : [[toPosix(path.relative(webRoot, path.dirname(layoutFile))), prefix]];\r\n }),\r\n ),\r\n };\r\n}\r\n\r\n/**\r\n * The layout slot's module for ONE request: the slot host's own namespace, with\r\n * the whole chain's middleware in place of its own — outermost first, which is\r\n * the order stage 3 runs the array in (`execute-page-request.ts:519-524`) and\r\n * the order an outer `optionalAuth` needs in order to have resolved an identity\r\n * before an inner `gate()` checks it.\r\n *\r\n * Loaded per call, not once at install time: a dev layout module must be the\r\n * one Vite is currently serving, edits and all.\r\n */\r\nasync function composeLayoutLevel(\r\n level: LayoutLevel & { layoutFile: string },\r\n loadLayout: LoadLayout,\r\n): Promise<LayoutModuleShape> {\r\n const modules = await Promise.all(level.chain.map(loadLayout));\r\n const hostIndex = level.chain.indexOf(level.layoutFile);\r\n const host = modules[hostIndex];\r\n\r\n return {\r\n ...host,\r\n middleware: modules.flatMap((layoutModule) => [...(layoutModule.middleware ?? [])]),\r\n loader: foldLayoutLoaders(\r\n modules.map((layoutModule) => layoutModule.loader),\r\n hostIndex,\r\n ),\r\n };\r\n}\r\n\r\nexport type InstallPageRoutesOptions = {\r\n router: Router;\r\n vite: ViteDevServer;\r\n /** v5/app/src — pages live under \"<appSrcRoot>/web/**\". */\r\n appSrcRoot: string;\r\n /** v5/app/src/web/root.tsx — the single global app-root file. */\r\n appFile: string;\r\n /**\r\n * The application root Vite's dev server serves from — `dev-error-transport.ts`'s\r\n * `paths.appRoot`, i.e. `<appRoot>/src === appSrcRoot` by default. Every\r\n * handler's stylesheet URLs are expressed relative to THIS, because that is\r\n * the root Vite's dev server actually resolves `/…` URLs against\r\n * (`stylesheet-urls.ts`'s `devStylesheetUrls`) — not `appSrcRoot`, which is\r\n * one directory level in.\r\n *\r\n * OPTIONAL and defaulted to `path.dirname(appSrcRoot)`: the caller that\r\n * wires dev boot (`web-connector.ts`) does not pass this field today, and\r\n * that default is exactly the relationship it constructs `appSrcRoot` from\r\n * (`appSrcRoot = path.join(appRoot, \"src\")`) — correct for every actual\r\n * deployment, and overridable by a caller with a non-default layout.\r\n */\r\n appRoot?: string;\r\n /** Browser module loaded after the server-rendered application and payload. */\r\n hydrationClientModuleUrl?: string;\r\n /**\r\n * UNUSED. Retained on this type only because `web-connector.ts` still builds\r\n * an options object naming it (`devStylesheetUrls(paths.appRoot,\r\n * paths.appFile)`, computed once for the whole application). Each handler\r\n * now computes its OWN stylesheet chain — `[root, ...outer-to-inner matched\r\n * layouts, page]`, via `devHandlerStylesheetUrls` — inside the registration\r\n * loop below, because a single application-wide list cannot express \"this\r\n * page's own CSS\" without also carrying every other page's.\r\n */\r\n stylesheetUrls?: readonly string[];\r\n /**\r\n * The Fastify instance `HttpConnector.boot()` published, resolved by the\r\n * caller on the NODE side (`web-connector.ts`) and forwarded to every\r\n * `createPageRouteHandler` call below instead of letting that factory read\r\n * `container.get(\"http.server\")` for itself.\r\n *\r\n * Load-bearing, not a convenience: Vite's SSR module runner\r\n * (`vite.ssrLoadModule`, used throughout this file) evaluates\r\n * `create-page-route-handler.ts` as a SECOND copy of `@warlock.js/core`, with\r\n * its own `container` that `HttpConnector.boot()` never wrote to. Reading the\r\n * container from inside that SSR graph therefore always misses, however\r\n * early or late this file calls it — the value has to arrive as a plain\r\n * argument from a caller on the Node side, where the real container lives.\r\n *\r\n * OPTIONAL so existing callers (and this file's own unit tests, which seed\r\n * `container.set(\"http.server\", …)` instead) keep resolving through the\r\n * container exactly as before — omitted here means \"not supplied\", the same\r\n * distinction `createPageRouteHandler` itself draws from `PageRouteHandlerOptions.httpServer`.\r\n */\r\n httpServer?: FastifyInstance;\r\n};\r\n\r\n/**\r\n * Registers every discoverable page into `options.router`. Throws\r\n * IMMEDIATELY, naming both files, the moment two pages declare the same\r\n * `route.path` — a registration-time failure, not a runtime 404 one of them\r\n * silently loses.\r\n *\r\n * Pages with no `route` export derive their path and name from their location\r\n * below `src/web`, using the same pure filesystem-routing helper as the build.\r\n */\r\nexport async function installPageRoutes(\r\n options: InstallPageRoutesOptions,\r\n): Promise<InstalledPageRoute[]> {\r\n const { router, vite, appSrcRoot, appFile, hydrationClientModuleUrl, httpServer } = options;\r\n // Spread conditionally, never as a bare `httpServer,` property: an explicit\r\n // `httpServer: undefined` key is its own signal to `createPageRouteHandler`\r\n // (`\"httpServer\" in options\"`, `create-page-route-handler.ts`) — \"no server,\r\n // on purpose\" — and this file must not manufacture that signal on behalf of\r\n // a caller (this file's own unit tests, most callers) that never supplied\r\n // one and means to fall back to the container instead.\r\n const httpServerOption = httpServer === undefined ? {} : { httpServer };\r\n // See `InstallPageRoutesOptions.appRoot` for why this default, not\r\n // `appSrcRoot` itself, is the root every handler's CSS is resolved against.\r\n const stylesheetRoot = options.appRoot ?? path.dirname(appSrcRoot);\r\n const discovered = [...discoverPageFiles(appSrcRoot)].sort((left, right) =>\r\n left.pageFile < right.pageFile ? -1 : left.pageFile > right.pageFile ? 1 : 0,\r\n );\r\n\r\n // THE NOT-FOUND PAGE IS TAKEN OUT OF THE ORDINARY LOOP, not filtered inside\r\n // it. It has no `route` export to read, no path to compose and no collision\r\n // to check — every step below is about a page with a URL, and `404.page.tsx`\r\n // does not have one. Registering it here would put it at `/404`, which is not\r\n // a page anybody asked to be able to visit.\r\n const errorPageFiles = discovered.filter((page) => isErrorPageFile(page.pageFile));\r\n const notFoundPageFiles = discovered.filter((page) => isNotFoundPageFile(page.pageFile));\r\n const pageFiles = discovered.filter(\r\n (page) => !isNotFoundPageFile(page.pageFile) && !isErrorPageFile(page.pageFile),\r\n );\r\n\r\n if (errorPageFiles.length > 1) {\r\n throw new Error(\r\n `Two error pages were found: ${errorPageFiles.map((page) => page.pageFile).join(\", \")}.`,\r\n );\r\n }\r\n\r\n const errorPageFile = errorPageFiles[0]?.pageFile;\r\n\r\n // Parse only: the error boundary must remain lazy until a request actually\r\n // fails, while a route export is still rejected at install time.\r\n if (errorPageFile !== undefined) {\r\n const declarations = readRouteExports(errorPageFile);\r\n if (!declarations.ok) throw new NonLiteralRouteExportError(declarations.rejection);\r\n if (declarations.route !== undefined) throw new ErrorPageDeclaresRouteError(errorPageFile);\r\n }\r\n const loadErrorPage =\r\n errorPageFile === undefined\r\n ? undefined\r\n : () => vite.ssrLoadModule(errorPageFile) as Promise<ErrorPageModule>;\r\n\r\n if (notFoundPageFiles.length > 1) {\r\n throw new DuplicateNotFoundPageError(notFoundPageFiles.map((page) => page.pageFile));\r\n }\r\n\r\n const installed: InstalledPageRoute[] = [];\r\n const fileByPath = new Map<string, string>();\r\n\r\n for (const { pageFile, webRoot } of pageFiles) {\r\n let pageModule: PageModuleShape;\r\n\r\n try {\r\n pageModule = (await vite.ssrLoadModule(pageFile)) as PageModuleShape;\r\n } catch (loadError) {\r\n // THE PAGE ITSELF MUST NOT ABORT THE INSTALL: every other page still\r\n // needs to install and serve. Layout loading is deliberately NOT\r\n // wrapped here — a broken layout is a different failure with a\r\n // different blast radius (it can affect more than one page) and is out\r\n // of this card's scope.\r\n await registerFailedPageRoute({ router, pageFile, appSrcRoot, loadError, fileByPath });\r\n\r\n continue;\r\n }\r\n\r\n const route = pageModule.route as unknown;\r\n\r\n if (typeof route === \"object\" && route !== null) {\r\n if (\"middleware\" in route) throw new RouteMiddlewareRemovedError(pageFile);\r\n if (\"validate\" in route) throw new RouteValidationRemovedError(pageFile);\r\n }\r\n\r\n const sourceFile = canonicalSourceFileFor(pageFile, appSrcRoot);\r\n\r\n // Route identity is explicit when declared and filesystem-derived otherwise.\r\n const { path: routePath, name } = resolvePageRouteIdentity(\r\n pageModule.route,\r\n filesystemPageFileFor(pageFile, appSrcRoot),\r\n pageFile,\r\n );\r\n\r\n // Validated at INSTALL time, with everything else — a malformed `cache`\r\n // opt-in fails boot, not the first request that would have served it.\r\n const cache = resolvePageRouteCache(pageModule.route, pageFile);\r\n\r\n const loadLayout: LoadLayout = (layoutFile) =>\r\n vite.ssrLoadModule(layoutFile) as Promise<LayoutModuleShape>;\r\n const layoutLevel = await resolveLayoutLevel(pageFile, webRoot, loadLayout);\r\n const { layoutFile, prefix: layoutPrefix } = layoutLevel;\r\n\r\n const effectivePath =\r\n pageModule.route === undefined\r\n ? deriveFilesystemRoutePath({\r\n pageFile: filesystemPageFileFor(pageFile, appSrcRoot),\r\n layoutPrefixes: layoutLevel.prefixesByDirectory,\r\n })\r\n : composeRoutePath(layoutPrefix, routePath);\r\n\r\n const existingFile = fileByPath.get(effectivePath);\r\n\r\n if (existingFile) {\r\n throw new Error(\r\n duplicateRoutePathMessage({\r\n effectivePath,\r\n existingFile,\r\n newFile: pageFile,\r\n composition: { layoutPrefix, routePath },\r\n }),\r\n );\r\n }\r\n\r\n fileByPath.set(effectivePath, pageFile);\r\n\r\n // Every registered handler gets ITS OWN immutable, ordered, deduped CSS\r\n // chain: root, then every matched layout outer to inner\r\n // (`layoutLevel.chain`), then the page — the same order the render\r\n // pipeline loads that chain in, so cascade order matches load order.\r\n // Computed once here, at registration, not per request: dev re-registers\r\n // on every restart, so a stale chain cannot outlive the source edit that\r\n // changed it.\r\n const stylesheetUrls = devHandlerStylesheetUrls(\r\n stylesheetRoot,\r\n [appFile, ...layoutLevel.chain, pageFile],\r\n vite.moduleGraph,\r\n );\r\n\r\n await router.withSourceFile(sourceFile, () =>\r\n router.get(\r\n effectivePath,\r\n // The handler itself is `createPageRouteHandler`\r\n // (`web/src/server/create-page-route-handler.ts`) — a named seam a\r\n // future `type: \"page\"` route can bind to, and testable without a Vite\r\n // server. Vite appears here only as the dev answer to \"how do I load a\r\n // module\"; the handler takes that as an input and knows nothing else\r\n // about it.\r\n createPageRouteHandler({\r\n path: effectivePath,\r\n name,\r\n appFile,\r\n pageFile,\r\n layoutFile,\r\n // The layout slot's id resolves to the COMPOSED level — every layout's\r\n // middleware, in chain order — and every other id goes straight to\r\n // Vite. A one-layout chain has nothing to compose, so it is left to\r\n // resolve as the exact module Vite hands back, untouched.\r\n loadModule:\r\n layoutLevel.chain.length > 1 && layoutFile !== undefined\r\n ? (moduleId) =>\r\n moduleId === layoutFile\r\n ? composeLayoutLevel({ ...layoutLevel, layoutFile }, loadLayout)\r\n : vite.ssrLoadModule(moduleId)\r\n : (moduleId) => vite.ssrLoadModule(moduleId),\r\n // Registration tracks real module namespaces, not the composed\r\n // layout wrapper above. Loading the raw chain per request also lets\r\n // Vite hand over a replacement namespace after an HMR update; the\r\n // helper's WeakSet then gives that new identity its one invocation.\r\n loadRegistrationLayouts: () => Promise.all(layoutLevel.chain.map(loadLayout)),\r\n hydrationClientModuleUrl,\r\n loadErrorPage,\r\n stylesheetUrls,\r\n cache,\r\n ...httpServerOption,\r\n }),\r\n // `isPage` marks this route as SSR-served. Pages and API routes share one\r\n // router and one route-name namespace, so the router's duplicate-name\r\n // error reads this flag to say which claimant is the page.\r\n { name, isPage: true },\r\n ),\r\n );\r\n\r\n installed.push({\r\n declaredPath: routePath,\r\n path: effectivePath,\r\n name,\r\n file: pageFile,\r\n layoutFile,\r\n });\r\n }\r\n\r\n /*\r\n THE CATCH-ALL, registered LAST and only when this application has a page\r\n surface at all. \"Configured with web, no pages yet\" is a legal state, and an\r\n application serving no pages has no page 404 to answer with — its unmatched\r\n URLs stay core's to answer, exactly as they are today.\r\n\r\n Registered even when the application ships no `404.page.tsx`: the framework\r\n default still answers 404, so an application that has not written one yet\r\n gets the right STATUS from the first request, and adding the file later\r\n changes the body and nothing else.\r\n */\r\n if (pageFiles.length > 0 || notFoundPageFiles.length > 0) {\r\n const notFoundPageFile = notFoundPageFiles[0]?.pageFile;\r\n\r\n // Read at INSTALL time, so a `route` export on the not-found page is\r\n // refused at boot with everything else — not on the first request that\r\n // misses, which is the one request nobody is watching.\r\n if (notFoundPageFile !== undefined) {\r\n const notFoundModule = (await vite.ssrLoadModule(notFoundPageFile)) as PageModuleShape;\r\n\r\n if (notFoundModule.route !== undefined) {\r\n throw new NotFoundPageDeclaresRouteError(notFoundPageFile);\r\n }\r\n }\r\n\r\n const registerNotFoundRoute = () =>\r\n registerNotFoundPageRoute({\r\n router,\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(\r\n stylesheetRoot,\r\n [appFile, notFoundPageFile],\r\n vite.moduleGraph,\r\n ),\r\n // The URL that missed IS this route's pattern for this request.\r\n matchPath: (requestPath) => requestPath,\r\n statusForRenderedOk: 404,\r\n skipPageLoader: true,\r\n ...httpServerOption,\r\n }),\r\n });\r\n\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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"}
@@ -2,17 +2,13 @@
2
2
  /**
3
3
  * Stage 1 — turn a URL into a route entry plus its params.
4
4
  *
5
- * ⚠ **This is a SECOND matcher, and on the HTTP path it is redundant.** Core's
6
- * router has already matched by the time a page handler runs, and
7
- * `create-page-route-handler.ts` ignores the match it is handed. The one caller
8
- * that genuinely needs this is `renderPage(name, options)`, which synthesizes a
9
- * URL with no HTTP request behind it — and that path is not wired up
10
- * (`connectPageRoutes()` is never called).
5
+ * This matches only URL-driven render requests that have no HTTP router result
6
+ * to carry through. On the live HTTP path core has already selected the route
7
+ * and decoded its params; `create-page-route-handler.ts` passes that match into
8
+ * the pipeline rather than re-running this matcher.
11
9
  *
12
- * Removing it from the HTTP path is carded. Two things must be proven first:
13
- * that these params agree with core's, since `bundle.route.params` reaches the
14
- * hydration payload; and that dropping `bundle.route.query` — a public type
15
- * member — is announced rather than slipped in.
10
+ * `match-page-route.parity.spec.ts` keeps the param contract gated, including
11
+ * a multi-segment URL and the param-free page catch-all convention.
16
12
  */
17
13
  function splitSegments(path) {
18
14
  return path.split("/").filter((segment) => segment.length > 0);
@@ -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\";\r\n\r\n/**\r\n * Stage 1 — turn a URL into a route entry plus its params.\r\n *\r\n * ⚠ **This is a SECOND matcher, and on the HTTP path it is redundant.** Core's\r\n * router has already matched by the time a page handler runs, and\r\n * `create-page-route-handler.ts` ignores the match it is handed. The one caller\r\n * that genuinely needs this is `renderPage(name, options)`, which synthesizes a\r\n * URL with no HTTP request behind it — and that path is not wired up\r\n * (`connectPageRoutes()` is never called).\r\n *\r\n * Removing it from the HTTP path is carded. Two things must be proven first:\r\n * that these params agree with core's, since `bundle.route.params` reaches the\r\n * hydration payload; and that dropping `bundle.route.query` — a public type\r\n * member is announced rather than slipped in.\r\n */\r\n\r\nfunction splitSegments(path: string): string[] {\r\n return path.split(\"/\").filter((segment) => segment.length > 0);\r\n}\r\n\r\nexport function matchPath(pattern: string, pathname: string): Record<string, string> | undefined {\r\n const patternSegments = splitSegments(pattern);\r\n const pathSegments = splitSegments(pathname);\r\n\r\n if (patternSegments.length !== pathSegments.length) return undefined;\r\n\r\n const params: Record<string, string> = {};\r\n\r\n for (let index = 0; index < patternSegments.length; index++) {\r\n const patternSegment = patternSegments[index];\r\n const pathSegment = pathSegments[index];\r\n\r\n if (patternSegment.startsWith(\":\")) {\r\n params[patternSegment.slice(1)] = decodeURIComponent(pathSegment);\r\n continue;\r\n }\r\n\r\n if (patternSegment !== pathSegment) return undefined;\r\n }\r\n\r\n return params;\r\n}\r\n\r\nexport function matchRoute(\r\n pathname: string,\r\n routes: readonly PageRouteEntry[],\r\n): { entry: PageRouteEntry; params: Record<string, string> } | undefined {\r\n for (const entry of routes) {\r\n const params = matchPath(entry.path, pathname);\r\n\r\n if (params) return { entry, params };\r\n }\r\n\r\n return undefined;\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;AAkBA,SAAS,cAAc,MAAwB;CAC7C,OAAO,KAAK,MAAM,GAAG,CAAC,CAAC,QAAQ,YAAY,QAAQ,SAAS,CAAC;AAC/D;AAEA,SAAgB,UAAU,SAAiB,UAAsD;CAC/F,MAAM,kBAAkB,cAAc,OAAO;CAC7C,MAAM,eAAe,cAAc,QAAQ;CAE3C,IAAI,gBAAgB,WAAW,aAAa,QAAQ,OAAO;CAE3D,MAAM,SAAiC,CAAC;CAExC,KAAK,IAAI,QAAQ,GAAG,QAAQ,gBAAgB,QAAQ,SAAS;EAC3D,MAAM,iBAAiB,gBAAgB;EACvC,MAAM,cAAc,aAAa;EAEjC,IAAI,eAAe,WAAW,GAAG,GAAG;GAClC,OAAO,eAAe,MAAM,CAAC,KAAK,mBAAmB,WAAW;GAChE;EACF;EAEA,IAAI,mBAAmB,aAAa,OAAO;CAC7C;CAEA,OAAO;AACT;AAEA,SAAgB,WACd,UACA,QACuE;CACvE,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,SAAS,UAAU,MAAM,MAAM,QAAQ;EAE7C,IAAI,QAAQ,OAAO;GAAE;GAAO;EAAO;CACrC;AAGF"}
1
+ {"version":3,"file":"match-page-route.mjs","names":[],"sources":["../../../../../../../web/src/server/match-page-route.ts"],"sourcesContent":["import type { PageRouteEntry } from \"./execute-page-request.types\";\r\n\r\n/**\r\n * Stage 1 — turn a URL into a route entry plus its params.\r\n *\r\n * This matches only URL-driven render requests that have no HTTP router result\r\n * to carry through. On the live HTTP path core has already selected the route\r\n * and decoded its params; `create-page-route-handler.ts` passes that match into\r\n * the pipeline rather than re-running this matcher.\r\n *\r\n * `match-page-route.parity.spec.ts` keeps the param contract gated, including\r\n * a multi-segment URL and the param-free page catch-all convention.\r\n */\r\n\r\nfunction splitSegments(path: string): string[] {\r\n return path.split(\"/\").filter((segment) => segment.length > 0);\r\n}\r\n\r\nexport function matchPath(pattern: string, pathname: string): Record<string, string> | undefined {\r\n const patternSegments = splitSegments(pattern);\r\n const pathSegments = splitSegments(pathname);\r\n\r\n if (patternSegments.length !== pathSegments.length) return undefined;\r\n\r\n const params: Record<string, string> = {};\r\n\r\n for (let index = 0; index < patternSegments.length; index++) {\r\n const patternSegment = patternSegments[index];\r\n const pathSegment = pathSegments[index];\r\n\r\n if (patternSegment.startsWith(\":\")) {\r\n params[patternSegment.slice(1)] = decodeURIComponent(pathSegment);\r\n continue;\r\n }\r\n\r\n if (patternSegment !== pathSegment) return undefined;\r\n }\r\n\r\n return params;\r\n}\r\n\r\nexport function matchRoute(\r\n pathname: string,\r\n routes: readonly PageRouteEntry[],\r\n): { entry: PageRouteEntry; params: Record<string, string> } | undefined {\r\n for (const entry of routes) {\r\n const params = matchPath(entry.path, pathname);\r\n\r\n if (params) return { entry, params };\r\n }\r\n\r\n return undefined;\r\n}\r\n"],"mappings":";;;;;;;;;;;;AAcA,SAAS,cAAc,MAAwB;CAC7C,OAAO,KAAK,MAAM,GAAG,CAAC,CAAC,QAAQ,YAAY,QAAQ,SAAS,CAAC;AAC/D;AAEA,SAAgB,UAAU,SAAiB,UAAsD;CAC/F,MAAM,kBAAkB,cAAc,OAAO;CAC7C,MAAM,eAAe,cAAc,QAAQ;CAE3C,IAAI,gBAAgB,WAAW,aAAa,QAAQ,OAAO;CAE3D,MAAM,SAAiC,CAAC;CAExC,KAAK,IAAI,QAAQ,GAAG,QAAQ,gBAAgB,QAAQ,SAAS;EAC3D,MAAM,iBAAiB,gBAAgB;EACvC,MAAM,cAAc,aAAa;EAEjC,IAAI,eAAe,WAAW,GAAG,GAAG;GAClC,OAAO,eAAe,MAAM,CAAC,KAAK,mBAAmB,WAAW;GAChE;EACF;EAEA,IAAI,mBAAmB,aAAa,OAAO;CAC7C;CAEA,OAAO;AACT;AAEA,SAAgB,WACd,UACA,QACuE;CACvE,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,SAAS,UAAU,MAAM,MAAM,QAAQ;EAE7C,IAAI,QAAQ,OAAO;GAAE;GAAO;EAAO;CACrC;AAGF"}
@@ -1,4 +1,5 @@
1
1
  import { PageRouteHandler } from "./create-page-route-handler.mjs";
2
+ import { Router } from "@warlock.js/core";
2
3
 
3
4
  //#region ../web/src/server/not-found-page.d.ts
4
5
  /**
@@ -196,7 +196,14 @@ function createNotFoundRouteHandler(options) {
196
196
  return renderPage(context);
197
197
  };
198
198
  }
199
+ /** Registers the invariant catch-all route both page installers share. */
200
+ function registerNotFoundPageRoute(options) {
201
+ options.router.get("*", createNotFoundRouteHandler({ renderPage: options.renderPage }), {
202
+ name: NOT_FOUND_ROUTE_NAME,
203
+ isPage: true
204
+ });
205
+ }
199
206
 
200
207
  //#endregion
201
- export { DuplicateNotFoundPageError, NOT_FOUND_PAGE_FILENAME, NOT_FOUND_ROUTE_NAME, NOT_FOUND_ROUTE_PATH, NotFoundPageDeclaresRouteError, acceptsHtmlExplicitly, classifyUnmatchedRequest, createNotFoundRouteHandler, frameworkDefaultNotFoundDocument, isNotFoundPageFile };
208
+ export { DuplicateNotFoundPageError, NOT_FOUND_PAGE_FILENAME, NOT_FOUND_ROUTE_NAME, NOT_FOUND_ROUTE_PATH, NotFoundPageDeclaresRouteError, acceptsHtmlExplicitly, classifyUnmatchedRequest, createNotFoundRouteHandler, frameworkDefaultNotFoundDocument, isNotFoundPageFile, registerNotFoundPageRoute };
202
209
  //# sourceMappingURL=not-found-page.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"not-found-page.mjs","names":[],"sources":["../../../../../../../web/src/server/not-found-page.ts"],"sourcesContent":["/**\r\n * THE NOT-FOUND PATH — the one route in the application that answers for URLs\r\n * nobody declared.\r\n *\r\n * An application gets it by writing `404.page.tsx` anywhere under a web root.\r\n * The file is named for the status it answers with, not for a concept\r\n * (\"not-found\"), because `404` is the string people actually search for, and\r\n * the `*.page.tsx` suffix is what makes it a page in the first place.\r\n *\r\n * ── THE RULE THIS FILE EXISTS FOR ────────────────────────────────────────────\r\n *\r\n * Pages and API routes share ONE route namespace and ONE router. So a catch-all\r\n * page route sees every unmatched request in the process, including\r\n * `GET /api/uzers` — and if it renders a document for that, a `fetch()` gets\r\n * `<!doctype html>` back and dies inside `response.json()` with a SyntaxError\r\n * pointing at the parser instead of at the typo. That failure is expensive\r\n * precisely because the error names nothing near its cause.\r\n *\r\n * Because pages and API share one namespace there is no path-prefix rule\r\n * available: `/anything` may legitimately be either. The discriminator is\r\n * therefore the `Accept` header, and it is stated as a narrow permission rather\r\n * than a guess — the document is the exception, JSON is the default:\r\n *\r\n * A request renders the not-found PAGE only if\r\n * 1. its method is GET or HEAD, and\r\n * 2. `text/html` appears EXPLICITLY in its `Accept` header.\r\n *\r\n * (1) is not a heuristic about browsers. Pages are registered with\r\n * `router.get` and nothing else — every page route in this codebase is\r\n * installed by `installPageRoutes` / `installPageRoutesFromManifest`, both\r\n * of which call `router.get`. A `POST` therefore cannot have been meant\r\n * for a page, by construction. HEAD rides along because Fastify answers it\r\n * from the GET route.\r\n *\r\n * (2) is EXPLICIT and the word is load-bearing. A wildcard does NOT count:\r\n * `* /*` — what a bare `fetch()` sends — is not a request for a document,\r\n * it is the absence of a preference, and `text/*` claims a family rather\r\n * than the type. Only the literal `text/html` media range, with a non-zero\r\n * `q`, opens the page path. A browser address-bar navigation always sends\r\n * an explicit `text/html`; a `fetch()` that has not asked for one never\r\n * does. So the mistyped `/api/...` in a `fetch()` keeps its JSON body and\r\n * dies at the typo rather than inside `response.json()`.\r\n *\r\n * WHAT THE RULE CANNOT DECIDE, and does not pretend to: a browser navigating to\r\n * a typo'd API URL asks for `text/html`, and so is answered with the document.\r\n * Nothing in that request distinguishes it from a typo'd page URL — same verb,\r\n * same header, same absence of a match — so the rule does not guess. The status\r\n * is 404 either way, which is the part machines read.\r\n *\r\n * ── DEPENDENCY NOTE ──────────────────────────────────────────────────────────\r\n * This module has one runtime asset dependency: the framework fallback's\r\n * stylesheet, read as a plain string constant from\r\n * `./framework-default-not-found-stylesheet` and turned into a data URL at\r\n * render time — never a static-asset import, which the production server\r\n * build refuses to compile (see that module's own comment). `../build/discover-pages`\r\n * imports the filename constant and identity helpers from here so build\r\n * discovery and both installers cannot disagree about what a not-found page\r\n * is. No renderer or application runtime is imported, so that edge cannot\r\n * drag the render pipeline into the build.\r\n */\r\nimport type { HttpContext } from \"@warlock.js/core\";\r\nimport type { PageRouteHandler } from \"./create-page-route-handler\";\r\nimport { buildFrameworkDefaultNotFoundStylesheetUrl } from \"./framework-default-not-found-stylesheet\";\r\n\r\n/**\r\n * The one filename that makes a page THE not-found page.\r\n *\r\n * `404.page.tsx`, not `not-found.page.tsx`: it keeps the `*.page.tsx`\r\n * convention every other page follows, and `404` is the token a developer\r\n * greps for when a URL answers with one.\r\n */\r\nexport const NOT_FOUND_PAGE_FILENAME = \"404.page.tsx\";\r\n\r\n/**\r\n * The path the not-found route is registered on — find-my-way's and Fastify's\r\n * catch-all, and the same literal core's own dev dispatcher registers\r\n * (`core/src/router/router.ts`, `server.route({ url: \"*\" })`).\r\n *\r\n * A catch-all has the LOWEST matching priority in both routers, so every\r\n * declared page and every declared API route still wins on its own path; this\r\n * route is only ever reached because nothing else claimed the URL.\r\n */\r\nexport const NOT_FOUND_ROUTE_PATH = \"*\";\r\n\r\n/**\r\n * The reserved route name the not-found page is registered under.\r\n *\r\n * Namespaced under `warlock.` because it is the framework's route rather than\r\n * the application's, and because the router's name namespace is shared with API\r\n * routes — an application that takes this name gets core's duplicate-name error,\r\n * which is the loud answer, not a silent overwrite.\r\n *\r\n * It is deliberately NOT published into the route table (`href()` / `<Link>`):\r\n * the not-found page has no URL of its own to link to.\r\n */\r\nexport const NOT_FOUND_ROUTE_NAME = \"warlock.not-found\";\r\n\r\n/** True when `sourceFile`'s basename is exactly {@link NOT_FOUND_PAGE_FILENAME}. */\r\nexport function isNotFoundPageFile(sourceFile: string): boolean {\r\n const separator = Math.max(sourceFile.lastIndexOf(\"/\"), sourceFile.lastIndexOf(\"\\\\\"));\r\n\r\n return sourceFile.slice(separator + 1) === NOT_FOUND_PAGE_FILENAME;\r\n}\r\n\r\n/**\r\n * Raised when more than one `404.page.tsx` exists.\r\n *\r\n * There is exactly one not-found route in a process, so a second file is not a\r\n * per-module override — it is two files claiming one route, with the winner\r\n * decided by directory-walk order. Both are named because the fix is to delete\r\n * one and the operator has to know which two are in play.\r\n */\r\nexport class DuplicateNotFoundPageError extends Error {\r\n public constructor(public readonly pageFiles: readonly string[]) {\r\n super(\r\n `Two or more not-found pages were found: ${pageFiles.map((file) => `\"${file}\"`).join(\", \")}. ` +\r\n `An application has exactly one \\`${NOT_FOUND_PAGE_FILENAME}\\` — it answers every ` +\r\n \"unmatched page URL in the process, so a second one would silently never render. \" +\r\n \"Keep one and delete the rest.\",\r\n );\r\n this.name = \"DuplicateNotFoundPageError\";\r\n }\r\n}\r\n\r\n/**\r\n * Raised when `404.page.tsx` declares a `route` export.\r\n *\r\n * The not-found page has no URL of its own — it is reached by NOT matching. A\r\n * `route` export on it reads like a promise that `/404` is browsable, and it is\r\n * not: the installers register this file on the catch-all and nowhere else. So\r\n * the export is refused rather than ignored, because a declaration the framework\r\n * silently drops is worse than one it rejects.\r\n */\r\nexport class NotFoundPageDeclaresRouteError extends Error {\r\n public constructor(public readonly pageFile: string) {\r\n super(\r\n `\"${pageFile}\" is the not-found page but declares a \\`route\\` export. ` +\r\n `\\`${NOT_FOUND_PAGE_FILENAME}\\` has no URL of its own — it answers every page URL that ` +\r\n \"matched nothing, and is never registered at a path of its own. Remove the `route` \" +\r\n \"export; to serve a browsable page at a fixed path, use a normal `*.page.tsx`.\",\r\n );\r\n this.name = \"NotFoundPageDeclaresRouteError\";\r\n }\r\n}\r\n\r\n/** The shape this module reads off a registered route — core's `Route`, narrowed. */\r\nexport type RegisteredRouteShape = {\r\n path: string;\r\n isPage?: boolean;\r\n};\r\n\r\n/**\r\n * The media range the not-found PAGE is gated on. Compared literally: a request\r\n * either named this exact type or it did not.\r\n */\r\nconst HTML_MEDIA_TYPE = \"text/html\";\r\n\r\n/**\r\n * True when `text/html` is named EXPLICITLY in an `Accept` header — the whole\r\n * discriminator, in one predicate.\r\n *\r\n * Wildcards are refused on purpose. `* /*` is what `fetch()` and `curl` send\r\n * when the caller expressed no preference at all, and `text/*` names a family;\r\n * neither is a request for a document, and treating either as one is what makes\r\n * a mistyped `/api/...` answer HTML to a JSON parser.\r\n *\r\n * `q=0` is honoured because it is the header's own way of saying \"not this\r\n * one\" — `Accept: text/html;q=0, application/json` is a client refusing the\r\n * document, and reading it as a request for one would be reading the header\r\n * backwards. Any other `q`, present or absent, counts.\r\n */\r\nexport function acceptsHtmlExplicitly(accept: string | undefined): boolean {\r\n if (!accept) return false;\r\n\r\n for (const entry of accept.split(\",\")) {\r\n const [rawType, ...parameters] = entry.split(\";\");\r\n\r\n if (rawType.trim().toLowerCase() !== HTML_MEDIA_TYPE) continue;\r\n\r\n const quality = parameters\r\n .map((parameter) => parameter.trim().toLowerCase())\r\n .find((parameter) => parameter.startsWith(\"q=\"));\r\n\r\n // A malformed `q` is not a refusal — only an explicit zero is.\r\n if (quality !== undefined && Number.parseFloat(quality.slice(2)) === 0) continue;\r\n\r\n return true;\r\n }\r\n\r\n return false;\r\n}\r\n\r\nexport type UnmatchedRequestKind = \"page\" | \"api\";\r\n\r\n/**\r\n * The rule, in one function — see this file's header for why it is these two\r\n * conditions and why the second one refuses wildcards.\r\n */\r\nexport function classifyUnmatchedRequest(input: {\r\n method: string;\r\n accept: string | undefined;\r\n}): UnmatchedRequestKind {\r\n const method = input.method.toUpperCase();\r\n\r\n // Pages are installed with `router.get`, so only these two verbs can ever\r\n // have been asking for one.\r\n if (method !== \"GET\" && method !== \"HEAD\") return \"api\";\r\n\r\n return acceptsHtmlExplicitly(input.accept) ? \"page\" : \"api\";\r\n}\r\n\r\n/**\r\n * The document served when the application ships no `404.page.tsx`.\r\n *\r\n * Deliberately a STRING, not a React render: it must survive the case where the\r\n * application root, a layout or the page module is exactly what is broken, and\r\n * a default that can itself fail is not a default. Its stylesheet is embedded in\r\n * the document as a data URL, and it carries no hydration script, so serving the\r\n * fallback requires no application render and no follow-up fetch.\r\n *\r\n * It answers 404 like the real page does, because the status is the part that\r\n * search engines, caches and monitoring read; a framework default that soft-404s\r\n * would teach every un-customised application to lie.\r\n */\r\nfunction resolveDefaultNotFoundDocumentLocale(locale: unknown): {\r\n lang: string;\r\n dir: \"ltr\" | \"rtl\";\r\n} {\r\n const fallback = { lang: \"en\", dir: \"ltr\" } as const;\r\n\r\n if (typeof locale !== \"string\" || locale.trim() === \"\") return fallback;\r\n\r\n // Locale negotiation is normally complete by this point, but the final 404\r\n // must also survive a partial or unusual JavaScript runtime. Keep this local:\r\n // it is a document-specific last line of defence, not a public locale API.\r\n try {\r\n const Locale = Intl.Locale;\r\n\r\n if (typeof Locale !== \"function\") return fallback;\r\n\r\n const resolvedLocale = new Locale(locale);\r\n const lang = resolvedLocale.toString();\r\n const textInfo = (resolvedLocale as Intl.Locale & { textInfo?: unknown }).textInfo;\r\n\r\n if (typeof textInfo !== \"object\" || textInfo === null) return fallback;\r\n\r\n const direction = (textInfo as { direction?: unknown }).direction;\r\n\r\n if (direction !== \"ltr\" && direction !== \"rtl\") return fallback;\r\n\r\n return { lang, dir: direction };\r\n } catch {\r\n return fallback;\r\n }\r\n}\r\n\r\nexport function frameworkDefaultNotFoundDocument(locale?: string): string {\r\n const { lang, dir } = resolveDefaultNotFoundDocumentLocale(locale);\r\n\r\n return `<!doctype html>\r\n<html lang=\"${lang}\" dir=\"${dir}\">\r\n<head>\r\n <meta charset=\"utf-8\">\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\r\n <meta name=\"robots\" content=\"noindex\">\r\n <title>404 | Warlock</title>\r\n <link rel=\"stylesheet\" href=\"${buildFrameworkDefaultNotFoundStylesheetUrl()}\">\r\n</head>\r\n<body>\r\n <main>\r\n <div class=\"rule\" aria-hidden=\"true\"></div>\r\n <h1>404</h1>\r\n <p dir=\"auto\">This page is outside the spellbook.</p>\r\n <a href=\"/\" dir=\"auto\">Return home</a>\r\n </main>\r\n</body>\r\n</html>`;\r\n}\r\n\r\nexport type NotFoundRouteHandlerOptions = {\r\n /**\r\n * The application's `404.page.tsx`, already built into a page handler by\r\n * whichever installer owns module loading — `undefined` when the application\r\n * ships no such file, which is what selects\r\n * {@link frameworkDefaultNotFoundDocument}.\r\n *\r\n * Taking a built handler rather than a module keeps this file out of the\r\n * render pipeline entirely: dev hands over a Vite-backed handler, production a\r\n * manifest-backed one, and neither difference is visible here.\r\n */\r\n renderPage?: PageRouteHandler;\r\n};\r\n\r\n/**\r\n * The handler registered on the catch-all.\r\n *\r\n * Three answers, in this order, and the order is the safety property: the API\r\n * check runs BEFORE anything can render, so no request that the rule calls an\r\n * API request can reach a React render even if the page module is broken.\r\n */\r\nexport function createNotFoundRouteHandler(options: NotFoundRouteHandlerOptions): PageRouteHandler {\r\n const { renderPage } = options;\r\n\r\n return async (context: HttpContext) => {\r\n const { request, response } = context;\r\n // A catch-all 404 must never become a cached answer for a later route.\r\n // Set this before classifying so it also travels with an application's\r\n // custom 404 page, whose rendering remains otherwise entirely its own.\r\n response.header(\"Cache-Control\", \"no-store\");\r\n // Node lowercases header names and collapses a repeated `Accept` into an\r\n // array; both forms are read, so a duplicated header cannot silently mean\r\n // \"no preference\".\r\n const accept = request.header(\"accept\");\r\n\r\n if (\r\n classifyUnmatchedRequest({\r\n method: request.method,\r\n accept: Array.isArray(accept) ? accept.join(\",\") : accept,\r\n }) === \"api\"\r\n ) {\r\n // The same body core's own dev dispatcher writes for an unmatched route\r\n // (`core/src/router/router.ts`), so an API 404 reads identically whether\r\n // it fell through to core or was declined here — and identically in\r\n // development and in production, which it previously was not.\r\n await response.send(\r\n { error: \"Route not found\", path: request.path, method: request.method },\r\n 404,\r\n );\r\n\r\n return;\r\n }\r\n\r\n if (renderPage === undefined) {\r\n await response.html(frameworkDefaultNotFoundDocument(request.locale), 404);\r\n\r\n return;\r\n }\r\n\r\n return renderPage(context);\r\n };\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"not-found-page.mjs","names":[],"sources":["../../../../../../../web/src/server/not-found-page.ts"],"sourcesContent":["/**\r\n * THE NOT-FOUND PATH — the one route in the application that answers for URLs\r\n * nobody declared.\r\n *\r\n * An application gets it by writing `404.page.tsx` anywhere under a web root.\r\n * The file is named for the status it answers with, not for a concept\r\n * (\"not-found\"), because `404` is the string people actually search for, and\r\n * the `*.page.tsx` suffix is what makes it a page in the first place.\r\n *\r\n * ── THE RULE THIS FILE EXISTS FOR ────────────────────────────────────────────\r\n *\r\n * Pages and API routes share ONE route namespace and ONE router. So a catch-all\r\n * page route sees every unmatched request in the process, including\r\n * `GET /api/uzers` — and if it renders a document for that, a `fetch()` gets\r\n * `<!doctype html>` back and dies inside `response.json()` with a SyntaxError\r\n * pointing at the parser instead of at the typo. That failure is expensive\r\n * precisely because the error names nothing near its cause.\r\n *\r\n * Because pages and API share one namespace there is no path-prefix rule\r\n * available: `/anything` may legitimately be either. The discriminator is\r\n * therefore the `Accept` header, and it is stated as a narrow permission rather\r\n * than a guess — the document is the exception, JSON is the default:\r\n *\r\n * A request renders the not-found PAGE only if\r\n * 1. its method is GET or HEAD, and\r\n * 2. `text/html` appears EXPLICITLY in its `Accept` header.\r\n *\r\n * (1) is not a heuristic about browsers. Pages are registered with\r\n * `router.get` and nothing else — every page route in this codebase is\r\n * installed by `installPageRoutes` / `installPageRoutesFromManifest`, both\r\n * of which call `router.get`. A `POST` therefore cannot have been meant\r\n * for a page, by construction. HEAD rides along because Fastify answers it\r\n * from the GET route.\r\n *\r\n * (2) is EXPLICIT and the word is load-bearing. A wildcard does NOT count:\r\n * `* /*` — what a bare `fetch()` sends — is not a request for a document,\r\n * it is the absence of a preference, and `text/*` claims a family rather\r\n * than the type. Only the literal `text/html` media range, with a non-zero\r\n * `q`, opens the page path. A browser address-bar navigation always sends\r\n * an explicit `text/html`; a `fetch()` that has not asked for one never\r\n * does. So the mistyped `/api/...` in a `fetch()` keeps its JSON body and\r\n * dies at the typo rather than inside `response.json()`.\r\n *\r\n * WHAT THE RULE CANNOT DECIDE, and does not pretend to: a browser navigating to\r\n * a typo'd API URL asks for `text/html`, and so is answered with the document.\r\n * Nothing in that request distinguishes it from a typo'd page URL — same verb,\r\n * same header, same absence of a match — so the rule does not guess. The status\r\n * is 404 either way, which is the part machines read.\r\n *\r\n * ── DEPENDENCY NOTE ──────────────────────────────────────────────────────────\r\n * This module has one runtime asset dependency: the framework fallback's\r\n * stylesheet, read as a plain string constant from\r\n * `./framework-default-not-found-stylesheet` and turned into a data URL at\r\n * render time — never a static-asset import, which the production server\r\n * build refuses to compile (see that module's own comment). `../build/discover-pages`\r\n * imports the filename constant and identity helpers from here so build\r\n * discovery and both installers cannot disagree about what a not-found page\r\n * is. No renderer or application runtime is imported, so that edge cannot\r\n * drag the render pipeline into the build.\r\n */\r\nimport type { HttpContext } from \"@warlock.js/core\";\r\nimport type { PageRouteHandler } from \"./create-page-route-handler\";\r\nimport type { Router } from \"@warlock.js/core\";\r\nimport { buildFrameworkDefaultNotFoundStylesheetUrl } from \"./framework-default-not-found-stylesheet\";\r\n\r\n/**\r\n * The one filename that makes a page THE not-found page.\r\n *\r\n * `404.page.tsx`, not `not-found.page.tsx`: it keeps the `*.page.tsx`\r\n * convention every other page follows, and `404` is the token a developer\r\n * greps for when a URL answers with one.\r\n */\r\nexport const NOT_FOUND_PAGE_FILENAME = \"404.page.tsx\";\r\n\r\n/**\r\n * The path the not-found route is registered on — find-my-way's and Fastify's\r\n * catch-all, and the same literal core's own dev dispatcher registers\r\n * (`core/src/router/router.ts`, `server.route({ url: \"*\" })`).\r\n *\r\n * A catch-all has the LOWEST matching priority in both routers, so every\r\n * declared page and every declared API route still wins on its own path; this\r\n * route is only ever reached because nothing else claimed the URL.\r\n */\r\nexport const NOT_FOUND_ROUTE_PATH = \"*\";\r\n\r\n/**\r\n * The reserved route name the not-found page is registered under.\r\n *\r\n * Namespaced under `warlock.` because it is the framework's route rather than\r\n * the application's, and because the router's name namespace is shared with API\r\n * routes — an application that takes this name gets core's duplicate-name error,\r\n * which is the loud answer, not a silent overwrite.\r\n *\r\n * It is deliberately NOT published into the route table (`href()` / `<Link>`):\r\n * the not-found page has no URL of its own to link to.\r\n */\r\nexport const NOT_FOUND_ROUTE_NAME = \"warlock.not-found\";\r\n\r\n/** True when `sourceFile`'s basename is exactly {@link NOT_FOUND_PAGE_FILENAME}. */\r\nexport function isNotFoundPageFile(sourceFile: string): boolean {\r\n const separator = Math.max(sourceFile.lastIndexOf(\"/\"), sourceFile.lastIndexOf(\"\\\\\"));\r\n\r\n return sourceFile.slice(separator + 1) === NOT_FOUND_PAGE_FILENAME;\r\n}\r\n\r\n/**\r\n * Raised when more than one `404.page.tsx` exists.\r\n *\r\n * There is exactly one not-found route in a process, so a second file is not a\r\n * per-module override — it is two files claiming one route, with the winner\r\n * decided by directory-walk order. Both are named because the fix is to delete\r\n * one and the operator has to know which two are in play.\r\n */\r\nexport class DuplicateNotFoundPageError extends Error {\r\n public constructor(public readonly pageFiles: readonly string[]) {\r\n super(\r\n `Two or more not-found pages were found: ${pageFiles.map((file) => `\"${file}\"`).join(\", \")}. ` +\r\n `An application has exactly one \\`${NOT_FOUND_PAGE_FILENAME}\\` — it answers every ` +\r\n \"unmatched page URL in the process, so a second one would silently never render. \" +\r\n \"Keep one and delete the rest.\",\r\n );\r\n this.name = \"DuplicateNotFoundPageError\";\r\n }\r\n}\r\n\r\n/**\r\n * Raised when `404.page.tsx` declares a `route` export.\r\n *\r\n * The not-found page has no URL of its own — it is reached by NOT matching. A\r\n * `route` export on it reads like a promise that `/404` is browsable, and it is\r\n * not: the installers register this file on the catch-all and nowhere else. So\r\n * the export is refused rather than ignored, because a declaration the framework\r\n * silently drops is worse than one it rejects.\r\n */\r\nexport class NotFoundPageDeclaresRouteError extends Error {\r\n public constructor(public readonly pageFile: string) {\r\n super(\r\n `\"${pageFile}\" is the not-found page but declares a \\`route\\` export. ` +\r\n `\\`${NOT_FOUND_PAGE_FILENAME}\\` has no URL of its own — it answers every page URL that ` +\r\n \"matched nothing, and is never registered at a path of its own. Remove the `route` \" +\r\n \"export; to serve a browsable page at a fixed path, use a normal `*.page.tsx`.\",\r\n );\r\n this.name = \"NotFoundPageDeclaresRouteError\";\r\n }\r\n}\r\n\r\n/** The shape this module reads off a registered route — core's `Route`, narrowed. */\r\nexport type RegisteredRouteShape = {\r\n path: string;\r\n isPage?: boolean;\r\n};\r\n\r\n/**\r\n * The media range the not-found PAGE is gated on. Compared literally: a request\r\n * either named this exact type or it did not.\r\n */\r\nconst HTML_MEDIA_TYPE = \"text/html\";\r\n\r\n/**\r\n * True when `text/html` is named EXPLICITLY in an `Accept` header — the whole\r\n * discriminator, in one predicate.\r\n *\r\n * Wildcards are refused on purpose. `* /*` is what `fetch()` and `curl` send\r\n * when the caller expressed no preference at all, and `text/*` names a family;\r\n * neither is a request for a document, and treating either as one is what makes\r\n * a mistyped `/api/...` answer HTML to a JSON parser.\r\n *\r\n * `q=0` is honoured because it is the header's own way of saying \"not this\r\n * one\" — `Accept: text/html;q=0, application/json` is a client refusing the\r\n * document, and reading it as a request for one would be reading the header\r\n * backwards. Any other `q`, present or absent, counts.\r\n */\r\nexport function acceptsHtmlExplicitly(accept: string | undefined): boolean {\r\n if (!accept) return false;\r\n\r\n for (const entry of accept.split(\",\")) {\r\n const [rawType, ...parameters] = entry.split(\";\");\r\n\r\n if (rawType.trim().toLowerCase() !== HTML_MEDIA_TYPE) continue;\r\n\r\n const quality = parameters\r\n .map((parameter) => parameter.trim().toLowerCase())\r\n .find((parameter) => parameter.startsWith(\"q=\"));\r\n\r\n // A malformed `q` is not a refusal — only an explicit zero is.\r\n if (quality !== undefined && Number.parseFloat(quality.slice(2)) === 0) continue;\r\n\r\n return true;\r\n }\r\n\r\n return false;\r\n}\r\n\r\nexport type UnmatchedRequestKind = \"page\" | \"api\";\r\n\r\n/**\r\n * The rule, in one function — see this file's header for why it is these two\r\n * conditions and why the second one refuses wildcards.\r\n */\r\nexport function classifyUnmatchedRequest(input: {\r\n method: string;\r\n accept: string | undefined;\r\n}): UnmatchedRequestKind {\r\n const method = input.method.toUpperCase();\r\n\r\n // Pages are installed with `router.get`, so only these two verbs can ever\r\n // have been asking for one.\r\n if (method !== \"GET\" && method !== \"HEAD\") return \"api\";\r\n\r\n return acceptsHtmlExplicitly(input.accept) ? \"page\" : \"api\";\r\n}\r\n\r\n/**\r\n * The document served when the application ships no `404.page.tsx`.\r\n *\r\n * Deliberately a STRING, not a React render: it must survive the case where the\r\n * application root, a layout or the page module is exactly what is broken, and\r\n * a default that can itself fail is not a default. Its stylesheet is embedded in\r\n * the document as a data URL, and it carries no hydration script, so serving the\r\n * fallback requires no application render and no follow-up fetch.\r\n *\r\n * It answers 404 like the real page does, because the status is the part that\r\n * search engines, caches and monitoring read; a framework default that soft-404s\r\n * would teach every un-customised application to lie.\r\n */\r\nfunction resolveDefaultNotFoundDocumentLocale(locale: unknown): {\r\n lang: string;\r\n dir: \"ltr\" | \"rtl\";\r\n} {\r\n const fallback = { lang: \"en\", dir: \"ltr\" } as const;\r\n\r\n if (typeof locale !== \"string\" || locale.trim() === \"\") return fallback;\r\n\r\n // Locale negotiation is normally complete by this point, but the final 404\r\n // must also survive a partial or unusual JavaScript runtime. Keep this local:\r\n // it is a document-specific last line of defence, not a public locale API.\r\n try {\r\n const Locale = Intl.Locale;\r\n\r\n if (typeof Locale !== \"function\") return fallback;\r\n\r\n const resolvedLocale = new Locale(locale);\r\n const lang = resolvedLocale.toString();\r\n const textInfo = (resolvedLocale as Intl.Locale & { textInfo?: unknown }).textInfo;\r\n\r\n if (typeof textInfo !== \"object\" || textInfo === null) return fallback;\r\n\r\n const direction = (textInfo as { direction?: unknown }).direction;\r\n\r\n if (direction !== \"ltr\" && direction !== \"rtl\") return fallback;\r\n\r\n return { lang, dir: direction };\r\n } catch {\r\n return fallback;\r\n }\r\n}\r\n\r\nexport function frameworkDefaultNotFoundDocument(locale?: string): string {\r\n const { lang, dir } = resolveDefaultNotFoundDocumentLocale(locale);\r\n\r\n return `<!doctype html>\r\n<html lang=\"${lang}\" dir=\"${dir}\">\r\n<head>\r\n <meta charset=\"utf-8\">\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\r\n <meta name=\"robots\" content=\"noindex\">\r\n <title>404 | Warlock</title>\r\n <link rel=\"stylesheet\" href=\"${buildFrameworkDefaultNotFoundStylesheetUrl()}\">\r\n</head>\r\n<body>\r\n <main>\r\n <div class=\"rule\" aria-hidden=\"true\"></div>\r\n <h1>404</h1>\r\n <p dir=\"auto\">This page is outside the spellbook.</p>\r\n <a href=\"/\" dir=\"auto\">Return home</a>\r\n </main>\r\n</body>\r\n</html>`;\r\n}\r\n\r\nexport type NotFoundRouteHandlerOptions = {\r\n /**\r\n * The application's `404.page.tsx`, already built into a page handler by\r\n * whichever installer owns module loading — `undefined` when the application\r\n * ships no such file, which is what selects\r\n * {@link frameworkDefaultNotFoundDocument}.\r\n *\r\n * Taking a built handler rather than a module keeps this file out of the\r\n * render pipeline entirely: dev hands over a Vite-backed handler, production a\r\n * manifest-backed one, and neither difference is visible here.\r\n */\r\n renderPage?: PageRouteHandler;\r\n};\r\n\r\n/**\r\n * The handler registered on the catch-all.\r\n *\r\n * Three answers, in this order, and the order is the safety property: the API\r\n * check runs BEFORE anything can render, so no request that the rule calls an\r\n * API request can reach a React render even if the page module is broken.\r\n */\r\nexport function createNotFoundRouteHandler(options: NotFoundRouteHandlerOptions): PageRouteHandler {\r\n const { renderPage } = options;\r\n\r\n return async (context: HttpContext) => {\r\n const { request, response } = context;\r\n // A catch-all 404 must never become a cached answer for a later route.\r\n // Set this before classifying so it also travels with an application's\r\n // custom 404 page, whose rendering remains otherwise entirely its own.\r\n response.header(\"Cache-Control\", \"no-store\");\r\n // Node lowercases header names and collapses a repeated `Accept` into an\r\n // array; both forms are read, so a duplicated header cannot silently mean\r\n // \"no preference\".\r\n const accept = request.header(\"accept\");\r\n\r\n if (\r\n classifyUnmatchedRequest({\r\n method: request.method,\r\n accept: Array.isArray(accept) ? accept.join(\",\") : accept,\r\n }) === \"api\"\r\n ) {\r\n // The same body core's own dev dispatcher writes for an unmatched route\r\n // (`core/src/router/router.ts`), so an API 404 reads identically whether\r\n // it fell through to core or was declined here — and identically in\r\n // development and in production, which it previously was not.\r\n await response.send(\r\n { error: \"Route not found\", path: request.path, method: request.method },\r\n 404,\r\n );\r\n\r\n return;\r\n }\r\n\r\n if (renderPage === undefined) {\r\n await response.html(frameworkDefaultNotFoundDocument(request.locale), 404);\r\n\r\n return;\r\n }\r\n\r\n return renderPage(context);\r\n };\r\n}\r\n\r\n/** Registers the invariant catch-all route both page installers share. */\r\nexport function registerNotFoundPageRoute(options: {\r\n router: Router;\r\n renderPage?: PageRouteHandler;\r\n}): void {\r\n options.router.get(\r\n NOT_FOUND_ROUTE_PATH,\r\n createNotFoundRouteHandler({ renderPage: options.renderPage }),\r\n { name: NOT_FOUND_ROUTE_NAME, isPage: true },\r\n );\r\n}\r\n"],"mappings":";;;;;;;;;;AAwEA,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;;;;;;;;;;;;;;AAeA,SAAS,qCAAqC,QAG5C;CACA,MAAM,WAAW;EAAE,MAAM;EAAM,KAAK;CAAM;CAE1C,IAAI,OAAO,WAAW,YAAY,OAAO,KAAK,MAAM,IAAI,OAAO;CAK/D,IAAI;EACF,MAAM,SAAS,KAAK;EAEpB,IAAI,OAAO,WAAW,YAAY,OAAO;EAEzC,MAAM,iBAAiB,IAAI,OAAO,MAAM;EACxC,MAAM,OAAO,eAAe,SAAS;EACrC,MAAM,WAAY,eAAwD;EAE1E,IAAI,OAAO,aAAa,YAAY,aAAa,MAAM,OAAO;EAE9D,MAAM,YAAa,SAAqC;EAExD,IAAI,cAAc,SAAS,cAAc,OAAO,OAAO;EAEvD,OAAO;GAAE;GAAM,KAAK;EAAU;CAChC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAgB,iCAAiC,QAAyB;CACxE,MAAM,EAAE,MAAM,QAAQ,qCAAqC,MAAM;CAEjE,OAAO;cACK,KAAK,SAAS,IAAI;;;;;;iCAMC,2CAA2C,EAAE;;;;;;;;;;;AAW9E;;;;;;;;AAuBA,SAAgB,2BAA2B,SAAwD;CACjG,MAAM,EAAE,eAAe;CAEvB,OAAO,OAAO,YAAyB;EACrC,MAAM,EAAE,SAAS,aAAa;EAI9B,SAAS,OAAO,iBAAiB,UAAU;EAI3C,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,QAAQ,MAAM,GAAG,GAAG;GAEzE;EACF;EAEA,OAAO,WAAW,OAAO;CAC3B;AACF;;AAGA,SAAgB,0BAA0B,SAGjC;CACP,QAAQ,OAAO,SAEb,2BAA2B,EAAE,YAAY,QAAQ,WAAW,CAAC,GAC7D;EAAE,MAAM;EAAsB,QAAQ;CAAK,CAC7C;AACF"}
@@ -1,5 +1,5 @@
1
- import fs from "node:fs";
2
1
  import path from "node:path";
2
+ import fs from "node:fs";
3
3
 
4
4
  //#region ../web/src/server/page-file-change.ts
5
5
  const PAGE_FILE_SUFFIX = ".page.tsx";
@@ -0,0 +1,24 @@
1
+ import { PageCacheOptIn } from "../routing/route-identity.mjs";
2
+ import { PipelineLoader, PipelineMiddleware } from "./execute-page-request.types.mjs";
3
+ //#region ../web/src/server/page-module-shapes.d.ts
4
+ /** A page declares either a bare path or a path plus an explicit route name. */
5
+ type PageRouteExport = string | {
6
+ path: string;
7
+ name?: string;
8
+ cache?: PageCacheOptIn;
9
+ };
10
+ /** The only export the page installers read off a page module namespace. */
11
+ type PageModuleShape = {
12
+ route?: PageRouteExport;
13
+ };
14
+ /** The exports the page installers read off a layout module namespace. */
15
+ type LayoutModuleShape = {
16
+ /** Universal registration hook; invoked on this real namespace, never a composed wrapper. */register?: () => unknown;
17
+ prefix?: string; /** The default export decides whether this layout renders. */
18
+ default?: unknown; /** The layout's guards, in declaration order. */
19
+ middleware?: readonly PipelineMiddleware[];
20
+ loader?: PipelineLoader;
21
+ };
22
+ //#endregion
23
+ export { LayoutModuleShape, PageModuleShape, PageRouteExport };
24
+ //# sourceMappingURL=page-module-shapes.d.mts.map
@@ -1,5 +1,5 @@
1
- import { resolvePageRouteIdentity } from "../routing/route-identity.mjs";
2
1
  import { isNotFoundPageFile } from "./not-found-page.mjs";
2
+ import { resolvePageRouteIdentity } from "../routing/route-identity.mjs";
3
3
  import { filesystemPageFileFor } from "./install-page-routes.mjs";
4
4
  import { isErrorPageFilePath } from "./page-file-change.mjs";
5
5
  import path from "node:path";
@@ -1,6 +1,6 @@
1
1
  import { collectPublicFilesSync } from "../build/public-files.mjs";
2
- import fs from "node:fs";
3
2
  import path from "node:path";
3
+ import fs from "node:fs";
4
4
 
5
5
  //#region ../web/src/server/register-production-public-files.ts
6
6
  var InvalidProductionPublicFileError = class extends Error {
@@ -1,12 +1,13 @@
1
1
  import { PAYLOAD_SCRIPT_ID, escapePayload } from "../components/document-context.mjs";
2
2
  import { BufferedCookie } from "./settle-page-response.mjs";
3
- import { ExecutePageRequestOptions, PageDataBundle, PageRouteEntry } from "./execute-page-request.types.mjs";
3
+ import { ExecutePageRequestOptions, PageDataBundle, PageRouteEntry, PageRouteMatch } from "./execute-page-request.types.mjs";
4
4
  import { ErrorPageModuleLoader } from "./error-page.mjs";
5
5
  import { Request, Response } from "@warlock.js/core";
6
6
 
7
7
  //#region ../web/src/server/render-page.d.ts
8
8
  type RenderPageRequestOptions = {
9
- routes: readonly PageRouteEntry[];
9
+ routes: readonly PageRouteEntry[]; /** Core's already-resolved HTTP route, when this is serving a live request. */
10
+ matched?: Pick<PageRouteMatch, "entry" | "params">;
10
11
  createHttp: ExecutePageRequestOptions["createHttp"]; /** Loaded only after the ordinary boundary chain has been exhausted. */
11
12
  loadErrorPage?: ErrorPageModuleLoader;
12
13
  };
@@ -321,6 +321,7 @@ async function renderPageRequest(url, options) {
321
321
  const rendered = await executePageRequest({
322
322
  url,
323
323
  routes: registry.routes,
324
+ matched: options.matched,
324
325
  createHttp,
325
326
  finish: (bundle) => finishRender(state.match.entry.triple, bundle, documentSlotsFrom(state.captured), state.captured.response, options.loadErrorPage)
326
327
  });