@warlock.js/web 5.0.2 → 5.2.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 (219) hide show
  1. package/CHANGELOG.md +250 -0
  2. package/README.md +126 -12
  3. package/esm/build/contribution.d.mts +11 -4
  4. package/esm/build/contribution.mjs +46 -22
  5. package/esm/build/contribution.mjs.map +1 -1
  6. package/esm/build/discover-pages.mjs +318 -44
  7. package/esm/build/discover-pages.mjs.map +1 -1
  8. package/esm/build/generate-client-registry.mjs +47 -13
  9. package/esm/build/generate-client-registry.mjs.map +1 -1
  10. package/esm/build/generate-pages-barrel.mjs +55 -13
  11. package/esm/build/generate-pages-barrel.mjs.map +1 -1
  12. package/esm/build/page-default-export.mjs +48 -0
  13. package/esm/build/page-default-export.mjs.map +1 -0
  14. package/esm/build/page-routes-manifest.mjs +21 -0
  15. package/esm/build/page-routes-manifest.mjs.map +1 -0
  16. package/esm/build/public-files.mjs +45 -0
  17. package/esm/build/public-files.mjs.map +1 -0
  18. package/esm/build/read-route-exports.mjs +1 -1
  19. package/esm/build/read-route-exports.mjs.map +1 -1
  20. package/esm/client/build-hydrated-tree.mjs +40 -9
  21. package/esm/client/build-hydrated-tree.mjs.map +1 -1
  22. package/esm/client/hydrate-page.mjs +1 -1
  23. package/esm/client/hydrate-page.mjs.map +1 -1
  24. package/esm/client/navigation/current-route.d.mts +1 -1
  25. package/esm/client/navigation/current-route.mjs +1 -1
  26. package/esm/client/navigation/current-route.mjs.map +1 -1
  27. package/esm/client/navigation/fetch-page-data.mjs +1 -1
  28. package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
  29. package/esm/client/navigation/navigation-root.mjs +44 -7
  30. package/esm/client/navigation/navigation-root.mjs.map +1 -1
  31. package/esm/client/navigation/prefetch.mjs +1 -1
  32. package/esm/client/navigation/prefetch.mjs.map +1 -1
  33. package/esm/client/navigation/refresh.d.mts +1 -1
  34. package/esm/client/navigation/refresh.mjs +1 -1
  35. package/esm/client/navigation/refresh.mjs.map +1 -1
  36. package/esm/client/navigation/scroll-to-fragment.mjs +26 -0
  37. package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -0
  38. package/esm/client/navigation/verbs.d.mts +1 -1
  39. package/esm/client/navigation/verbs.mjs +1 -1
  40. package/esm/client/navigation/verbs.mjs.map +1 -1
  41. package/esm/client/runtime/index.d.mts +2 -1
  42. package/esm/client/runtime/index.mjs +2 -1
  43. package/esm/client/runtime/manifest.d.mts +1 -1
  44. package/esm/client/runtime/manifest.mjs +3 -2
  45. package/esm/client/runtime/manifest.mjs.map +1 -1
  46. package/esm/client/runtime/matcher.d.mts +13 -1
  47. package/esm/client/runtime/matcher.mjs +13 -1
  48. package/esm/client/runtime/matcher.mjs.map +1 -1
  49. package/esm/client/runtime/types.d.mts +15 -2
  50. package/esm/components/default-app.mjs +1 -1
  51. package/esm/components/default-app.mjs.map +1 -1
  52. package/esm/components/document-context.d.mts +40 -2
  53. package/esm/components/document-context.mjs +1 -1
  54. package/esm/components/document-context.mjs.map +1 -1
  55. package/esm/components/head.d.mts +1 -1
  56. package/esm/components/head.mjs +1 -1
  57. package/esm/components/head.mjs.map +1 -1
  58. package/esm/components/link.d.mts +1 -1
  59. package/esm/components/link.mjs +1 -1
  60. package/esm/components/link.mjs.map +1 -1
  61. package/esm/components/scripts.d.mts +1 -1
  62. package/esm/components/scripts.mjs +4 -2
  63. package/esm/components/scripts.mjs.map +1 -1
  64. package/esm/context.d.mts +6 -102
  65. package/esm/hydration/index.mjs +1 -1
  66. package/esm/hydration/index.mjs.map +1 -1
  67. package/esm/hydration-payload.d.mts +1 -1
  68. package/esm/hydration-payload.mjs +37 -8
  69. package/esm/hydration-payload.mjs.map +1 -1
  70. package/esm/index.d.mts +4 -3
  71. package/esm/loaders.d.mts +7 -36
  72. package/esm/metadata.d.mts +15 -1
  73. package/esm/metadata.mjs +45 -0
  74. package/esm/metadata.mjs.map +1 -0
  75. package/esm/props.d.mts +16 -8
  76. package/esm/route.d.mts +2 -17
  77. package/esm/routing/compose-route-path.d.mts +1 -1
  78. package/esm/routing/compose-route-path.mjs +1 -1
  79. package/esm/routing/compose-route-path.mjs.map +1 -1
  80. package/esm/routing/data-request.mjs +1 -1
  81. package/esm/routing/data-request.mjs.map +1 -1
  82. package/esm/routing/filesystem-route.mjs +45 -0
  83. package/esm/routing/filesystem-route.mjs.map +1 -0
  84. package/esm/routing/layout-policy.mjs +1 -1
  85. package/esm/routing/layout-policy.mjs.map +1 -1
  86. package/esm/routing/navigator.mjs +1 -1
  87. package/esm/routing/navigator.mjs.map +1 -1
  88. package/esm/routing/query-string.d.mts +1 -1
  89. package/esm/routing/query-string.mjs +1 -1
  90. package/esm/routing/query-string.mjs.map +1 -1
  91. package/esm/routing/route-identity.mjs +1 -1
  92. package/esm/routing/route-identity.mjs.map +1 -1
  93. package/esm/routing/route-table.d.mts +1 -1
  94. package/esm/routing/route-table.mjs +1 -1
  95. package/esm/routing/route-table.mjs.map +1 -1
  96. package/esm/routing/router-events.d.mts +1 -1
  97. package/esm/routing/router-events.mjs +1 -1
  98. package/esm/routing/router-events.mjs.map +1 -1
  99. package/esm/routing/url-fragment.mjs +120 -0
  100. package/esm/routing/url-fragment.mjs.map +1 -0
  101. package/esm/runtime/register-modules.d.mts +19 -0
  102. package/esm/runtime/register-modules.mjs +22 -0
  103. package/esm/runtime/register-modules.mjs.map +1 -0
  104. package/esm/server/build-hydration-payload.mjs +2 -1
  105. package/esm/server/build-hydration-payload.mjs.map +1 -1
  106. package/esm/server/client-asset-url-prefix.d.mts +1 -1
  107. package/esm/server/client-asset-url-prefix.mjs +1 -1
  108. package/esm/server/client-asset-url-prefix.mjs.map +1 -1
  109. package/esm/server/create-page-module-loader.d.mts +1 -1
  110. package/esm/server/create-page-module-loader.mjs +2 -1
  111. package/esm/server/create-page-module-loader.mjs.map +1 -1
  112. package/esm/server/create-page-route-handler.d.mts +58 -6
  113. package/esm/server/create-page-route-handler.mjs +127 -40
  114. package/esm/server/create-page-route-handler.mjs.map +1 -1
  115. package/esm/server/dev-server.mjs +2 -13
  116. package/esm/server/dev-server.mjs.map +1 -1
  117. package/esm/server/error-page.d.mts +14 -0
  118. package/esm/server/error-page.mjs +39 -0
  119. package/esm/server/error-page.mjs.map +1 -0
  120. package/esm/server/execute-page-request.d.mts +7 -4
  121. package/esm/server/execute-page-request.mjs +79 -80
  122. package/esm/server/execute-page-request.mjs.map +1 -1
  123. package/esm/server/execute-page-request.types.d.mts +33 -92
  124. package/esm/server/hydration-client-url.d.mts +1 -1
  125. package/esm/server/hydration-client-url.mjs +2 -2
  126. package/esm/server/hydration-client-url.mjs.map +1 -1
  127. package/esm/server/index.d.mts +6 -5
  128. package/esm/server/index.mjs +4 -4
  129. package/esm/server/install-page-routes-from-manifest.d.mts +17 -7
  130. package/esm/server/install-page-routes-from-manifest.mjs +91 -8
  131. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  132. package/esm/server/install-page-routes.d.mts +32 -13
  133. package/esm/server/install-page-routes.mjs +112 -32
  134. package/esm/server/install-page-routes.mjs.map +1 -1
  135. package/esm/server/install-production-page-routes.mjs +3 -31
  136. package/esm/server/install-production-page-routes.mjs.map +1 -1
  137. package/esm/server/match-page-route.mjs +2 -2
  138. package/esm/server/match-page-route.mjs.map +1 -1
  139. package/esm/server/not-found-page.d.mts +126 -0
  140. package/esm/server/not-found-page.mjs +157 -0
  141. package/esm/server/not-found-page.mjs.map +1 -0
  142. package/esm/server/page-context.d.mts +3 -9
  143. package/esm/server/page-context.mjs +3 -16
  144. package/esm/server/page-context.mjs.map +1 -1
  145. package/esm/server/page-file-change.mjs +77 -0
  146. package/esm/server/page-file-change.mjs.map +1 -0
  147. package/esm/server/page-manifest.d.mts +25 -10
  148. package/esm/server/page-manifest.mjs +1 -1
  149. package/esm/server/page-manifest.mjs.map +1 -1
  150. package/esm/server/page-render-bundle.mjs +38 -0
  151. package/esm/server/page-render-bundle.mjs.map +1 -0
  152. package/esm/server/page-route-reload.mjs +54 -0
  153. package/esm/server/page-route-reload.mjs.map +1 -0
  154. package/esm/server/register-production-public-files.mjs +40 -0
  155. package/esm/server/register-production-public-files.mjs.map +1 -0
  156. package/esm/server/render-page.d.mts +11 -14
  157. package/esm/server/render-page.mjs +154 -28
  158. package/esm/server/render-page.mjs.map +1 -1
  159. package/esm/server/resolve-page-metadata.mjs +2 -2
  160. package/esm/server/resolve-page-metadata.mjs.map +1 -1
  161. package/esm/server/resolve-validation-data.mjs +1 -1
  162. package/esm/server/resolve-validation-data.mjs.map +1 -1
  163. package/esm/server/settle-page-response.d.mts +58 -1
  164. package/esm/server/settle-page-response.mjs +111 -46
  165. package/esm/server/settle-page-response.mjs.map +1 -1
  166. package/esm/server/stylesheet-urls.d.mts +41 -30
  167. package/esm/server/stylesheet-urls.mjs +137 -52
  168. package/esm/server/stylesheet-urls.mjs.map +1 -1
  169. package/esm/server/unregistered-pages.mjs +42 -0
  170. package/esm/server/unregistered-pages.mjs.map +1 -0
  171. package/esm/server/web-connector-factory.d.mts +2 -3
  172. package/esm/server/web-connector-factory.mjs +6 -4
  173. package/esm/server/web-connector-factory.mjs.map +1 -1
  174. package/esm/server/web-connector.d.mts +1 -1
  175. package/esm/server/web-connector.mjs +233 -18
  176. package/esm/server/web-connector.mjs.map +1 -1
  177. package/esm/shared.d.mts +18 -3
  178. package/esm/shared.mjs +3 -7
  179. package/esm/shared.mjs.map +1 -1
  180. package/esm/validation.d.mts +1 -1
  181. package/esm/vite/app-convention-aliases.mjs +1 -1
  182. package/esm/vite/app-convention-aliases.mjs.map +1 -1
  183. package/esm/vite/build-client.d.mts +1 -1
  184. package/esm/vite/build-client.mjs +2 -2
  185. package/esm/vite/build-client.mjs.map +1 -1
  186. package/esm/vite/gate-a-resolve.d.mts +1 -1
  187. package/esm/vite/gate-a-resolve.mjs +67 -13
  188. package/esm/vite/gate-a-resolve.mjs.map +1 -1
  189. package/esm/vite/gate-b-secrets.d.mts +1 -1
  190. package/esm/vite/gate-b-secrets.mjs +35 -17
  191. package/esm/vite/gate-b-secrets.mjs.map +1 -1
  192. package/esm/vite/gate-c-verify.d.mts +1 -1
  193. package/esm/vite/gate-c-verify.mjs +6 -6
  194. package/esm/vite/gate-c-verify.mjs.map +1 -1
  195. package/esm/vite/hydration-entries.d.mts +1 -1
  196. package/esm/vite/hydration-entries.mjs +10 -6
  197. package/esm/vite/hydration-entries.mjs.map +1 -1
  198. package/esm/vite/index.d.mts +9 -6
  199. package/esm/vite/index.mjs +124 -11
  200. package/esm/vite/index.mjs.map +1 -1
  201. package/esm/vite/page-registry-plugin.d.mts +22 -3
  202. package/esm/vite/page-registry-plugin.mjs +247 -2
  203. package/esm/vite/page-registry-plugin.mjs.map +1 -1
  204. package/esm/vite/projection.d.mts +2 -2
  205. package/esm/vite/projection.mjs +38 -15
  206. package/esm/vite/projection.mjs.map +1 -1
  207. package/llms-full.txt +241 -49
  208. package/llms.txt +6 -2
  209. package/package.json +3 -3
  210. package/skills/add-web-to-an-app/SKILL.md +3 -3
  211. package/skills/create-a-page/SKILL.md +130 -7
  212. package/skills/load-page-data/SKILL.md +92 -12
  213. package/skills/navigate-on-the-client/SKILL.md +3 -1
  214. package/skills/serve-styles/SKILL.md +13 -12
  215. package/skills/use-layouts/SKILL.md +20 -8
  216. package/skills/write-the-root/SKILL.md +5 -1
  217. package/esm/server/buffered-response.d.mts +0 -58
  218. package/esm/server/buffered-response.mjs +0 -115
  219. package/esm/server/buffered-response.mjs.map +0 -1
