@warlock.js/web 5.4.0 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/CHANGELOG.md +33 -5
  2. package/esm/build/contribution.d.mts +2 -2
  3. package/esm/build/contribution.mjs.map +1 -1
  4. package/esm/build/discover-pages.mjs +127 -205
  5. package/esm/build/discover-pages.mjs.map +1 -1
  6. package/esm/build/generate-pages-barrel.mjs +5 -3
  7. package/esm/build/generate-pages-barrel.mjs.map +1 -1
  8. package/esm/build/public-files.mjs +29 -1
  9. package/esm/build/public-files.mjs.map +1 -1
  10. package/esm/build/read-metadata-keys.mjs +225 -0
  11. package/esm/build/read-metadata-keys.mjs.map +1 -0
  12. package/esm/build/read-route-exports.mjs.map +1 -1
  13. package/esm/client/build-hydrated-tree.mjs +1 -1
  14. package/esm/client/build-hydrated-tree.mjs.map +1 -1
  15. package/esm/client/navigation/document-metadata.mjs +108 -0
  16. package/esm/client/navigation/document-metadata.mjs.map +1 -0
  17. package/esm/client/navigation/navigation-root.mjs +23 -103
  18. package/esm/client/navigation/navigation-root.mjs.map +1 -1
  19. package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -1
  20. package/esm/client/navigation/use-query-string.d.mts +35 -0
  21. package/esm/client/navigation/use-query-string.mjs +68 -0
  22. package/esm/client/navigation/use-query-string.mjs.map +1 -0
  23. package/esm/client/runtime/index.d.mts +1 -1
  24. package/esm/client/runtime/index.mjs +1 -1
  25. package/esm/client/runtime/manifest.mjs.map +1 -1
  26. package/esm/client/runtime/matcher.mjs.map +1 -1
  27. package/esm/client/runtime/types.d.mts +1 -1
  28. package/esm/components/link.mjs.map +1 -1
  29. package/esm/core/src/router/normalize-route-path.mjs +33 -0
  30. package/esm/core/src/router/normalize-route-path.mjs.map +1 -0
  31. package/esm/{hydration → entry}/index.mjs +1 -1
  32. package/esm/entry/index.mjs.map +1 -0
  33. package/esm/hydration-payload.mjs.map +1 -1
  34. package/esm/index.d.mts +3 -2
  35. package/esm/index.mjs +3 -2
  36. package/esm/loaders.d.mts +10 -3
  37. package/esm/localization.mjs.map +1 -1
  38. package/esm/metadata.d.mts +4 -4
  39. package/esm/metadata.mjs.map +1 -1
  40. package/esm/node_modules/.pnpm/@mongez_concat-route@1.2.0/node_modules/@mongez/concat-route/esm/index.mjs +33 -0
  41. package/esm/node_modules/.pnpm/@mongez_concat-route@1.2.0/node_modules/@mongez/concat-route/esm/index.mjs.map +1 -0
  42. package/esm/{runtime/register-modules.d.mts → register-modules.d.mts} +1 -1
  43. package/esm/{runtime/register-modules.mjs → register-modules.mjs} +1 -1
  44. package/esm/register-modules.mjs.map +1 -0
  45. package/esm/route.d.mts +35 -2
  46. package/esm/routing/duplicate-route-path.mjs +22 -0
  47. package/esm/routing/duplicate-route-path.mjs.map +1 -0
  48. package/esm/routing/layout-level.mjs +50 -0
  49. package/esm/routing/layout-level.mjs.map +1 -0
  50. package/esm/routing/layout-policy.mjs.map +1 -1
  51. package/esm/routing/query-string.d.mts +23 -1
  52. package/esm/routing/query-string.mjs +23 -1
  53. package/esm/routing/query-string.mjs.map +1 -1
  54. package/esm/routing/route-identity.mjs +27 -2
  55. package/esm/routing/route-identity.mjs.map +1 -1
  56. package/esm/routing/route-table.mjs.map +1 -1
  57. package/esm/routing/router-events.d.mts +5 -4
  58. package/esm/routing/router-events.mjs.map +1 -1
  59. package/esm/server/create-page-route-handler.d.mts +2 -2
  60. package/esm/server/create-page-route-handler.mjs +6 -6
  61. package/esm/server/create-page-route-handler.mjs.map +1 -1
  62. package/esm/server/{dev-server.mjs → dev-error-transport.mjs} +2 -2
  63. package/esm/server/dev-error-transport.mjs.map +1 -0
  64. package/esm/server/execute-page-request.mjs +87 -23
  65. package/esm/server/execute-page-request.mjs.map +1 -1
  66. package/esm/server/execute-page-request.types.d.mts +10 -1
  67. package/esm/server/fold-layout-loaders.mjs +36 -0
  68. package/esm/server/fold-layout-loaders.mjs.map +1 -0
  69. package/esm/server/hydration-client-url.mjs +2 -2
  70. package/esm/server/hydration-client-url.mjs.map +1 -1
  71. package/esm/server/index.d.mts +2 -2
  72. package/esm/server/index.mjs +2 -2
  73. package/esm/server/install-page-routes-from-manifest.d.mts +1 -1
  74. package/esm/server/install-page-routes-from-manifest.mjs +43 -40
  75. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  76. package/esm/server/install-page-routes.d.mts +22 -2
  77. package/esm/server/install-page-routes.mjs +108 -42
  78. package/esm/server/install-page-routes.mjs.map +1 -1
  79. package/esm/server/install-production-page-routes.mjs.map +1 -1
  80. package/esm/server/match-page-route.mjs.map +1 -1
  81. package/esm/server/page-file-change.mjs.map +1 -1
  82. package/esm/server/page-route-reload.mjs +4 -3
  83. package/esm/server/page-route-reload.mjs.map +1 -1
  84. package/esm/server/register-production-public-files.mjs +25 -1
  85. package/esm/server/register-production-public-files.mjs.map +1 -1
  86. package/esm/server/render-page.d.mts +11 -48
  87. package/esm/server/render-page.mjs +13 -55
  88. package/esm/server/render-page.mjs.map +1 -1
  89. package/esm/server/resolve-page-metadata.mjs.map +1 -1
  90. package/esm/server/resolve-route-validation-input.mjs +11 -0
  91. package/esm/server/resolve-route-validation-input.mjs.map +1 -0
  92. package/esm/server/route-validation-error.mjs +32 -0
  93. package/esm/server/route-validation-error.mjs.map +1 -0
  94. package/esm/server/settle-page-response.mjs +5 -3
  95. package/esm/server/settle-page-response.mjs.map +1 -1
  96. package/esm/server/unregistered-pages.mjs +2 -1
  97. package/esm/server/unregistered-pages.mjs.map +1 -1
  98. package/esm/server/web-connector-factory.mjs +1 -1
  99. package/esm/server/web-connector-factory.mjs.map +1 -1
  100. package/esm/server/web-connector.mjs +24 -321
  101. package/esm/server/web-connector.mjs.map +1 -1
  102. package/esm/shared/module-key.mjs +15 -0
  103. package/esm/shared/module-key.mjs.map +1 -0
  104. package/esm/shared/to-posix.mjs +13 -0
  105. package/esm/shared/to-posix.mjs.map +1 -0
  106. package/esm/shared.mjs +24 -0
  107. package/esm/shared.mjs.map +1 -1
  108. package/esm/vite/build-client.mjs.map +1 -1
  109. package/esm/vite/dev-server-config.mjs +240 -0
  110. package/esm/vite/dev-server-config.mjs.map +1 -0
  111. package/esm/vite/gate-a-resolve.d.mts +5 -4
  112. package/esm/vite/gate-a-resolve.mjs +31 -70
  113. package/esm/vite/gate-a-resolve.mjs.map +1 -1
  114. package/esm/vite/gate-b-secrets.mjs.map +1 -1
  115. package/esm/vite/gate-c-verify.d.mts +1 -1
  116. package/esm/vite/gate-c-verify.mjs +1 -1
  117. package/esm/vite/gate-c-verify.mjs.map +1 -1
  118. package/esm/vite/hydration-entries.mjs +5 -7
  119. package/esm/vite/hydration-entries.mjs.map +1 -1
  120. package/esm/vite/index.d.mts +1 -1
  121. package/esm/vite/index.mjs +4 -110
  122. package/esm/vite/index.mjs.map +1 -1
  123. package/esm/vite/page-registry-plugin.mjs +9 -26
  124. package/esm/vite/page-registry-plugin.mjs.map +1 -1
  125. package/esm/vite/projection.mjs.map +1 -1
  126. package/esm/vite/react-refresh-preamble.mjs +90 -0
  127. package/esm/vite/react-refresh-preamble.mjs.map +1 -0
  128. package/esm/vite/ssr-client-view.mjs +135 -0
  129. package/esm/vite/ssr-client-view.mjs.map +1 -0
  130. package/llms-full.txt +564 -485
  131. package/llms.txt +2 -2
  132. package/package.json +6 -6
  133. package/skills/add-web-to-an-app/SKILL.md +3 -3
  134. package/skills/create-a-page/SKILL.md +348 -295
  135. package/skills/navigate-on-the-client/SKILL.md +212 -189
  136. package/esm/hydration/index.mjs.map +0 -1
  137. package/esm/runtime/register-modules.mjs.map +0 -1
  138. package/esm/server/dev-server.mjs.map +0 -1
  139. /package/esm/{hydration → entry}/index.d.mts +0 -0
@@ -1,14 +1,15 @@
1
1
  import { publishRouteTable } from "../routing/route-table.mjs";
2
- import { isLoaderShortCircuit } from "./settle-page-response.mjs";
3
2
  import { createPageModuleLoader } from "./create-page-module-loader.mjs";
4
3
  import { composeRoutePath } from "../routing/compose-route-path.mjs";