@@ -1,11 +1,16 @@
1
1
  import { publishRouteTable } from "../routing/route-table.mjs";
2
+ import { isLoaderShortCircuit } from "./settle-page-response.mjs";
2
3
  import { createPageModuleLoader } from "./create-page-module-loader.mjs";
3
4
  import { composeRoutePath } from "../routing/compose-route-path.mjs";
5
+ import { deriveFilesystemRouteName, deriveFilesystemRoutePath } from "../routing/filesystem-route.mjs";
4
6
  import { NestedLayoutsNotSupportedError, selectPageLayout } from "../routing/layout-policy.mjs";
5
7
  import { canonicalizeRouteExport, deriveFallbackRouteName } from "../routing/route-identity.mjs";
6
8
  import { createPageRouteHandler } from "./create-page-route-handler.mjs";
9
+ import { productionStylesheetUrls } from "./stylesheet-urls.mjs";
10
+ 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";
7
12
 
8
- //#region ../web/src/server/install-page-routes-from-manifest.ts
13
+ //#region web/src/server/install-page-routes-from-manifest.ts
9
14
  /**
10
15
  * Page-route registration for a built application.
11
16
  *
@@ -30,7 +35,27 @@ import { createPageRouteHandler } from "./create-page-route-handler.mjs";
30
35
  * in memory, so registration has nothing to await; the loader handed to each
31
36
  * handler is a lookup over the same table, not an evaluation step.
32
37
  */
38
+ /**
39
+ * `sourceFile`'s path relative to the web root — `src/web/**`, the only page
40
+ * root discovery enumerates (`discoverWebRoots`,
41
+ * `web/src/build/discover-pages.ts:210-213`). Manifest `sourceFile`s are
42
+ * app-root-relative (`"src/web/..."`, `page-manifest.ts`'s own doc comment),
43
+ * so dropping the first two segments — `<srcDir>`, then the literal `"web"` —
44
+ * recovers exactly what `deriveFilesystemRoutePath`/`deriveFilesystemRouteName`
45
+ * expect: the same value dev computes as `filesystemPageFileFor`
46
+ * (`install-page-routes.ts:101-103`).
47
+ */
48
+ function webRelativeSourceFile(sourceFile) {
49
+ return sourceFile.split("/").slice(2).join("/");
50
+ }
33
51
  function resolveRoute(routeExport, sourceFile) {
52
+ if (routeExport === void 0) {
53
+ const pageFile = webRelativeSourceFile(sourceFile);
54
+ return {
55
+ path: deriveFilesystemRoutePath({ pageFile }),
56
+ name: deriveFilesystemRouteName(pageFile)
57
+ };
58
+ }
34
59
  const canonical = canonicalizeRouteExport(routeExport);
35
60
  return {
36
61
  path: canonical.path,
@@ -40,6 +65,22 @@ function resolveRoute(routeExport, sourceFile) {
40
65
  })
41
66
  };
42
67
  }