4
+ import { duplicateRoutePathMessage } from "../routing/duplicate-route-path.mjs";
5
5
  import { deriveFilesystemRoutePath } from "../routing/filesystem-route.mjs";
6
- import { NestedLayoutsNotSupportedError, selectPageLayout } from "../routing/layout-policy.mjs";
7
- import { canonicalizeRouteExport, resolvePageRouteCache, resolvePageRouteName } from "../routing/route-identity.mjs";
6
+ import { resolveLayoutLevel } from "../routing/layout-level.mjs";
7
+ import { resolvePageRouteCache, resolvePageRouteIdentity } from "../routing/route-identity.mjs";
8
8
  import { createPageRouteHandler } from "./create-page-route-handler.mjs";
9
+ import { foldLayoutLoaders } from "./fold-layout-loaders.mjs";
9
10
  import { productionStylesheetUrls } from "./stylesheet-urls.mjs";
10
11
  import { DuplicateNotFoundPageError, NOT_FOUND_ROUTE_NAME, NOT_FOUND_ROUTE_PATH, NotFoundPageDeclaresRouteError, createNotFoundRouteHandler, isNotFoundPageFile } from "./not-found-page.mjs";
11
- import { Response } from "@warlock.js/core";
12
+ import "@warlock.js/core";
12
13
 
13
14
  //#region ../web/src/server/install-page-routes-from-manifest.ts
14
15
  /**
@@ -26,10 +27,13 @@ import { Response } from "@warlock.js/core";
26
27
  * and the guards that run before it renders. A page's `route` export and the
27
28
  * `prefix` and `middleware` exports of EVERY layout on its path are read off the
28
29
  * module namespaces here, at boot, and composed by the same rules dev composes
29
- * them by ({@link layoutLevelOf}, {@link composeLayoutLevel}) — so the URL a page
30
- * answers on and the chain that guards it are decided by the page's own source
31
- * in both modes, and a build cannot quietly disagree with the dev server about
32
- * either.
30
+ * them by a shared mechanism now, not a promise: {@link layoutLevelOf} calls
31
+ * `../routing/layout-level.ts`'s `resolveLayoutLevel`, the same selection and
32
+ * prefix-composition rule dev's own `resolveLayoutLevel` calls, and
33
+ * {@link composeLayoutLevel} folds loaders through the same
34
+ * `./fold-layout-loaders.ts` dev's does — so the URL a page answers on and the
35
+ * chain that guards it are decided by the page's own source in both modes, and
36
+ * a build cannot quietly disagree with the dev server about either.
33
37
  *
34
38
  * WHAT IS DELIBERATELY DIFFERENT: this is synchronous. Every module is already
35
39
  * in memory, so registration has nothing to await; the loader handed to each
@@ -42,30 +46,29 @@ import { Response } from "@warlock.js/core";
42
46
  * app-root-relative (`"src/web/..."`, `page-manifest.ts`'s own doc comment),
43
47
  * so dropping the first two segments — `<srcDir>`, then the literal `"web"` —
44
48
  * recovers exactly what `deriveFilesystemRoutePath`/`deriveFilesystemRouteName`
45
- * expect: the same value dev computes as `filesystemPageFileFor`
46
- * (`install-page-routes.ts:101-103`).
49
+ * expect: the same value dev computes as `install-page-routes.ts`'s
50
+ * `filesystemPageFileFor`.
47
51
  */
48
52
  function webRelativeSourceFile(sourceFile) {
49
53
  return sourceFile.split("/").slice(2).join("/");
50
54
  }
51
- /** Exported for `../routing/route-name-parity.spec.ts`, which proves this and dev's `resolvePageRouteIdentity` agree. */
55
+ /**
56
+ * Exported for `../routing/route-name-parity.spec.ts`, which proves this and
57
+ * dev's own call agree — both now call the same `../routing/route-identity.ts`
58
+ * `resolvePageRouteIdentity`, so this wrapper's only job is supplying THIS
59
+ * installer's identifiers: the manifest `sourceFile` doubles as the
60
+ * `route.path` rejection context, and its web-root-relative form is the
61
+ * filesystem-derivation input.
62
+ */
52
63
  function resolveRoute(routeExport, sourceFile) {
53
- const pageFile = webRelativeSourceFile(sourceFile);
54
- if (routeExport === void 0) return {
55
- path: deriveFilesystemRoutePath({ pageFile }),
56
- name: resolvePageRouteName(routeExport, pageFile)
57
- };
58
- return {
59
- path: canonicalizeRouteExport(routeExport, sourceFile).path,
60
- name: resolvePageRouteName(routeExport, pageFile)
61
- };
64
+ return resolvePageRouteIdentity(routeExport, webRelativeSourceFile(sourceFile), sourceFile);
62
65
  }
63
66
  /**
64
67
  * Every layout's declared `prefix`, keyed by its directory relative to the
65
- * web root — the same table dev builds as `LayoutLevel.prefixesByDirectory`
66
- * (`install-page-routes.ts:214-222`) and the one
67
- * {@link deriveFilesystemRoutePath} uses to let a directory's own layout
68
- * rename the URL segment a bare directory name would otherwise contribute.
68
+ * web root — the same table dev builds as its own `LayoutLevel.prefixesByDirectory`,
69
+ * and the one {@link deriveFilesystemRoutePath} uses to let a directory's own
70
+ * layout rename the URL segment a bare directory name would otherwise
71
+ * contribute.
69
72
  */