68
+ /**
69
+ * Every layout's declared `prefix`, keyed by its directory relative to the
70
+ * web root — the same table dev builds as `LayoutLevel.prefixesByDirectory`
71
+ * (`install-page-routes.ts:214-222`) and the one
72
+ * {@link deriveFilesystemRoutePath} uses to let a directory's own layout
73
+ * rename the URL segment a bare directory name would otherwise contribute.
74
+ */
75
+ function layoutPrefixesOf(page) {
76
+ return Object.fromEntries(page.layouts.flatMap((layout) => {
77
+ const prefix = layout.module.prefix;
78
+ if (prefix === void 0) return [];
79
+ const relative = webRelativeSourceFile(layout.sourceFile);
80
+ const slashIndex = relative.lastIndexOf("/");
81
+ return [[slashIndex === -1 ? "" : relative.slice(0, slashIndex), prefix]];
82
+ }));
83
+ }
43
84
  function layoutLevelOf(page) {
44
85
  const selection = selectPageLayout(page.layouts.map((layout) => ({
45
86
  layout: layout.sourceFile,
@@ -66,9 +107,19 @@ function layoutLevelOf(page) {
66
107
  * already in memory and cannot change under a running process.
67
108
  */
68
109
  function composeLayoutLevel(page, host) {
110
+ const hostIndex = page.layouts.indexOf(host);
69
111
  return {
70
112
  ...host.module,
71
- middleware: page.layouts.flatMap((layout) => [...layout.module.middleware ?? []])
113
+ middleware: page.layouts.flatMap((layout) => [...layout.module.middleware ?? []]),
114
+ loader: async (context) => {
115
+ let hostData;
116
+ for (let index = 0; index < page.layouts.length; index++) {
117
+ const value = await page.layouts[index].module.loader?.(context);
118
+ if (value instanceof Response || isLoaderShortCircuit(value)) return value;
119
+ if (index === hostIndex) hostData = value;
120
+ }
121
+ return hostData;
122
+ }
72
123
  };
73
124
  }
74
125
  /**
@@ -86,23 +137,36 @@ function composeLayoutLevel(page, host) {
86
137
  * them silently loses at runtime.
87
138
  */
88
139
  function installPageRoutesFromManifest(options) {
89
- const { router, manifest, hydrationClientModuleUrl, stylesheetUrls, applyBufferedCookie, createHandler = createPageRouteHandler } = options;
140
+ const { router, manifest, hydrationClientModuleUrl, clientDir, createHandler = createPageRouteHandler } = options;
90
141
  if (manifest.pages.length === 0) return [];
91
142
  const app = manifest.app;
92
143
  if (app === void 0) throw new Error(`installPageRoutesFromManifest: this build's page manifest carries ${manifest.pages.length} page(s) but no application root. Every page renders inside the app component, so no page can be registered without it. Re-run the build so the generated pages barrel provides an \`app\` entry.`);
93
144
  const loadModule = createPageModuleLoader(manifest);
145
+ const loadErrorPage = manifest.errorPage === void 0 ? void 0 : async () => manifest.errorPage.module;
146
+ const notFoundPages = manifest.pages.filter((page) => isNotFoundPageFile(page.sourceFile));
147
+ const pages = manifest.pages.filter((page) => !isNotFoundPageFile(page.sourceFile));
148
+ if (notFoundPages.length > 1) throw new DuplicateNotFoundPageError(notFoundPages.map((page) => page.sourceFile));
149
+ const notFoundPage = notFoundPages[0];
150
+ if (notFoundPage !== void 0 && notFoundPage.module.route !== void 0) throw new NotFoundPageDeclaresRouteError(notFoundPage.sourceFile);
94
151
  const installed = [];
95
152
  const fileByPath = /* @__PURE__ */ new Map();
96
- for (const page of manifest.pages) {
153
+ for (const page of pages) {
97
154
  const { host: layout, prefix: layoutPrefix } = layoutLevelOf(page);
98
155
  const routeExport = page.module.route;
99
- if (routeExport === void 0) continue;
100
156
  const { path: routePath, name } = resolveRoute(routeExport, page.sourceFile);
101
- const effectivePath = composeRoutePath(layoutPrefix, routePath);
157
+ const effectivePath = routeExport === void 0 ? deriveFilesystemRoutePath({
158
+ pageFile: webRelativeSourceFile(page.sourceFile),
159
+ layoutPrefixes: layoutPrefixesOf(page)
160
+ }) : composeRoutePath(layoutPrefix, routePath);
102
161
  const existingFile = fileByPath.get(effectivePath);
103
162
  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.`);
104
163
  fileByPath.set(effectivePath, page.sourceFile);
105
164
  const composedLayout = page.layouts.length > 1 && layout !== void 0 ? composeLayoutLevel(page, layout) : void 0;
165
+ const stylesheetUrls = clientDir === void 0 ? [] : productionStylesheetUrls(clientDir, [
166
+ app.sourceFile,
167
+ ...page.layouts.map((pageLayout) => pageLayout.sourceFile),
168
+ page.sourceFile
169
+ ]);
106
170
  router.get(effectivePath, createHandler({
107
171
  path: effectivePath,
108
172
  name,
@@ -110,20 +174,39 @@ function installPageRoutesFromManifest(options) {
110
174
  pageFile: page.sourceFile,
111
175
  layoutFile: layout?.sourceFile,
112
176
  loadModule: composedLayout === void 0 ? loadModule : (moduleId) => moduleId === layout?.sourceFile ? Promise.resolve(composedLayout) : loadModule(moduleId),
177
+ loadRegistrationLayouts: () => Promise.resolve(page.layouts.map((layout) => layout.module)),
113
178
  hydrationClientModuleUrl,
114
- stylesheetUrls,
115
- applyBufferedCookie
179
+ loadErrorPage,
180
+ stylesheetUrls
116
181
  }), {
117
182
  name,
118
183
  isPage: true
119
184
  });
120
185
  installed.push({
186
+ declaredPath: routePath,
121
187
  path: effectivePath,
122
188
  name,
123
189
  file: page.sourceFile,
124
190
  layoutFile: layout?.sourceFile
125
191
  });
126
192
  }
193
+ router.get("*", createNotFoundRouteHandler({ renderPage: notFoundPage === void 0 ? void 0 : createHandler({
194
+ path: "*",
195
+ name: NOT_FOUND_ROUTE_NAME,
196
+ appFile: app.sourceFile,
197
+ pageFile: notFoundPage.sourceFile,
198
+ layoutFile: void 0,
199
+ loadModule,
200
+ hydrationClientModuleUrl,
201
+ loadErrorPage,
202
+ stylesheetUrls: clientDir === void 0 ? [] : productionStylesheetUrls(clientDir, [app.sourceFile, notFoundPage.sourceFile]),
203
+ matchPath: (requestPath) => requestPath,
204
+ statusForRenderedOk: 404,
205
+ skipPageLoader: true
206
+ }) }), {
207
+ name: NOT_FOUND_ROUTE_NAME,
208
+ isPage: true
209
+ });
127
210
  publishRouteTable(installed, "installPageRoutesFromManifest (production)");
128
211
  return installed;
129
212
  }
@@ -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 { NestedLayoutsNotSupportedError, selectPageLayout } from \"../routing/layout-policy\";\r\nimport { canonicalizeRouteExport, deriveFallbackRouteName } from \"../routing/route-identity\";\r\nimport { publishRouteTable } from \"../routing/route-table\";\r\nimport type { Response, Router } from \"@warlock.js/core\";\r\nimport type { BufferedCookie } from \"./buffered-response\";\r\nimport { createPageModuleLoader } from \"./create-page-module-loader\";\r\nimport {\r\n createPageRouteHandler,\r\n type PageRouteHandler,\r\n type PageRouteHandlerOptions,\r\n} from \"./create-page-route-handler\";\r\nimport type { PipelineMiddleware } from \"./execute-page-request\";\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 };\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};\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 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 /** Stylesheet URLs emitted into every page's `<head>`. */\r\n stylesheetUrls?: readonly string[];\r\n /** Same helper `dev-server.ts` exports — passed in, never imported. */\r\n applyBufferedCookie: (response: Response, cookie: BufferedCookie) => void;\r\n createHandler?: PageRouteHandlerFactory;\r\n};\r\n\r\nfunction resolveRoute(\r\n routeExport: PageRouteExport,\r\n sourceFile: string,\r\n): { path: string; name: string } {\r\n const canonical = canonicalizeRouteExport(routeExport);\r\n\r\n return {\r\n path: canonical.path,\r\n name: canonical.name ?? deriveFallbackRouteName({ routePath: canonical.path, sourceFile }),\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 return {\r\n ...host.module,\r\n middleware: page.layouts.flatMap((layout) => [\r\n ...((layout.module as LayoutModuleShape).middleware ?? []),\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 stylesheetUrls,\r\n applyBufferedCookie,\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\r\n const installed: InstalledManifestPageRoute[] = [];\r\n const fileByPath = new Map<string, string>();\r\n\r\n for (const page of manifest.pages) {\r\n const { host: layout, prefix: layoutPrefix } = layoutLevelOf(page);\r\n const routeExport = (page.module as PageModuleShape).route;\r\n\r\n // A page that declares no route has no public URL to be registered under —\r\n // the same page discovery skips in development.\r\n if (routeExport === undefined) continue;\r\n\r\n const { path: routePath, name } = resolveRoute(routeExport, page.sourceFile);\r\n const effectivePath = 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 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 hydrationClientModuleUrl,\r\n stylesheetUrls,\r\n applyBufferedCookie,\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 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 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6FA,SAAS,aACP,aACA,YACgC;CAChC,MAAM,YAAY,wBAAwB,WAAW;CAErD,OAAO;EACL,MAAM,UAAU;EAChB,MAAM,UAAU,QAAQ,wBAAwB;GAAE,WAAW,UAAU;GAAM;EAAW,CAAC;CAC3F;AACF;AA2CA,SAAS,cAAc,MAA0C;CAC/D,MAAM,YAAY,iBAChB,KAAK,QAAQ,KAAK,YAAY;EAC5B,QAAQ,OAAO;EACf,SAAS,OAAQ,OAAO,OAA6B,YAAY;CACnE,EAAE,CACJ;CAEA,IAAI,UAAU,SAAS,YACrB,MAAM,IAAI,+BAA+B,KAAK,YAAY,UAAU,OAAO;CAG7E,OAAO;EACL,MACE,UAAU,SAAS,aACf,KAAK,QAAQ,MAAM,WAAW,OAAO,eAAe,UAAU,MAAM,IACpE,KAAK,QAAQ,GAAG,EAAE;EACxB,QAAQ,KAAK,QAAQ,QAClB,UAAU,WACT,iBAAiB,UAAW,OAAO,OAA6B,UAAU,GAAG,GAC/E,GACF;CACF;AACF;;;;;;;;;;;;;;;AAgBA,SAAS,mBACP,MACA,MACyB;CACzB,OAAO;EACL,GAAG,KAAK;EACR,YAAY,KAAK,QAAQ,SAAS,WAAW,CAC3C,GAAK,OAAO,OAA6B,cAAc,CAAC,CAC1D,CAAC;CACH;AACF;;;;;;;;;;;;;;;AAgBA,SAAgB,8BACd,SAC8B;CAC9B,MAAM,EACJ,QACA,UACA,0BACA,gBACA,qBACA,gBAAgB,2BACd;CAEJ,IAAI,SAAS,MAAM,WAAW,GAAG,OAAO,CAAC;CAEzC,MAAM,MAAM,SAAS;CAErB,IAAI,QAAQ,QACV,MAAM,IAAI,MACR,qEAAqE,SAAS,MAAM,OAAO,kMAI7F;CAOF,MAAM,aAAa,uBAAuB,QAAQ;CAElD,MAAM,YAA0C,CAAC;CACjD,MAAM,6BAAa,IAAI,IAAoB;CAE3C,KAAK,MAAM,QAAQ,SAAS,OAAO;EACjC,MAAM,EAAE,MAAM,QAAQ,QAAQ,iBAAiB,cAAc,IAAI;EACjE,MAAM,cAAe,KAAK,OAA2B;EAIrD,IAAI,gBAAgB,QAAW;EAE/B,MAAM,EAAE,MAAM,WAAW,SAAS,aAAa,aAAa,KAAK,UAAU;EAC3E,MAAM,gBAAgB,iBAAiB,cAAc,SAAS;EAC9D,MAAM,eAAe,WAAW,IAAI,aAAa;EAEjD,IAAI,cACF,MAAM,IAAI,MACR,uDAAuD,cAAc,oBACxD,aAAa,kBAAkB,UAAU,iCAChD,aAAa,SAAS,KAAK,WAAW,oDAE9C;EAGF,WAAW,IAAI,eAAe,KAAK,UAAU;EAM7C,MAAM,iBACJ,KAAK,QAAQ,SAAS,KAAK,WAAW,SAClC,mBAAmB,MAAM,MAAM,IAC/B;EAEN,OAAO,IACL,eACA,cAAc;GACZ,MAAM;GACN;GACA,SAAS,IAAI;GACb,UAAU,KAAK;GACf,YAAY,QAAQ;GACpB,YACE,mBAAmB,SACf,cACC,aACC,aAAa,QAAQ,aACjB,QAAQ,QAAQ,cAAc,IAC9B,WAAW,QAAQ;GAC/B;GACA;GACA;EACF,CAAC,GAID;GAAE;GAAM,QAAQ;EAAK,CACvB;EAEA,UAAU,KAAK;GACb,MAAM;GACN;GACA,MAAM,KAAK;GACX,YAAY,QAAQ;EACtB,CAAC;CACH;CAQA,kBAAkB,WAAW,4CAA4C;CAEzE,OAAO;AACT"}
1
+ {"version":3,"file":"install-page-routes-from-manifest.mjs","names":[],"sources":["C:/Work/Warlock.js/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 { deriveFilesystemRouteName, deriveFilesystemRoutePath } from \"../routing/filesystem-route\";\r\nimport { NestedLayoutsNotSupportedError, selectPageLayout } from \"../routing/layout-policy\";\r\nimport { canonicalizeRouteExport, deriveFallbackRouteName } 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\";\nimport { isLoaderShortCircuit } from \"./settle-page-response\";\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 };\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\nfunction resolveRoute(\r\n routeExport: PageRouteExport | undefined,\r\n sourceFile: string,\r\n): { path: string; name: string } {\r\n if (routeExport === undefined) {\r\n const pageFile = webRelativeSourceFile(sourceFile);\r\n\r\n return {\r\n path: deriveFilesystemRoutePath({ pageFile }),\r\n name: deriveFilesystemRouteName(pageFile),\r\n };\r\n }\r\n\r\n const canonical = canonicalizeRouteExport(routeExport);\r\n\r\n return {\r\n path: canonical.path,\r\n name: canonical.name ?? deriveFallbackRouteName({ routePath: canonical.path, sourceFile }),\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;\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 = 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 // 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: () =>\r\n Promise.resolve(page.layouts.map((layout) => layout.module)),\r\n hydrationClientModuleUrl,\r\n loadErrorPage,\r\n stylesheetUrls,\r\n }),\r\n // `isPage` marks this route as SSR-served. Pages and API routes share one\r\n // router and one route-name namespace, so the router's duplicate-name\r\n // error reads this flag to say which claimant is the page.\r\n { name, isPage: true },\r\n );\r\n\r\n 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,\n statusForRenderedOk: 404,\n skipPageLoader: true,\n }),\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,10 +1,9 @@
1
1
  import { composeRoutePath } from "../routing/compose-route-path.mjs";
2
- import { BufferedCookie } from "./buffered-response.mjs";
3
- import { PipelineMiddleware } from "./execute-page-request.types.mjs";
4
- import { Response, Router } from "@warlock.js/core";
2
+ import { PipelineLoader, PipelineMiddleware } from "./execute-page-request.types.mjs";
3
+ import { Router } from "@warlock.js/core";
5
4
  import { ViteDevServer } from "vite";
6
5
 
7
- //#region ../web/src/server/install-page-routes.d.ts
6
+ //#region web/src/server/install-page-routes.d.ts
8
7
  type PageRouteExport = string | {
9
8
  path: string;
10
9
  name?: string;
@@ -13,12 +12,14 @@ type PageModuleShape = {
13
12
  route?: PageRouteExport;
14
13
  };
15
14
  type InstalledPageRoute = {
15
+ /** The canonical declared route path, before layout-prefix composition. */declaredPath: string;
16
16
  path: string;
17
17
  name: string;
18
18
  file: string;
19
19
  layoutFile: string | undefined;
20
20
  };
21
21
  type LayoutModuleShape = {
22
+ /** Universal registration hook; invoked on this real namespace, never a composed wrapper. */register?: () => unknown;
22
23
  prefix?: string;
23
24
  /**
24
25
  * The default export — the thing that puts an element in the document, and
@@ -28,21 +29,39 @@ type LayoutModuleShape = {
28
29
  */
29
30
  default?: unknown; /** The layout's guards, in the order it declared them. */
30
31
  middleware?: readonly PipelineMiddleware[];
32
+ loader?: PipelineLoader;
31
33
  };
32
34
  type InstallPageRoutesOptions = {
33
35
  router: Router;
34
36
  vite: ViteDevServer; /** v5/app/src — pages live under "<appSrcRoot>/app/*\/web/**" and "<appSrcRoot>/web/**". */
35
37
  appSrcRoot: string; /** v5/app/src/web/root.tsx — the single global app-root file. */
36
- appFile: string; /** Browser module loaded after the server-rendered application and payload. */
37
- hydrationClientModuleUrl?: string;
38
+ appFile: string;
38
39
  /**
39
- * Stylesheet URLs emitted into every page's `<head>`.
40
+ * The application root Vite's dev server serves from — `dev-server.ts`'s
41
+ * `paths.appRoot`, i.e. `<appRoot>/src === appSrcRoot` by default. Every
42
+ * handler's stylesheet URLs are expressed relative to THIS, because that is
43
+ * the root Vite's dev server actually resolves `/…` URLs against
44
+ * (`stylesheet-urls.ts`'s `devStylesheetUrls`) — not `appSrcRoot`, which is
45
+ * one directory level in.
40
46
  *
41
- * In dev these are Vite source URLs; see `devStylesheetUrls` for why they
42
- * carry `?direct`.
47
+ * OPTIONAL and defaulted to `path.dirname(appSrcRoot)`: the caller that
48
+ * wires dev boot (`web-connector.ts`) does not pass this field today, and
49
+ * that default is exactly the relationship it constructs `appSrcRoot` from
50
+ * (`appSrcRoot = path.join(appRoot, "src")`) — correct for every actual
51
+ * deployment, and overridable by a caller with a non-default layout.
52
+ */
53
+ appRoot?: string; /** Browser module loaded after the server-rendered application and payload. */
54
+ hydrationClientModuleUrl?: string;
55
+ /**
56
+ * UNUSED. Retained on this type only because `web-connector.ts` still builds
57
+ * an options object naming it (`devStylesheetUrls(paths.appRoot,
58
+ * paths.appFile)`, computed once for the whole application). Each handler
59
+ * now computes its OWN stylesheet chain — `[root, ...outer-to-inner matched
60
+ * layouts, page]`, via `devHandlerStylesheetUrls` — inside the registration
61
+ * loop below, because a single application-wide list cannot express "this
62
+ * page's own CSS" without also carrying every other page's.
43
63
  */
44
- stylesheetUrls?: readonly string[]; /** Same helper `dev-server.ts` exports — passed in, not imported, to avoid a dev-server.ts <-> this-file cycle. */
45
- applyBufferedCookie: (response: Response, cookie: BufferedCookie) => void;
64
+ stylesheetUrls?: readonly string[];
46
65
  };
47
66
  /**
48
67
  * Registers every discoverable page into `options.router`. Throws
@@ -50,8 +69,8 @@ type InstallPageRoutesOptions = {
50
69
  * `route.path` — a registration-time failure, not a runtime 404 one of them
51
70
  * silently loses.
52
71
  *
53
- * Pages with no `route` export are skipped: discovery cannot invent a public
54
- * URL or route name for an undeclared page.
72
+ * Pages with no `route` export derive their path and name from their location
73
+ * below `src/web`, using the same pure filesystem-routing helper as the build.
55
74
  */
56
75
  declare function installPageRoutes(options: InstallPageRoutesOptions): Promise<InstalledPageRoute[]>;
57
76
  //#endregion
@@ -1,12 +1,18 @@
1
1
  import { publishRouteTable } from "../routing/route-table.mjs";
2
+ import { isLoaderShortCircuit } from "./settle-page-response.mjs";
2
3
  import { composeRoutePath } from "../routing/compose-route-path.mjs";
4
+ import { deriveFilesystemRouteName, deriveFilesystemRoutePath } from "../routing/filesystem-route.mjs";
3
5
  import { NestedLayoutsNotSupportedError, selectPageLayout } from "../routing/layout-policy.mjs";
4
- import { canonicalizeRouteExport, deriveFallbackRouteName } from "../routing/route-identity.mjs";
6
+ import { canonicalizeRouteExport } from "../routing/route-identity.mjs";
5
7
  import { createPageRouteHandler } from "./create-page-route-handler.mjs";
6
- import { discoverPageFiles, layoutChainFor, toPosix } from "../build/discover-pages.mjs";
8
+ import { devHandlerStylesheetUrls } from "./stylesheet-urls.mjs";
9
+ import { DuplicateNotFoundPageError, NOT_FOUND_ROUTE_NAME, NOT_FOUND_ROUTE_PATH, NotFoundPageDeclaresRouteError, createNotFoundRouteHandler, isNotFoundPageFile } from "./not-found-page.mjs";
10
+ 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";
7
13
  import path from "node:path";
8
14
 
9
- //#region ../web/src/server/install-page-routes.ts
15
+ //#region web/src/server/install-page-routes.ts
10
16
  /**
11
17
  * Registers every page {@link discoverPageFiles} finds under `<appSrcRoot>`
12
18
  * into Warlock's router (`router.get`, `core/src/router/router.ts:359-361`)
@@ -41,25 +47,38 @@ import path from "node:path";
41
47
  * page module must be the one Vite serves, warm cache and all.
42
48
  */
43
49
  /**
44
- * The page's app-root-relative POSIX source path, e.g.
45
- * ".../v5/app/src/app/main/web/home.page.tsx" with appSrcRoot
46
- * ".../v5/app/src" -> "src/app/main/web/home.page.tsx" — the canonical form
47
- * `deriveFallbackRouteName` (`../routing/route-identity`) requires. The first
48
- * segment's actual name is arbitrary to that function (it only inspects the
49
- * segment AFTER it), so `appSrcRoot`'s own basename is used rather than
50
- * discovering the true app root.
50
+ * Ownership key for the framework's fallback 404 route. A NUL-prefixed value
51
+ * cannot be a real filesystem path, so it cannot collide with an app page's
52
+ * canonical source-file key.
53
+ */
54
+ const FRAMEWORK_DEFAULT_NOT_FOUND_SOURCE_FILE = "\0warlock:framework-default-404";
55
+ /**
56
+ * The page's application-source-relative POSIX source path used as the router's
57
+ * stable ownership key. `appSrcRoot`'s own basename preserves the existing
58
+ * `src/web/...` source-file convention.
51
59
  */
52
60
  function canonicalSourceFileFor(pageFile, appSrcRoot) {
53
61
  return `${path.basename(appSrcRoot)}/${toPosix(path.relative(appSrcRoot, pageFile))}`;
54
62
  }
55
- function resolveRoute(routeExport, sourceFile) {
56
- const canonical = canonicalizeRouteExport(routeExport);
63
+ function filesystemPageFileFor(pageFile, appSrcRoot) {
64
+ return toPosix(path.relative(path.join(appSrcRoot, "web"), pageFile));
65
+ }
66
+ /**
67
+ * Resolve the stable identity used to distinguish a route-export edit from an
68
+ * ordinary component-body edit. The declared path is retained before layout
69
+ * composition so `/settings` under `/admin` compares with the next declared
70
+ * `/settings`, not with the effective `/admin/settings` route.
71
+ */
72
+ function resolvePageRouteIdentity(routeExport, pageFile, appSrcRoot) {
73
+ const filesystemPageFile = filesystemPageFileFor(pageFile, appSrcRoot);
74
+ if (routeExport === void 0) return {
75
+ declaredPath: deriveFilesystemRoutePath({ pageFile: filesystemPageFile }),
76
+ name: deriveFilesystemRouteName(filesystemPageFile)
77
+ };
78
+ const route = canonicalizeRouteExport(routeExport);
57
79
  return {
58
- path: canonical.path,
59
- name: canonical.name ?? deriveFallbackRouteName({
60
- routePath: canonical.path,
61
- sourceFile
62
- })
80
+ declaredPath: route.path,
81
+ name: route.name ?? deriveFilesystemRouteName(filesystemPageFile)
63
82
  };
64
83
  }
65
84
  async function resolveLayoutLevel(pageFile, webRoot, loadLayout) {
@@ -73,7 +92,11 @@ async function resolveLayoutLevel(pageFile, webRoot, loadLayout) {
73
92
  return {
74
93
  chain,
75
94
  layoutFile: selection.type === "selected" ? selection.layout : chain.at(-1),
76
- prefix: modules.reduce((composed, layoutModule) => composeRoutePath(composed, layoutModule.prefix ?? "/"), "/")
95
+ prefix: modules.reduce((composed, layoutModule) => composeRoutePath(composed, layoutModule.prefix ?? "/"), "/"),
96
+ prefixesByDirectory: Object.fromEntries(chain.flatMap((layoutFile, index) => {
97
+ const prefix = modules[index].prefix;
98
+ return prefix === void 0 ? [] : [[toPosix(path.relative(webRoot, path.dirname(layoutFile))), prefix]];
99
+ }))
77
100
  };
78
101
  }
79
102
  /**
@@ -88,9 +111,19 @@ async function resolveLayoutLevel(pageFile, webRoot, loadLayout) {
88
111
  */
89
112
  async function composeLayoutLevel(level, loadLayout) {
90
113
  const modules = await Promise.all(level.chain.map(loadLayout));
114
+ const hostIndex = level.chain.indexOf(level.layoutFile);
91
115
  return {
92
- ...modules[level.chain.indexOf(level.layoutFile)],
93
- middleware: modules.flatMap((layoutModule) => [...layoutModule.middleware ?? []])
116
+ ...modules[hostIndex],
117
+ middleware: modules.flatMap((layoutModule) => [...layoutModule.middleware ?? []]),
118
+ loader: async (context) => {
119
+ let hostData;
120
+ for (let index = 0; index < modules.length; index++) {
121
+ const value = await modules[index].loader?.(context);
122
+ if (value instanceof Response || isLoaderShortCircuit(value)) return value;
123
+ if (index === hostIndex) hostData = value;
124
+ }
125
+ return hostData;
126
+ }
94
127
  };
95
128
  }
96
129
  /**
@@ -99,27 +132,47 @@ async function composeLayoutLevel(level, loadLayout) {
99
132
  * `route.path` — a registration-time failure, not a runtime 404 one of them
100
133
  * silently loses.
101
134
  *
102
- * Pages with no `route` export are skipped: discovery cannot invent a public
103
- * URL or route name for an undeclared page.
135
+ * Pages with no `route` export derive their path and name from their location
136
+ * below `src/web`, using the same pure filesystem-routing helper as the build.
104
137
  */
105
138
  async function installPageRoutes(options) {
106
- const { router, vite, appSrcRoot, appFile, hydrationClientModuleUrl, stylesheetUrls, applyBufferedCookie } = options;
107
- const pageFiles = [...discoverPageFiles(appSrcRoot)].sort((left, right) => left.pageFile < right.pageFile ? -1 : left.pageFile > right.pageFile ? 1 : 0);
139
+ const { router, vite, appSrcRoot, appFile, hydrationClientModuleUrl } = options;
140
+ const stylesheetRoot = options.appRoot ?? path.dirname(appSrcRoot);
141
+ const discovered = [...discoverPageFiles(appSrcRoot)].sort((left, right) => left.pageFile < right.pageFile ? -1 : left.pageFile > right.pageFile ? 1 : 0);
142
+ const errorPageFiles = discovered.filter((page) => isErrorPageFile(page.pageFile));
143
+ const notFoundPageFiles = discovered.filter((page) => isNotFoundPageFile(page.pageFile));
144
+ const pageFiles = discovered.filter((page) => !isNotFoundPageFile(page.pageFile) && !isErrorPageFile(page.pageFile));
145
+ if (errorPageFiles.length > 1) throw new Error(`Two error pages were found: ${errorPageFiles.map((page) => page.pageFile).join(", ")}.`);
146
+ const errorPageFile = errorPageFiles[0]?.pageFile;
147
+ if (errorPageFile !== void 0) {
148
+ const declarations = readRouteExports(errorPageFile);
149
+ if (!declarations.ok) throw new NonLiteralRouteExportError(declarations.rejection);
150
+ if (declarations.route !== void 0) throw new ErrorPageDeclaresRouteError(errorPageFile);
151
+ }
152
+ const loadErrorPage = errorPageFile === void 0 ? void 0 : () => vite.ssrLoadModule(errorPageFile);
153
+ if (notFoundPageFiles.length > 1) throw new DuplicateNotFoundPageError(notFoundPageFiles.map((page) => page.pageFile));
108
154
  const installed = [];
109
155
  const fileByPath = /* @__PURE__ */ new Map();
110
156
  for (const { pageFile, webRoot } of pageFiles) {
111
157
  const pageModule = await vite.ssrLoadModule(pageFile);
112
- if (pageModule.route === void 0) continue;
113
158
  const sourceFile = canonicalSourceFileFor(pageFile, appSrcRoot);
114
- const { path: routePath, name } = resolveRoute(pageModule.route, sourceFile);
159
+ const { declaredPath: routePath, name } = resolvePageRouteIdentity(pageModule.route, pageFile, appSrcRoot);
115
160
  const loadLayout = (layoutFile) => vite.ssrLoadModule(layoutFile);
116
161
  const layoutLevel = await resolveLayoutLevel(pageFile, webRoot, loadLayout);
117
162
  const { layoutFile, prefix: layoutPrefix } = layoutLevel;
118
- const effectivePath = composeRoutePath(layoutPrefix, routePath);
163
+ const effectivePath = pageModule.route === void 0 ? deriveFilesystemRoutePath({
164
+ pageFile: filesystemPageFileFor(pageFile, appSrcRoot),
165
+ layoutPrefixes: layoutLevel.prefixesByDirectory
166
+ }) : composeRoutePath(layoutPrefix, routePath);
119
167
  const existingFile = fileByPath.get(effectivePath);
120
168
  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.`);
121
169
  fileByPath.set(effectivePath, pageFile);
122
- router.get(effectivePath, createPageRouteHandler({
170
+ const stylesheetUrls = devHandlerStylesheetUrls(stylesheetRoot, [
171
+ appFile,
172
+ ...layoutLevel.chain,
173
+ pageFile
174
+ ]);
175
+ await router.withSourceFile(sourceFile, () => router.get(effectivePath, createPageRouteHandler({
123
176
  path: effectivePath,
124
177
  name,
125
178
  appFile,
@@ -129,24 +182,51 @@ async function installPageRoutes(options) {
129
182
  ...layoutLevel,
130
183
  layoutFile
131
184
  }, loadLayout) : vite.ssrLoadModule(moduleId) : (moduleId) => vite.ssrLoadModule(moduleId),
185
+ loadRegistrationLayouts: () => Promise.all(layoutLevel.chain.map(loadLayout)),
132
186
  hydrationClientModuleUrl,
133
- stylesheetUrls,
134
- applyBufferedCookie
187
+ loadErrorPage,
188
+ stylesheetUrls
135
189
  }), {
136
190
  name,
137
191
  isPage: true
138
- });
192
+ }));
139
193
  installed.push({
194
+ declaredPath: routePath,
140
195
  path: effectivePath,
141
196
  name,
142
197
  file: pageFile,
143
198
  layoutFile
144
199
  });
145
200
  }
201
+ if (pageFiles.length > 0 || notFoundPageFiles.length > 0) {
202
+ const notFoundPageFile = notFoundPageFiles[0]?.pageFile;
203
+ if (notFoundPageFile !== void 0) {
204
+ if ((await vite.ssrLoadModule(notFoundPageFile)).route !== void 0) throw new NotFoundPageDeclaresRouteError(notFoundPageFile);
205
+ }
206
+ const registerNotFoundRoute = () => router.get("*", createNotFoundRouteHandler({ renderPage: notFoundPageFile === void 0 ? void 0 : createPageRouteHandler({
207
+ path: "*",
208
+ name: NOT_FOUND_ROUTE_NAME,
209
+ appFile,
210
+ pageFile: notFoundPageFile,
211
+ layoutFile: void 0,
212
+ loadModule: (moduleId) => vite.ssrLoadModule(moduleId),
213
+ hydrationClientModuleUrl,
214
+ loadErrorPage,
215
+ stylesheetUrls: devHandlerStylesheetUrls(stylesheetRoot, [appFile, notFoundPageFile]),
216
+ matchPath: (requestPath) => requestPath,
217
+ statusForRenderedOk: 404,
218
+ skipPageLoader: true
219
+ }) }), {
220
+ name: NOT_FOUND_ROUTE_NAME,
221
+ isPage: true
222
+ });
223
+ if (notFoundPageFile === void 0) await router.withSourceFile(FRAMEWORK_DEFAULT_NOT_FOUND_SOURCE_FILE, registerNotFoundRoute);
224
+ else await router.withSourceFile(canonicalSourceFileFor(notFoundPageFile, appSrcRoot), registerNotFoundRoute);
225
+ }
146
226
  publishRouteTable(installed, "installPageRoutes (dev)");
147
227
  return installed;
148
228
  }
149
229
 
150
230
  //#endregion
151
- export { installPageRoutes };
231
+ export { installPageRoutes, resolvePageRouteIdentity };
152
232
  //# sourceMappingURL=install-page-routes.mjs.map