70
73
  function layoutPrefixesOf(page) {
71
74
  return Object.fromEntries(page.layouts.flatMap((layout) => {
@@ -77,14 +80,14 @@ function layoutPrefixesOf(page) {
77
80
  }));
78
81
  }
79
82
  function layoutLevelOf(page) {
80
- const selection = selectPageLayout(page.layouts.map((layout) => ({
81
- layout: layout.sourceFile,
82
- renders: typeof layout.module.default !== "undefined"
83
+ const level = resolveLayoutLevel(page.sourceFile, page.layouts.map((layout) => ({
84
+ id: layout.sourceFile,
85
+ renders: typeof layout.module.default !== "undefined",
86
+ prefix: layout.module.prefix
83
87
  })));
84
- if (selection.type === "rejected") throw new NestedLayoutsNotSupportedError(page.sourceFile, selection.layouts);
85
88
  return {
86
- host: selection.type === "selected" ? page.layouts.find((layout) => layout.sourceFile === selection.layout) : page.layouts.at(-1),
87
- prefix: page.layouts.reduce((composed, layout) => composeRoutePath(composed, layout.module.prefix ?? "/"), "/")
89
+ host: page.layouts.find((layout) => layout.sourceFile === level.hostId),
90
+ prefix: level.prefix
88
91
  };
89
92
  }
90
93
  /**
@@ -106,15 +109,7 @@ function composeLayoutLevel(page, host) {
106
109
  return {
107
110
  ...host.module,
108
111
  middleware: page.layouts.flatMap((layout) => [...layout.module.middleware ?? []]),
109
- loader: async (context) => {
110
- let hostData;
111
- for (let index = 0; index < page.layouts.length; index++) {
112
- const value = await page.layouts[index].module.loader?.(context);
113
- if (value instanceof Response || isLoaderShortCircuit(value)) return value;
114
- if (index === hostIndex) hostData = value;
115
- }
116
- return hostData;
117
- }
112
+ loader: foldLayoutLoaders(page.layouts.map((layout) => layout.module.loader), hostIndex)
118
113
  };
119
114
  }
120
115
  /**
@@ -155,7 +150,15 @@ function installPageRoutesFromManifest(options) {
155
150
  layoutPrefixes: layoutPrefixesOf(page)
156
151
  }) : composeRoutePath(layoutPrefix, routePath);
157
152
  const existingFile = fileByPath.get(effectivePath);
158
- if (existingFile) throw new Error(`installPageRoutesFromManifest: composed route path "${effectivePath}" (layout prefix "${layoutPrefix}" + route.path "${routePath}") is declared by two pages — "${existingFile}" and "${page.sourceFile}". Every page's composed route path must be unique.`);
153
+ if (existingFile) throw new Error(duplicateRoutePathMessage({
154
+ effectivePath,
155
+ existingFile,
156
+ newFile: page.sourceFile,
157
+ composition: {
158
+ layoutPrefix,
159
+ routePath
160
+ }
161
+ }));
159
162
  fileByPath.set(effectivePath, page.sourceFile);
160
163
  const composedLayout = page.layouts.length > 1 && layout !== void 0 ? composeLayoutLevel(page, layout) : void 0;
161
164
  const stylesheetUrls = clientDir === void 0 ? [] : productionStylesheetUrls(clientDir, [
@@ -1 +1 @@
1
- {"version":3,"file":"install-page-routes-from-manifest.mjs","names":[],"sources":["../../../../../../../web/src/server/install-page-routes-from-manifest.ts"],"sourcesContent":["/**\r\n * Page-route registration for a built application.\r\n *\r\n * `installPageRoutes` answers \"which pages exist?\" by walking the filesystem\r\n * and \"what is this module?\" by asking Vite to evaluate it. Neither question\r\n * can be asked of a running production process: there is no `app/` tree beside\r\n * the bundle and no Vite. Both answers were therefore moved to build time — the\r\n * generated `pages.ts` barrel statically imported every page, layout and the\r\n * app root and handed them over as a {@link PageManifest}, and this module\r\n * turns that table into registered routes.\r\n *\r\n * WHAT IS DELIBERATELY IDENTICAL TO DEVELOPMENT: the route a page ends up on,\r\n * and the guards that run before it renders. A page's `route` export and the\r\n * `prefix` and `middleware` exports of EVERY layout on its path are read off the\r\n * module namespaces here, at boot, and composed by the same rules dev composes\r\n * them by ({@link layoutLevelOf}, {@link composeLayoutLevel}) — so the URL a page\r\n * answers on and the chain that guards it are decided by the page's own source\r\n * in both modes, and a build cannot quietly disagree with the dev server about\r\n * either.\r\n *\r\n * WHAT IS DELIBERATELY DIFFERENT: this is synchronous. Every module is already\r\n * in memory, so registration has nothing to await; the loader handed to each\r\n * handler is a lookup over the same table, not an evaluation step.\r\n */\r\nimport { composeRoutePath } from \"../routing/compose-route-path\";\r\nimport { deriveFilesystemRoutePath } from \"../routing/filesystem-route\";\r\nimport { NestedLayoutsNotSupportedError, selectPageLayout } from \"../routing/layout-policy\";\r\nimport {\r\n canonicalizeRouteExport,\r\n resolvePageRouteCache,\r\n resolvePageRouteName,\r\n type PageCacheOptIn,\r\n} from \"../routing/route-identity\";\r\nimport { publishRouteTable } from \"../routing/route-table\";\r\nimport { Response, type Router } from \"@warlock.js/core\";\r\nimport { createPageModuleLoader } from \"./create-page-module-loader\";\r\nimport type { ErrorPageModule } from \"./error-page\";\r\nimport {\r\n createPageRouteHandler,\r\n type PageRouteHandler,\r\n type PageRouteHandlerOptions,\r\n} from \"./create-page-route-handler\";\r\nimport type { PipelineLoader, PipelineMiddleware } from \"./execute-page-request\";\r\nimport { isLoaderShortCircuit } from \"./settle-page-response\";\r\nimport { productionStylesheetUrls } from \"./stylesheet-urls\";\r\nimport {\r\n createNotFoundRouteHandler,\r\n DuplicateNotFoundPageError,\r\n isNotFoundPageFile,\r\n NotFoundPageDeclaresRouteError,\r\n NOT_FOUND_ROUTE_NAME,\r\n NOT_FOUND_ROUTE_PATH,\r\n type RegisteredRouteShape,\r\n} from \"./not-found-page\";\r\nimport type { PageManifest, PageManifestLayoutEntry, PageManifestPageEntry } from \"./page-manifest\";\r\n\r\n/** A page declares either a bare path or a path plus an explicit route name. */\r\ntype PageRouteExport = string | { path: string; name?: string; cache?: PageCacheOptIn };\r\n\r\n/** The only export this module reads off a page module namespace. */\r\ntype PageModuleShape = {\r\n route?: PageRouteExport;\r\n};\r\n\r\n/** The exports this module reads off a layout module namespace. */\r\ntype LayoutModuleShape = {\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`). The manifest\r\n * carries LOADED modules, so this is a fact rather than a guess, exactly as it\r\n * is in dev (`install-page-routes.ts:145-150`).\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/**\r\n * How a handler is built for one page. Defaults to `createPageRouteHandler`;\r\n * taking it as an input keeps this module's own job — reading the manifest and\r\n * registering routes — provable without a render pipeline behind it.\r\n */\r\nexport type PageRouteHandlerFactory = (options: PageRouteHandlerOptions) => PageRouteHandler;\r\n\r\nexport type InstalledManifestPageRoute = {\r\n /** The canonical declared route path, before layout-prefix composition. */\r\n declaredPath: string;\r\n /** The composed path the route was registered on. */\r\n path: string;\r\n /** The resolved route name; shared namespace with API routes. */\r\n name: string;\r\n /** The page's manifest `sourceFile`. */\r\n file: string;\r\n /** The layout's manifest `sourceFile`, when the page has one. */\r\n layoutFile: string | undefined;\r\n};\r\n\r\nexport type InstallPageRoutesFromManifestOptions = {\r\n router: Router;\r\n /** The table the generated production barrel provided at import time. */\r\n manifest: PageManifest;\r\n /** Browser module loaded after the server-rendered application and payload. */\r\n hydrationClientModuleUrl?: string;\r\n /**\r\n * Where the client build wrote its output — `productionStylesheetUrls`'s own\r\n * `clientDir` argument, forwarded here rather than pre-read into a flat list:\r\n * each registered handler needs its OWN chain\r\n * (`[root, ...outer-to-inner matched layouts, page]`, matched by the\r\n * manifest's own `sourceFile` ids), not one list shared by every page.\r\n *\r\n * OPTIONAL for the same reason `PageManifest.clientDir` is: a build that\r\n * discovered zero pages emits no client bundle, so there is no directory to\r\n * read stylesheets from — and no page that could need one either.\r\n */\r\n clientDir?: string;\r\n /** Same helper `dev-server.ts` exports — passed in, never imported. */\r\n createHandler?: PageRouteHandlerFactory;\r\n};\r\n\r\n/**\r\n * `sourceFile`'s path relative to the web root — `src/web/**`, the only page\r\n * root discovery enumerates (`discoverWebRoots`,\r\n * `web/src/build/discover-pages.ts:210-213`). Manifest `sourceFile`s are\r\n * app-root-relative (`\"src/web/...\"`, `page-manifest.ts`'s own doc comment),\r\n * so dropping the first two segments — `<srcDir>`, then the literal `\"web\"` —\r\n * recovers exactly what `deriveFilesystemRoutePath`/`deriveFilesystemRouteName`\r\n * expect: the same value dev computes as `filesystemPageFileFor`\r\n * (`install-page-routes.ts:101-103`).\r\n */\r\nfunction webRelativeSourceFile(sourceFile: string): string {\r\n return sourceFile.split(\"/\").slice(2).join(\"/\");\r\n}\r\n\r\n/** Exported for `../routing/route-name-parity.spec.ts`, which proves this and dev's `resolvePageRouteIdentity` agree. */\r\nexport function resolveRoute(\r\n routeExport: PageRouteExport | undefined,\r\n sourceFile: string,\r\n): { path: string; name: string } {\r\n const pageFile = webRelativeSourceFile(sourceFile);\r\n\r\n if (routeExport === undefined) {\r\n return {\r\n path: deriveFilesystemRoutePath({ pageFile }),\r\n name: resolvePageRouteName(routeExport, pageFile),\r\n };\r\n }\r\n\r\n return {\r\n path: canonicalizeRouteExport(routeExport, sourceFile).path,\r\n name: resolvePageRouteName(routeExport, pageFile),\r\n };\r\n}\r\n\r\n/**\r\n * Every layout's declared `prefix`, keyed by its directory relative to the\r\n * web root — the same table dev builds as `LayoutLevel.prefixesByDirectory`\r\n * (`install-page-routes.ts:214-222`) and the one\r\n * {@link deriveFilesystemRoutePath} uses to let a directory's own layout\r\n * rename the URL segment a bare directory name would otherwise contribute.\r\n */\r\nfunction layoutPrefixesOf(page: PageManifestPageEntry): Record<string, string> {\r\n return Object.fromEntries(\r\n page.layouts.flatMap((layout) => {\r\n const prefix = (layout.module as LayoutModuleShape).prefix;\r\n\r\n if (prefix === undefined) return [];\r\n\r\n const relative = webRelativeSourceFile(layout.sourceFile);\r\n const slashIndex = relative.lastIndexOf(\"/\");\r\n const directory = slashIndex === -1 ? \"\" : relative.slice(0, slashIndex);\r\n\r\n return [[directory, prefix]];\r\n }),\r\n );\r\n}\r\n\r\n/**\r\n * The page's layout LEVEL, resolved from the whole chain the manifest carries\r\n * rather than from the one layout nearest to it — the same resolution dev makes\r\n * (`install-page-routes.ts:138-164`), against loaded modules instead of Vite's.\r\n *\r\n * The manifest carries the FULL chain, outermost first, and the render pipeline\r\n * has exactly one layout slot per page (`execute-page-request.ts`'s\r\n * `PageRouteEntry[\"triple\"]`), so the chain has to be collapsed into one module\r\n * before it reaches a handler. Two things collapse 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 entry's presence in\r\n * the chain — a `middleware`-only layout has no default export and is not a\r\n * wrapper. Passing bare `sourceFile` strings had every layout read as a\r\n * rendering one, so boot refused a middleware-only guard chain that the build\r\n * had already accepted: an application that builds and will not start.\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 own\r\n * directory knows nothing about is exactly the guard that must still run, and\r\n * a prefix nobody composed is a URL nobody wrote down.\r\n *\r\n * A chain with more than one RENDERING layout is still refused here, at boot,\r\n * before a single request can observe the wrong document. Like the missing\r\n * app-root refusal below, that arm defends against stale or hand-edited build\r\n * artifacts: the build refuses to emit such a chain, but a manifest can reach a\r\n * running process without that build having produced it.\r\n */\r\ntype LayoutLevel = {\r\n /**\r\n * The layout entry the handler's layout slot is registered under, or\r\n * `undefined` when the page has no layout at all: the layout that RENDERS,\r\n * or — when none does — the nearest one, which is the slot production has\r\n * always used and so the choice that changes nothing but the middleware for a\r\n * chain with no wrapper in it.\r\n */\r\n host: PageManifestLayoutEntry | undefined;\r\n /** Every layout's `prefix`, composed outermost first — `discoverPages`' own reduction. */\r\n prefix: string;\r\n};\r\n\r\nfunction layoutLevelOf(page: PageManifestPageEntry): LayoutLevel {\r\n const selection = selectPageLayout(\r\n page.layouts.map((layout) => ({\r\n layout: layout.sourceFile,\r\n renders: typeof (layout.module as LayoutModuleShape).default !== \"undefined\",\r\n })),\r\n );\r\n\r\n if (selection.type === \"rejected\") {\r\n throw new NestedLayoutsNotSupportedError(page.sourceFile, selection.layouts);\r\n }\r\n\r\n return {\r\n host:\r\n selection.type === \"selected\"\r\n ? page.layouts.find((layout) => layout.sourceFile === selection.layout)\r\n : page.layouts.at(-1),\r\n prefix: page.layouts.reduce(\r\n (composed, layout) =>\r\n composeRoutePath(composed, (layout.module as LayoutModuleShape).prefix ?? \"/\"),\r\n \"/\",\r\n ),\r\n };\r\n}\r\n\r\n/**\r\n * The layout slot's module for one page: the slot host's own namespace, with the\r\n * whole chain's middleware in place of its own — outermost first, which is the\r\n * order stage 3 runs the array in (`execute-page-request.ts:519-524`) and the\r\n * order an outer `optionalAuth` needs in order to have resolved an identity\r\n * before an inner `gate()` checks it.\r\n *\r\n * Deliberately NOT core's route-level `middleware` option: that runs before the\r\n * pipeline's App-level middleware, which would invert outermost-first — the one\r\n * property this composition exists to guarantee.\r\n *\r\n * Built once at registration, not per request: unlike dev, every module here is\r\n * already in memory and cannot change under a running process.\r\n */\r\nfunction composeLayoutLevel(\r\n page: PageManifestPageEntry,\r\n host: PageManifestLayoutEntry,\r\n): Record<string, unknown> {\r\n const hostIndex = page.layouts.indexOf(host);\r\n\r\n return {\r\n ...host.module,\r\n middleware: page.layouts.flatMap((layout) => [\r\n ...((layout.module as LayoutModuleShape).middleware ?? []),\r\n ]),\r\n loader: async (context: Parameters<NonNullable<LayoutModuleShape[\"loader\"]>>[0]) => {\r\n let hostData: unknown;\r\n\r\n for (let index = 0; index < page.layouts.length; index++) {\r\n const value = await (page.layouts[index].module as LayoutModuleShape).loader?.(context);\r\n\r\n if (value instanceof Response || isLoaderShortCircuit(value)) return value;\r\n if (index === hostIndex) hostData = value;\r\n }\r\n\r\n return hostData;\r\n },\r\n };\r\n}\r\n\r\n/**\r\n * Registers every page the manifest carries into `options.router`.\r\n *\r\n * An empty manifest registers nothing and is not an error: \"built with web, no\r\n * pages\" is a legal state of a built application, and treating it as a failure\r\n * would make an empty project unbootable. A manifest that DOES carry pages but\r\n * no app root is the opposite — every page renders inside the application root,\r\n * so that combination is a broken table rather than an empty one, and it is\r\n * refused before any route exists to serve a request with a missing root.\r\n *\r\n * Two pages composing to the same path is refused the moment the second one is\r\n * seen, naming both — a registration-time failure, rather than a route one of\r\n * them silently loses at runtime.\r\n */\r\nexport function installPageRoutesFromManifest(\r\n options: InstallPageRoutesFromManifestOptions,\r\n): InstalledManifestPageRoute[] {\r\n const {\r\n router,\r\n manifest,\r\n hydrationClientModuleUrl,\r\n clientDir,\r\n createHandler = createPageRouteHandler,\r\n } = options;\r\n\r\n if (manifest.pages.length === 0) return [];\r\n\r\n const app = manifest.app;\r\n\r\n if (app === undefined) {\r\n throw new Error(\r\n `installPageRoutesFromManifest: this build's page manifest carries ${manifest.pages.length} ` +\r\n \"page(s) but no application root. Every page renders inside the app component, so no \" +\r\n \"page can be registered without it. Re-run the build so the generated pages barrel \" +\r\n \"provides an `app` entry.\",\r\n );\r\n }\r\n\r\n // Ids are the manifest's own `sourceFile` strings and are passed on untouched:\r\n // the loader below matches them by exact string equality, so resolving,\r\n // joining or swapping separators on one side of that comparison would turn\r\n // every lookup into a miss.\r\n const loadModule = createPageModuleLoader(manifest);\r\n // The namespace is already statically imported by the generated barrel, but\r\n // do not hand it to the render pipeline until a request actually fails.\r\n const loadErrorPage =\r\n manifest.errorPage === undefined\r\n ? undefined\r\n : async () => manifest.errorPage!.module as ErrorPageModule;\r\n\r\n // Same partition development makes, on the same rule (the filename), so the\r\n // two modes cannot disagree about which file is the not-found page. It is\r\n // taken OUT of the registration loop rather than skipped inside it: every step\r\n // in there composes and claims a URL, and `404.page.tsx` has none.\r\n const notFoundPages = manifest.pages.filter((page) => isNotFoundPageFile(page.sourceFile));\r\n const pages = manifest.pages.filter((page) => !isNotFoundPageFile(page.sourceFile));\r\n\r\n if (notFoundPages.length > 1) {\r\n throw new DuplicateNotFoundPageError(notFoundPages.map((page) => page.sourceFile));\r\n }\r\n\r\n const notFoundPage = notFoundPages[0];\r\n\r\n if (notFoundPage !== undefined && (notFoundPage.module as PageModuleShape).route !== undefined) {\r\n throw new NotFoundPageDeclaresRouteError(notFoundPage.sourceFile);\r\n }\r\n\r\n const installed: InstalledManifestPageRoute[] = [];\r\n const fileByPath = new Map<string, string>();\r\n\r\n for (const page of pages) {\r\n const { host: layout, prefix: layoutPrefix } = layoutLevelOf(page);\r\n const routeExport = (page.module as PageModuleShape).route;\r\n\r\n const { path: routePath, name } = resolveRoute(routeExport, page.sourceFile);\r\n\r\n // Validated at INSTALL time — the same boot-time gate dev applies\r\n // (`install-page-routes.ts`) — so a malformed `cache` opt-in fails a\r\n // production boot instead of shipping a page whose freshness window the\r\n // framework silently guessed.\r\n const cache = resolvePageRouteCache(routeExport, page.sourceFile);\r\n\r\n // Explicit wins; otherwise the path is derived from the page's own source\r\n // location and the layouts on its path — the same rule dev applies at\r\n // registration (`install-page-routes.ts:377-382`) and discovery applies at\r\n // build (`discover-pages.ts:925-935`), read here off the manifest's own\r\n // `sourceFile`s instead of the filesystem.\r\n const effectivePath =\r\n routeExport === undefined\r\n ? deriveFilesystemRoutePath({\r\n pageFile: webRelativeSourceFile(page.sourceFile),\r\n layoutPrefixes: layoutPrefixesOf(page),\r\n })\r\n : composeRoutePath(layoutPrefix, routePath);\r\n const existingFile = fileByPath.get(effectivePath);\r\n\r\n if (existingFile) {\r\n throw new Error(\r\n `installPageRoutesFromManifest: composed route path \"${effectivePath}\" (layout ` +\r\n `prefix \"${layoutPrefix}\" + route.path \"${routePath}\") is declared by two pages — ` +\r\n `\"${existingFile}\" and \"${page.sourceFile}\". Every page's composed route path must ` +\r\n \"be unique.\",\r\n );\r\n }\r\n\r\n fileByPath.set(effectivePath, page.sourceFile);\r\n\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 the\r\n // manifest lookup. A one-layout chain has nothing to compose, so it is left\r\n // to resolve as the exact namespace object the manifest carries, untouched.\r\n const composedLayout =\r\n page.layouts.length > 1 && layout !== undefined\r\n ? composeLayoutLevel(page, layout)\r\n : undefined;\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 (`page.layouts`,\r\n // the manifest's own chain — the same one dev walks as\r\n // `layoutLevel.chain`), then the page. `PageManifest.clientDir` is present\r\n // whenever `pages` is non-empty (`page-manifest.ts`), which this loop only\r\n // ever reaches when it is — `clientDir === undefined` is handled anyway,\r\n // rather than trusted away, because a caller can still pass this function\r\n // a manifest that violates its own generator's invariant.\r\n const stylesheetUrls =\r\n clientDir === undefined\r\n ? []\r\n : productionStylesheetUrls(clientDir, [\r\n app.sourceFile,\r\n ...page.layouts.map((pageLayout) => pageLayout.sourceFile),\r\n page.sourceFile,\r\n ]);\r\n\r\n router.get(\r\n effectivePath,\r\n createHandler({\r\n path: effectivePath,\r\n name,\r\n appFile: app.sourceFile,\r\n pageFile: page.sourceFile,\r\n layoutFile: layout?.sourceFile,\r\n loadModule:\r\n composedLayout === undefined\r\n ? loadModule\r\n : (moduleId) =>\r\n moduleId === layout?.sourceFile\r\n ? Promise.resolve(composedLayout)\r\n : loadModule(moduleId),\r\n loadRegistrationLayouts: () => Promise.resolve(page.layouts.map((layout) => layout.module)),\r\n hydrationClientModuleUrl,\r\n loadErrorPage,\r\n stylesheetUrls,\r\n cache,\r\n }),\r\n // `isPage` marks this route as SSR-served. Pages and API routes share one\r\n // router and one route-name namespace, so the router's duplicate-name\r\n // error reads this flag to say which claimant is the page.\r\n { name, isPage: true },\r\n );\r\n\r\n installed.push({\r\n declaredPath: routePath,\r\n path: effectivePath,\r\n name,\r\n file: page.sourceFile,\r\n layoutFile: layout?.sourceFile,\r\n });\r\n }\r\n\r\n /*\r\n THE CATCH-ALL — the same route dev registers, built the same way, differing\r\n only in where a module comes from. Registered last, and registered even when\r\n the build carried no `404.page.tsx`, so a production deployment answers 404\r\n with the right STATUS whether or not anyone has designed the page yet.\r\n */\r\n router.get(\r\n NOT_FOUND_ROUTE_PATH,\r\n createNotFoundRouteHandler({\r\n renderPage:\r\n notFoundPage === undefined\r\n ? undefined\r\n : createHandler({\r\n path: NOT_FOUND_ROUTE_PATH,\r\n name: NOT_FOUND_ROUTE_NAME,\r\n appFile: app.sourceFile,\r\n pageFile: notFoundPage.sourceFile,\r\n // No layout, and therefore no layout middleware — see the dev\r\n // installer for why the not-found path takes nothing that can\r\n // redirect or throw.\r\n layoutFile: undefined,\r\n loadModule,\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:\r\n clientDir === undefined\r\n ? []\r\n : productionStylesheetUrls(clientDir, [app.sourceFile, notFoundPage.sourceFile]),\r\n matchPath: (requestPath) => requestPath,\r\n statusForRenderedOk: 404,\r\n skipPageLoader: true,\r\n }),\r\n }),\r\n // `isPage` for the same reason the dev installer carries it — the router's\r\n // duplicate-name error reads the flag to say which claimant is the page.\r\n { name: NOT_FOUND_ROUTE_NAME, isPage: true },\r\n );\r\n\r\n /*\r\n Same publish as the dev installer, for the same reason: `href()` and the\r\n router must agree, and they only can if both read the one loop that\r\n registered the routes. Production installs once at boot, so the wholesale\r\n replacement is a single write before the first request.\r\n */\r\n publishRouteTable(installed, \"installPageRoutesFromManifest (production)\");\r\n\r\n return installed;\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"install-page-routes-from-manifest.mjs","names":[],"sources":["../../../../../../../web/src/server/install-page-routes-from-manifest.ts"],"sourcesContent":["/**\r\n * Page-route registration for a built application.\r\n *\r\n * `installPageRoutes` answers \"which pages exist?\" by walking the filesystem\r\n * and \"what is this module?\" by asking Vite to evaluate it. Neither question\r\n * can be asked of a running production process: there is no `app/` tree beside\r\n * the bundle and no Vite. Both answers were therefore moved to build time — the\r\n * generated `pages.ts` barrel statically imported every page, layout and the\r\n * app root and handed them over as a {@link PageManifest}, and this module\r\n * turns that table into registered routes.\r\n *\r\n * WHAT IS DELIBERATELY IDENTICAL TO DEVELOPMENT: the route a page ends up on,\r\n * and the guards that run before it renders. A page's `route` export and the\r\n * `prefix` and `middleware` exports of EVERY layout on its path are read off the\r\n * module namespaces here, at boot, and composed by the same rules dev composes\r\n * them by — a shared mechanism now, not a promise: {@link layoutLevelOf} calls\r\n * `../routing/layout-level.ts`'s `resolveLayoutLevel`, the same selection and\r\n * prefix-composition rule dev's own `resolveLayoutLevel` calls, and\r\n * {@link composeLayoutLevel} folds loaders through the same\r\n * `./fold-layout-loaders.ts` dev's does — so the URL a page answers on and the\r\n * chain that guards it are decided by the page's own source in both modes, and\r\n * a build cannot quietly disagree with the dev server about either.\r\n *\r\n * WHAT IS DELIBERATELY DIFFERENT: this is synchronous. Every module is already\r\n * in memory, so registration has nothing to await; the loader handed to each\r\n * handler is a lookup over the same table, not an evaluation step.\r\n */\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 } from \"../routing/layout-level\";\r\nimport {\r\n resolvePageRouteCache,\r\n resolvePageRouteIdentity,\r\n type PageCacheOptIn,\r\n} from \"../routing/route-identity\";\r\nimport { publishRouteTable } from \"../routing/route-table\";\r\nimport { type Router } from \"@warlock.js/core\";\r\nimport { createPageModuleLoader } from \"./create-page-module-loader\";\r\nimport type { ErrorPageModule } from \"./error-page\";\r\nimport {\r\n createPageRouteHandler,\r\n type PageRouteHandler,\r\n type PageRouteHandlerOptions,\r\n} from \"./create-page-route-handler\";\r\nimport type { PipelineLoader, PipelineMiddleware } from \"./execute-page-request\";\r\nimport { foldLayoutLoaders } from \"./fold-layout-loaders\";\r\nimport { productionStylesheetUrls } 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\nimport type { PageManifest, PageManifestLayoutEntry, PageManifestPageEntry } from \"./page-manifest\";\r\n\r\n/** A page declares either a bare path or a path plus an explicit route name. */\r\ntype PageRouteExport = string | { path: string; name?: string; cache?: PageCacheOptIn };\r\n\r\n/** The only export this module reads off a page module namespace. */\r\ntype PageModuleShape = {\r\n route?: PageRouteExport;\r\n};\r\n\r\n/** The exports this module reads off a layout module namespace. */\r\ntype LayoutModuleShape = {\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`). The manifest\r\n * carries LOADED modules, so this is a fact rather than a guess, exactly as it\r\n * is in dev's own `LayoutModuleShape`.\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/**\r\n * How a handler is built for one page. Defaults to `createPageRouteHandler`;\r\n * taking it as an input keeps this module's own job — reading the manifest and\r\n * registering routes — provable without a render pipeline behind it.\r\n */\r\nexport type PageRouteHandlerFactory = (options: PageRouteHandlerOptions) => PageRouteHandler;\r\n\r\nexport type InstalledManifestPageRoute = {\r\n /** The canonical declared route path, before layout-prefix composition. */\r\n declaredPath: string;\r\n /** The composed path the route was registered on. */\r\n path: string;\r\n /** The resolved route name; shared namespace with API routes. */\r\n name: string;\r\n /** The page's manifest `sourceFile`. */\r\n file: string;\r\n /** The layout's manifest `sourceFile`, when the page has one. */\r\n layoutFile: string | undefined;\r\n};\r\n\r\nexport type InstallPageRoutesFromManifestOptions = {\r\n router: Router;\r\n /** The table the generated production barrel provided at import time. */\r\n manifest: PageManifest;\r\n /** Browser module loaded after the server-rendered application and payload. */\r\n hydrationClientModuleUrl?: string;\r\n /**\r\n * Where the client build wrote its output — `productionStylesheetUrls`'s own\r\n * `clientDir` argument, forwarded here rather than pre-read into a flat list:\r\n * each registered handler needs its OWN chain\r\n * (`[root, ...outer-to-inner matched layouts, page]`, matched by the\r\n * manifest's own `sourceFile` ids), not one list shared by every page.\r\n *\r\n * OPTIONAL for the same reason `PageManifest.clientDir` is: a build that\r\n * discovered zero pages emits no client bundle, so there is no directory to\r\n * read stylesheets from — and no page that could need one either.\r\n */\r\n clientDir?: string;\r\n /** Same helper `dev-error-transport.ts` exports — passed in, never imported. */\r\n createHandler?: PageRouteHandlerFactory;\r\n};\r\n\r\n/**\r\n * `sourceFile`'s path relative to the web root — `src/web/**`, the only page\r\n * root discovery enumerates (`discoverWebRoots`,\r\n * `web/src/build/discover-pages.ts:210-213`). Manifest `sourceFile`s are\r\n * app-root-relative (`\"src/web/...\"`, `page-manifest.ts`'s own doc comment),\r\n * so dropping the first two segments — `<srcDir>`, then the literal `\"web\"` —\r\n * recovers exactly what `deriveFilesystemRoutePath`/`deriveFilesystemRouteName`\r\n * expect: the same value dev computes as `install-page-routes.ts`'s\r\n * `filesystemPageFileFor`.\r\n */\r\nfunction webRelativeSourceFile(sourceFile: string): string {\r\n return sourceFile.split(\"/\").slice(2).join(\"/\");\r\n}\r\n\r\n/**\r\n * Exported for `../routing/route-name-parity.spec.ts`, which proves this and\r\n * dev's own call agree — both now call the same `../routing/route-identity.ts`\r\n * `resolvePageRouteIdentity`, so this wrapper's only job is supplying THIS\r\n * installer's identifiers: the manifest `sourceFile` doubles as the\r\n * `route.path` rejection context, and its web-root-relative form is the\r\n * filesystem-derivation input.\r\n */\r\nexport function resolveRoute(\r\n routeExport: PageRouteExport | undefined,\r\n sourceFile: string,\r\n): { path: string; name: string } {\r\n return resolvePageRouteIdentity(routeExport, webRelativeSourceFile(sourceFile), sourceFile);\r\n}\r\n\r\n/**\r\n * Every layout's declared `prefix`, keyed by its directory relative to the\r\n * web root — the same table dev builds as its own `LayoutLevel.prefixesByDirectory`,\r\n * and the one {@link deriveFilesystemRoutePath} uses to let a directory's own\r\n * layout rename the URL segment a bare directory name would otherwise\r\n * contribute.\r\n */\r\nfunction layoutPrefixesOf(page: PageManifestPageEntry): Record<string, string> {\r\n return Object.fromEntries(\r\n page.layouts.flatMap((layout) => {\r\n const prefix = (layout.module as LayoutModuleShape).prefix;\r\n\r\n if (prefix === undefined) return [];\r\n\r\n const relative = webRelativeSourceFile(layout.sourceFile);\r\n const slashIndex = relative.lastIndexOf(\"/\");\r\n const directory = slashIndex === -1 ? \"\" : relative.slice(0, slashIndex);\r\n\r\n return [[directory, prefix]];\r\n }),\r\n );\r\n}\r\n\r\n/**\r\n * The page's layout LEVEL, resolved from the whole chain the manifest carries\r\n * rather than from the one layout nearest to it. The selection and prefix\r\n * rules themselves do not depend on where a layout's module came from, so\r\n * they live in one place both installers call —\r\n * `../routing/layout-level.ts`'s `resolveLayoutLevel` — rather than being\r\n * re-derived here against loaded modules instead of Vite's. This function's\r\n * own job is reading THIS installer's inputs off the manifest (`renders` and\r\n * `prefix` off each already-loaded module, `host` recovered from the shared\r\n * result's `hostId`) and nothing else.\r\n *\r\n * The manifest carries the FULL chain, outermost first, and the render pipeline\r\n * has exactly one layout slot per page (`execute-page-request.ts`'s\r\n * `PageRouteEntry[\"triple\"]`), so the chain has to be collapsed into one module\r\n * before it reaches a handler. Two things collapse 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 entry's presence in\r\n * the chain — a `middleware`-only layout has no default export and is not a\r\n * wrapper. Passing bare `sourceFile` strings had every layout read as a\r\n * rendering one, so boot refused a middleware-only guard chain that the build\r\n * had already accepted: an application that builds and will not start.\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 own\r\n * directory knows nothing about is exactly the guard that must still run, and\r\n * a prefix nobody composed is a URL nobody wrote down.\r\n *\r\n * A chain with more than one RENDERING layout is still refused here, at boot,\r\n * before a single request can observe the wrong document (raised by the\r\n * shared `resolveLayoutLevel` itself). Like the missing app-root refusal\r\n * below, that defends against stale or hand-edited build artifacts: the build\r\n * refuses to emit such a chain, but a manifest can reach a running process\r\n * without that build having produced it.\r\n */\r\ntype LayoutLevel = {\r\n /**\r\n * The layout entry the handler's layout slot is registered under, or\r\n * `undefined` when the page has no layout at all: the layout that RENDERS,\r\n * or — when none does — the nearest one, which is the slot production has\r\n * always used and so the choice that changes nothing but the middleware for a\r\n * chain with no wrapper in it.\r\n */\r\n host: PageManifestLayoutEntry | undefined;\r\n /** Every layout's `prefix`, composed outermost first — `discoverPages`' own reduction. */\r\n prefix: string;\r\n};\r\n\r\nfunction layoutLevelOf(page: PageManifestPageEntry): LayoutLevel {\r\n const level = resolveLayoutLevel(\r\n page.sourceFile,\r\n page.layouts.map((layout) => ({\r\n id: layout.sourceFile,\r\n renders: typeof (layout.module as LayoutModuleShape).default !== \"undefined\",\r\n prefix: (layout.module as LayoutModuleShape).prefix,\r\n })),\r\n );\r\n\r\n return {\r\n host: page.layouts.find((layout) => layout.sourceFile === level.hostId),\r\n prefix: level.prefix,\r\n };\r\n}\r\n\r\n/**\r\n * The layout slot's module for one page: the slot host's own namespace, with the\r\n * whole chain's middleware in place of its own — outermost first, which is the\r\n * order stage 3 runs the array in (`execute-page-request.ts:519-524`) and the\r\n * order an outer `optionalAuth` needs in order to have resolved an identity\r\n * before an inner `gate()` checks it.\r\n *\r\n * Deliberately NOT core's route-level `middleware` option: that runs before the\r\n * pipeline's App-level middleware, which would invert outermost-first — the one\r\n * property this composition exists to guarantee.\r\n *\r\n * Built once at registration, not per request: unlike dev, every module here is\r\n * already in memory and cannot change under a running process.\r\n */\r\nfunction composeLayoutLevel(\r\n page: PageManifestPageEntry,\r\n host: PageManifestLayoutEntry,\r\n): Record<string, unknown> {\r\n const hostIndex = page.layouts.indexOf(host);\r\n\r\n return {\r\n ...host.module,\r\n middleware: page.layouts.flatMap((layout) => [\r\n ...((layout.module as LayoutModuleShape).middleware ?? []),\r\n ]),\r\n loader: foldLayoutLoaders(\r\n page.layouts.map((layout) => (layout.module as LayoutModuleShape).loader),\r\n hostIndex,\r\n ),\r\n };\r\n}\r\n\r\n/**\r\n * Registers every page the manifest carries into `options.router`.\r\n *\r\n * An empty manifest registers nothing and is not an error: \"built with web, no\r\n * pages\" is a legal state of a built application, and treating it as a failure\r\n * would make an empty project unbootable. A manifest that DOES carry pages but\r\n * no app root is the opposite — every page renders inside the application root,\r\n * so that combination is a broken table rather than an empty one, and it is\r\n * refused before any route exists to serve a request with a missing root.\r\n *\r\n * Two pages composing to the same path is refused the moment the second one is\r\n * seen, naming both — a registration-time failure, rather than a route one of\r\n * them silently loses at runtime.\r\n */\r\nexport function installPageRoutesFromManifest(\r\n options: InstallPageRoutesFromManifestOptions,\r\n): InstalledManifestPageRoute[] {\r\n const {\r\n router,\r\n manifest,\r\n hydrationClientModuleUrl,\r\n clientDir,\r\n createHandler = createPageRouteHandler,\r\n } = options;\r\n\r\n if (manifest.pages.length === 0) return [];\r\n\r\n const app = manifest.app;\r\n\r\n if (app === undefined) {\r\n throw new Error(\r\n `installPageRoutesFromManifest: this build's page manifest carries ${manifest.pages.length} ` +\r\n \"page(s) but no application root. Every page renders inside the app component, so no \" +\r\n \"page can be registered without it. Re-run the build so the generated pages barrel \" +\r\n \"provides an `app` entry.\",\r\n );\r\n }\r\n\r\n // Ids are the manifest's own `sourceFile` strings and are passed on untouched:\r\n // the loader below matches them by exact string equality, so resolving,\r\n // joining or swapping separators on one side of that comparison would turn\r\n // every lookup into a miss.\r\n const loadModule = createPageModuleLoader(manifest);\r\n // The namespace is already statically imported by the generated barrel, but\r\n // do not hand it to the render pipeline until a request actually fails.\r\n const loadErrorPage =\r\n manifest.errorPage === undefined\r\n ? undefined\r\n : async () => manifest.errorPage!.module as ErrorPageModule;\r\n\r\n // Same partition development makes, on the same rule (the filename), so the\r\n // two modes cannot disagree about which file is the not-found page. It is\r\n // taken OUT of the registration loop rather than skipped inside it: every step\r\n // in there composes and claims a URL, and `404.page.tsx` has none.\r\n const notFoundPages = manifest.pages.filter((page) => isNotFoundPageFile(page.sourceFile));\r\n const pages = manifest.pages.filter((page) => !isNotFoundPageFile(page.sourceFile));\r\n\r\n if (notFoundPages.length > 1) {\r\n throw new DuplicateNotFoundPageError(notFoundPages.map((page) => page.sourceFile));\r\n }\r\n\r\n const notFoundPage = notFoundPages[0];\r\n\r\n if (notFoundPage !== undefined && (notFoundPage.module as PageModuleShape).route !== undefined) {\r\n throw new NotFoundPageDeclaresRouteError(notFoundPage.sourceFile);\r\n }\r\n\r\n const installed: InstalledManifestPageRoute[] = [];\r\n const fileByPath = new Map<string, string>();\r\n\r\n for (const page of pages) {\r\n const { host: layout, prefix: layoutPrefix } = layoutLevelOf(page);\r\n const routeExport = (page.module as PageModuleShape).route;\r\n\r\n const { path: routePath, name } = resolveRoute(routeExport, page.sourceFile);\r\n\r\n // Validated at INSTALL time — the same boot-time gate dev applies in its\r\n // own installer — so a malformed `cache` opt-in fails a production boot\r\n // instead of shipping a page whose freshness window the framework\r\n // silently guessed.\r\n const cache = resolvePageRouteCache(routeExport, page.sourceFile);\r\n\r\n // Explicit wins; otherwise the path is derived from the page's own source\r\n // location and the layouts on its path — the same rule dev applies at\r\n // registration and discovery applies at build (`discover-pages.ts`), read\r\n // here off the manifest's own `sourceFile`s instead of the filesystem.\r\n const effectivePath =\r\n routeExport === undefined\r\n ? deriveFilesystemRoutePath({\r\n pageFile: webRelativeSourceFile(page.sourceFile),\r\n layoutPrefixes: layoutPrefixesOf(page),\r\n })\r\n : composeRoutePath(layoutPrefix, routePath);\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: page.sourceFile,\r\n composition: { layoutPrefix, routePath },\r\n }),\r\n );\r\n }\r\n\r\n fileByPath.set(effectivePath, page.sourceFile);\r\n\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 the\r\n // manifest lookup. A one-layout chain has nothing to compose, so it is left\r\n // to resolve as the exact namespace object the manifest carries, untouched.\r\n const composedLayout =\r\n page.layouts.length > 1 && layout !== undefined\r\n ? composeLayoutLevel(page, layout)\r\n : undefined;\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 (`page.layouts`,\r\n // the manifest's own chain — the same one dev walks as\r\n // `layoutLevel.chain`), then the page. `PageManifest.clientDir` is present\r\n // whenever `pages` is non-empty (`page-manifest.ts`), which this loop only\r\n // ever reaches when it is — `clientDir === undefined` is handled anyway,\r\n // rather than trusted away, because a caller can still pass this function\r\n // a manifest that violates its own generator's invariant.\r\n const stylesheetUrls =\r\n clientDir === undefined\r\n ? []\r\n : productionStylesheetUrls(clientDir, [\r\n app.sourceFile,\r\n ...page.layouts.map((pageLayout) => pageLayout.sourceFile),\r\n page.sourceFile,\r\n ]);\r\n\r\n router.get(\r\n effectivePath,\r\n createHandler({\r\n path: effectivePath,\r\n name,\r\n appFile: app.sourceFile,\r\n pageFile: page.sourceFile,\r\n layoutFile: layout?.sourceFile,\r\n loadModule:\r\n composedLayout === undefined\r\n ? loadModule\r\n : (moduleId) =>\r\n moduleId === layout?.sourceFile\r\n ? Promise.resolve(composedLayout)\r\n : loadModule(moduleId),\r\n loadRegistrationLayouts: () => Promise.resolve(page.layouts.map((layout) => layout.module)),\r\n hydrationClientModuleUrl,\r\n loadErrorPage,\r\n stylesheetUrls,\r\n cache,\r\n }),\r\n // `isPage` marks this route as SSR-served. Pages and API routes share one\r\n // router and one route-name namespace, so the router's duplicate-name\r\n // error reads this flag to say which claimant is the page.\r\n { name, isPage: true },\r\n );\r\n\r\n installed.push({\r\n declaredPath: routePath,\r\n path: effectivePath,\r\n name,\r\n file: page.sourceFile,\r\n layoutFile: layout?.sourceFile,\r\n });\r\n }\r\n\r\n /*\r\n THE CATCH-ALL — the same route dev registers, built the same way, differing\r\n only in where a module comes from. Registered last, and registered even when\r\n the build carried no `404.page.tsx`, so a production deployment answers 404\r\n with the right STATUS whether or not anyone has designed the page yet.\r\n */\r\n router.get(\r\n NOT_FOUND_ROUTE_PATH,\r\n createNotFoundRouteHandler({\r\n renderPage:\r\n notFoundPage === undefined\r\n ? undefined\r\n : createHandler({\r\n path: NOT_FOUND_ROUTE_PATH,\r\n name: NOT_FOUND_ROUTE_NAME,\r\n appFile: app.sourceFile,\r\n pageFile: notFoundPage.sourceFile,\r\n // No layout, and therefore no layout middleware — see the dev\r\n // installer for why the not-found path takes nothing that can\r\n // redirect or throw.\r\n layoutFile: undefined,\r\n loadModule,\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:\r\n clientDir === undefined\r\n ? []\r\n : productionStylesheetUrls(clientDir, [app.sourceFile, notFoundPage.sourceFile]),\r\n matchPath: (requestPath) => requestPath,\r\n statusForRenderedOk: 404,\r\n skipPageLoader: true,\r\n }),\r\n }),\r\n // `isPage` for the same reason the dev installer carries it — the router's\r\n // duplicate-name error reads the flag to say which claimant is the page.\r\n { name: NOT_FOUND_ROUTE_NAME, isPage: true },\r\n );\r\n\r\n /*\r\n Same publish as the dev installer, for the same reason: `href()` and the\r\n router must agree, and they only can if both read the one loop that\r\n registered the routes. Production installs once at boot, so the wholesale\r\n replacement is a single write before the first request.\r\n */\r\n publishRouteTable(installed, \"installPageRoutesFromManifest (production)\");\r\n\r\n return installed;\r\n}\r\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { composeRoutePath } from "../routing/compose-route-path.mjs";
2
2
  import { PageCacheOptIn } from "../routing/route-identity.mjs";
3
3
  import { PipelineLoader, PipelineMiddleware } from "./execute-page-request.types.mjs";
4
- import { Router } from "@warlock.js/core";
4
+ import { FastifyInstance, Router } from "@warlock.js/core";
5
5
  import { ViteDevServer } from "vite";
6
6
 
7
7
  //#region ../web/src/server/install-page-routes.d.ts
@@ -39,7 +39,7 @@ type InstallPageRoutesOptions = {
39
39
  appSrcRoot: string; /** v5/app/src/web/root.tsx — the single global app-root file. */
40
40
  appFile: string;
41
41
  /**
42
- * The application root Vite's dev server serves from — `dev-server.ts`'s
42
+ * The application root Vite's dev server serves from — `dev-error-transport.ts`'s
43
43
  * `paths.appRoot`, i.e. `<appRoot>/src === appSrcRoot` by default. Every
44
44
  * handler's stylesheet URLs are expressed relative to THIS, because that is
45
45
  * the root Vite's dev server actually resolves `/…` URLs against
@@ -64,6 +64,26 @@ type InstallPageRoutesOptions = {
64
64
  * page's own CSS" without also carrying every other page's.
65
65
  */
66
66
  stylesheetUrls?: readonly string[];
67
+ /**
68
+ * The Fastify instance `HttpConnector.boot()` published, resolved by the
69
+ * caller on the NODE side (`web-connector.ts`) and forwarded to every
70
+ * `createPageRouteHandler` call below instead of letting that factory read
71
+ * `container.get("http.server")` for itself.
72
+ *
73
+ * Load-bearing, not a convenience: Vite's SSR module runner
74
+ * (`vite.ssrLoadModule`, used throughout this file) evaluates
75
+ * `create-page-route-handler.ts` as a SECOND copy of `@warlock.js/core`, with
76
+ * its own `container` that `HttpConnector.boot()` never wrote to. Reading the
77
+ * container from inside that SSR graph therefore always misses, however
78
+ * early or late this file calls it — the value has to arrive as a plain
79
+ * argument from a caller on the Node side, where the real container lives.
80
+ *
81
+ * OPTIONAL so existing callers (and this file's own unit tests, which seed
82
+ * `container.set("http.server", …)` instead) keep resolving through the
83
+ * container exactly as before — omitted here means "not supplied", the same
84
+ * distinction `createPageRouteHandler` itself draws from `PageRouteHandlerOptions.httpServer`.
85
+ */
86
+ httpServer?: FastifyInstance;
67
87
  };
68
88
  /**
69
89
  * Registers every discoverable page into `options.router`. Throws
@@ -1,15 +1,18 @@
1
1
  import { publishRouteTable } from "../routing/route-table.mjs";
2
- import { isLoaderShortCircuit } from "./settle-page-response.mjs";
3
2
  import { composeRoutePath } from "../routing/compose-route-path.mjs";
3
+ import { duplicateRoutePathMessage } from "../routing/duplicate-route-path.mjs";
4
+ import { PageFileSegmentNotSupportedError } from "../routing/page-file-segment.mjs";
4
5
  import { deriveFilesystemRoutePath } from "../routing/filesystem-route.mjs";
5
- import { NestedLayoutsNotSupportedError, selectPageLayout } from "../routing/layout-policy.mjs";
6
- import { canonicalizeRouteExport, resolvePageRouteCache, resolvePageRouteName } from "../routing/route-identity.mjs";
6
+ import { resolveLayoutLevel as resolveLayoutLevel$1 } from "../routing/layout-level.mjs";
7
+ import { resolvePageRouteCache, resolvePageRouteIdentity, resolvePageRouteName } from "../routing/route-identity.mjs";
7
8
  import { createPageRouteHandler } from "./create-page-route-handler.mjs";
9
+ import { foldLayoutLoaders } from "./fold-layout-loaders.mjs";
8
10
  import { devHandlerStylesheetUrls } from "./stylesheet-urls.mjs";
9
11
  import { DuplicateNotFoundPageError, NOT_FOUND_ROUTE_NAME, NOT_FOUND_ROUTE_PATH, NotFoundPageDeclaresRouteError, createNotFoundRouteHandler, isNotFoundPageFile } from "./not-found-page.mjs";
10
12
  import { NonLiteralRouteExportError, readRouteExports } from "../build/read-route-exports.mjs";
11
- import { ErrorPageDeclaresRouteError, discoverPageFiles, isErrorPageFile, layoutChainFor, toPosix } from "../build/discover-pages.mjs";
12
- import { Response } from "@warlock.js/core";
13
+ import { toPosix } from "../shared/to-posix.mjs";
14
+ import { ErrorPageDeclaresRouteError, discoverPageFiles, isErrorPageFile, layoutChainFor } from "../build/discover-pages.mjs";
15
+ import "@warlock.js/core";
13
16
  import path from "node:path";
14
17
 
15
18
  //#region ../web/src/server/install-page-routes.ts
@@ -20,10 +23,10 @@ import path from "node:path";
20
23
  * the sanctioned dev-server dispatch path (server matching is Warlock's
21
24
  * router; there is no second server matcher) — picks
22
25
  * it up. Replaces the two hand-rolled `fastify.get()` calls this file's
23
- * sibling, `dev-server.ts`, used to make directly.
26
+ * sibling, `dev-error-transport.ts`, used to make directly.
24
27
  *
25
28
  * DELIBERATE EXCEPTION to "web has no core dependency", same
26
- * reasoning `dev-server.ts`'s own header comment records: this module is not
29
+ * reasoning `dev-error-transport.ts`'s own header comment records: this module is not
27
30
  * exported from either package barrel and is not part of `web/package.json`'s
28
31
  * dependency graph — dev/CLI bootstrap only.
29
32
  *
@@ -63,34 +66,82 @@ function filesystemPageFileFor(pageFile, appSrcRoot) {
63
66
  return toPosix(path.relative(path.join(appSrcRoot, "web"), pageFile));
64
67
  }
65
68
  /**
66
- * Resolve the stable identity used to distinguish a route-export edit from an
67
- * ordinary component-body edit. The declared path is retained before layout
68
- * composition so `/settings` under `/admin` compares with the next declared
69
- * `/settings`, not with the effective `/admin/settings` route.
69
+ * Raised when a page's own module fails to load in dev (`vite.ssrLoadModule`
70
+ * rejects on the page file itself never a layout, which this class does not
71
+ * cover).
72
+ *
73
+ * Before this class existed, that rejection propagated straight out of
74
+ * `installPageRoutes`'s loop and failed the WHOLE install: every other page's
75
+ * route went unregistered along with the broken one, and the failure named
76
+ * neither the page file nor which module actually threw. One bad page taking
77
+ * every other page down with it is a worse outage than the bad page alone, so
78
+ * this error is what the broken page's own route now fails with instead —
79
+ * named, with its cause attached, while the rest of the application keeps
80
+ * serving.
70
81
  */
71
- function resolvePageRouteIdentity(routeExport, pageFile, appSrcRoot) {
82
+ var PageModuleLoadError = class extends Error {
83
+ pageFile;
84
+ constructor(pageFile, cause) {
85
+ const rawCause = cause instanceof Error ? cause.message : String(cause);
86
+ const causeMessage = /[.!?]$/.test(rawCause.trim()) ? rawCause.trim() : `${rawCause.trim()}.`;
87
+ super(`"${pageFile}" failed to load: ${causeMessage} Every other page still installed and is still serving; fix the error in this page's module and it will start serving again.`, { cause });
88
+ this.pageFile = pageFile;
89
+ this.name = "PageModuleLoadError";
90
+ }
91
+ };
92
+ /**
93
+ * Registers a page whose OWN module failed to load at its filesystem-derived
94
+ * URL, so a request there reports {@link PageModuleLoadError} — naming the
95
+ * page file and the underlying cause — instead of a bare 404 that explains
96
+ * nothing.
97
+ *
98
+ * Only the filesystem-derived path is attempted: the page's `route` export
99
+ * cannot be read (that requires the very module that failed to load), and its
100
+ * layout chain is not resolved either, so no layout prefix composes into this
101
+ * path. When the filesystem path itself is not derivable
102
+ * (`deriveFilesystemRoutePath` rejects a segment), there is no path left to
103
+ * register a route at, so nothing is registered — the caller reports both
104
+ * failures loudly at boot and moves on.
105
+ */
106
+ async function registerFailedPageRoute(input) {
107
+ const { router, pageFile, appSrcRoot, loadError, fileByPath } = input;
108
+ const attributed = new PageModuleLoadError(pageFile, loadError);
72
109
  const filesystemPageFile = filesystemPageFileFor(pageFile, appSrcRoot);
73
- if (routeExport === void 0) return {
74
- declaredPath: deriveFilesystemRoutePath({ pageFile: filesystemPageFile }),
75
- name: resolvePageRouteName(routeExport, filesystemPageFile)
76
- };
77
- return {
78
- declaredPath: canonicalizeRouteExport(routeExport, pageFile).path,
79
- name: resolvePageRouteName(routeExport, filesystemPageFile)
80
- };
110
+ let effectivePath;
111
+ try {
112
+ effectivePath = deriveFilesystemRoutePath({ pageFile: filesystemPageFile });
113
+ } catch (segmentError) {
114
+ if (!(segmentError instanceof PageFileSegmentNotSupportedError)) throw segmentError;
115
+ console.error(attributed.message);
116
+ console.error(segmentError.message);
117
+ return;
118
+ }
119
+ const existingFile = fileByPath.get(effectivePath);
120
+ if (existingFile) throw new Error(duplicateRoutePathMessage({
121
+ effectivePath,
122
+ existingFile,
123
+ newFile: pageFile
124
+ }));
125
+ fileByPath.set(effectivePath, pageFile);
126
+ await router.withSourceFile(canonicalSourceFileFor(pageFile, appSrcRoot), () => router.get(effectivePath, async () => {
127
+ throw attributed;
128
+ }, {
129
+ name: resolvePageRouteName(void 0, filesystemPageFile),
130
+ isPage: true
131
+ }));
81
132
  }
82
133
  async function resolveLayoutLevel(pageFile, webRoot, loadLayout) {
83
134
  const chain = layoutChainFor(pageFile, webRoot);
84
135
  const modules = await Promise.all(chain.map(loadLayout));
85
- const selection = selectPageLayout(chain.map((layout, index) => ({
86
- layout,
87
- renders: typeof modules[index].default !== "undefined"
136
+ const level = resolveLayoutLevel$1(pageFile, chain.map((layoutFile, index) => ({
137
+ id: layoutFile,
138
+ renders: typeof modules[index].default !== "undefined",
139
+ prefix: modules[index].prefix
88
140
  })));
89
- if (selection.type === "rejected") throw new NestedLayoutsNotSupportedError(pageFile, selection.layouts);
90
141
  return {
91
142
  chain,
92
- layoutFile: selection.type === "selected" ? selection.layout : chain.at(-1),
93
- prefix: modules.reduce((composed, layoutModule) => composeRoutePath(composed, layoutModule.prefix ?? "/"), "/"),
143
+ layoutFile: level.hostId,
144
+ prefix: level.prefix,
94
145
  prefixesByDirectory: Object.fromEntries(chain.flatMap((layoutFile, index) => {
95
146
  const prefix = modules[index].prefix;
96
147
  return prefix === void 0 ? [] : [[toPosix(path.relative(webRoot, path.dirname(layoutFile))), prefix]];
@@ -113,15 +164,7 @@ async function composeLayoutLevel(level, loadLayout) {
113
164
  return {
114
165
  ...modules[hostIndex],
115
166
  middleware: modules.flatMap((layoutModule) => [...layoutModule.middleware ?? []]),
116
- loader: async (context) => {
117
- let hostData;
118
- for (let index = 0; index < modules.length; index++) {
119
- const value = await modules[index].loader?.(context);
120
- if (value instanceof Response || isLoaderShortCircuit(value)) return value;
121
- if (index === hostIndex) hostData = value;
122
- }
123
- return hostData;
124
- }
167
+ loader: foldLayoutLoaders(modules.map((layoutModule) => layoutModule.loader), hostIndex)
125
168
  };
126
169
  }
127
170
  /**
@@ -134,7 +177,8 @@ async function composeLayoutLevel(level, loadLayout) {
134
177
  * below `src/web`, using the same pure filesystem-routing helper as the build.
135
178
  */
136
179
  async function installPageRoutes(options) {
137
- const { router, vite, appSrcRoot, appFile, hydrationClientModuleUrl } = options;
180
+ const { router, vite, appSrcRoot, appFile, hydrationClientModuleUrl, httpServer } = options;
181
+ const httpServerOption = httpServer === void 0 ? {} : { httpServer };
138
182
  const stylesheetRoot = options.appRoot ?? path.dirname(appSrcRoot);
139
183
  const discovered = [...discoverPageFiles(appSrcRoot)].sort((left, right) => left.pageFile < right.pageFile ? -1 : left.pageFile > right.pageFile ? 1 : 0);
140
184
  const errorPageFiles = discovered.filter((page) => isErrorPageFile(page.pageFile));
@@ -152,9 +196,21 @@ async function installPageRoutes(options) {
152
196
  const installed = [];
153
197
  const fileByPath = /* @__PURE__ */ new Map();
154
198
  for (const { pageFile, webRoot } of pageFiles) {
155
- const pageModule = await vite.ssrLoadModule(pageFile);
199
+ let pageModule;
200
+ try {
201
+ pageModule = await vite.ssrLoadModule(pageFile);
202
+ } catch (loadError) {
203
+ await registerFailedPageRoute({
204
+ router,
205
+ pageFile,
206
+ appSrcRoot,
207
+ loadError,
208
+ fileByPath
209
+ });
210
+ continue;
211
+ }
156
212
  const sourceFile = canonicalSourceFileFor(pageFile, appSrcRoot);
157
- const { declaredPath: routePath, name } = resolvePageRouteIdentity(pageModule.route, pageFile, appSrcRoot);
213
+ const { path: routePath, name } = resolvePageRouteIdentity(pageModule.route, filesystemPageFileFor(pageFile, appSrcRoot), pageFile);
158
214
  const cache = resolvePageRouteCache(pageModule.route, pageFile);
159
215
  const loadLayout = (layoutFile) => vite.ssrLoadModule(layoutFile);
160
216
  const layoutLevel = await resolveLayoutLevel(pageFile, webRoot, loadLayout);
@@ -164,7 +220,15 @@ async function installPageRoutes(options) {
164
220
  layoutPrefixes: layoutLevel.prefixesByDirectory
165
221
  }) : composeRoutePath(layoutPrefix, routePath);
166
222
  const existingFile = fileByPath.get(effectivePath);
167
- if (existingFile) throw new Error(`installPageRoutes: composed route path "${effectivePath}" (layout prefix "${layoutPrefix}" + route.path "${routePath}") is declared by two pages (web/src/server/install-page-routes.ts) — "${existingFile}" and "${pageFile}". Every page's composed route path must be unique.`);
223
+ if (existingFile) throw new Error(duplicateRoutePathMessage({
224
+ effectivePath,
225
+ existingFile,
226
+ newFile: pageFile,
227
+ composition: {
228
+ layoutPrefix,
229
+ routePath
230
+ }
231
+ }));
168
232
  fileByPath.set(effectivePath, pageFile);
169
233
  const stylesheetUrls = devHandlerStylesheetUrls(stylesheetRoot, [
170
234
  appFile,
@@ -185,7 +249,8 @@ async function installPageRoutes(options) {
185
249
  hydrationClientModuleUrl,
186
250
  loadErrorPage,
187
251
  stylesheetUrls,
188
- cache
252
+ cache,
253
+ ...httpServerOption
189
254
  }), {
190
255
  name,
191
256
  isPage: true
@@ -215,7 +280,8 @@ async function installPageRoutes(options) {
215
280
  stylesheetUrls: devHandlerStylesheetUrls(stylesheetRoot, [appFile, notFoundPageFile]),
216
281
  matchPath: (requestPath) => requestPath,
217
282
  statusForRenderedOk: 404,
218
- skipPageLoader: true
283
+ skipPageLoader: true,
284
+ ...httpServerOption
219
285
  }) }), {
220
286
  name: NOT_FOUND_ROUTE_NAME,
221
287
  isPage: true
@@ -228,5 +294,5 @@ async function installPageRoutes(options) {
228
294
  }
229
295
 
230
296
  //#endregion
231
- export { installPageRoutes, resolvePageRouteIdentity };
297
+ export { filesystemPageFileFor, installPageRoutes };
232
298
  //# sourceMappingURL=install-page-routes.mjs.map