@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
@@ -0,0 +1,77 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ //#region web/src/server/page-file-change.ts
5
+ const PAGE_FILE_SUFFIX = ".page.tsx";
6
+ function isFile(file) {
7
+ try {
8
+ return fs.statSync(file).isFile();
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+ function normalizePath(appRoot, file) {
14
+ return path.resolve(appRoot, file.replace(/[\\/]+/g, path.sep));
15
+ }
16
+ function pathKey(file) {
17
+ const normalized = file.replace(/\\/g, "/");
18
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
19
+ }
20
+ function isPageFilePath(file, appSrcRoot) {
21
+ if (!file.endsWith(".page.tsx")) return false;
22
+ const relative = path.relative(appSrcRoot, file);
23
+ return relative !== "" && !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative) && relative.split(path.sep)[0] === "web";
24
+ }
25
+ /**
26
+ * Layouts contribute to every descendant page's derived route identity, but
27
+ * never own a route themselves. Keep this predicate aligned with
28
+ * `page-registry-plugin.ts` so a layout lifecycle event re-derives the whole
29
+ * page table rather than being discarded as an ordinary Vite update.
30
+ */
31
+ function isPageLayoutFilePath(file, appSrcRoot) {
32
+ const relative = path.relative(appSrcRoot, file);
33
+ const base = path.basename(file);
34
+ return relative !== "" && !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative) && relative.split(path.sep)[0] === "web" && (base === "layout.ts" || base === "layout.tsx" || /\.layout\.tsx?$/.test(base));
35
+ }
36
+ /** Error boundaries participate in reload tracking but never in router ownership. */
37
+ function isErrorPageFilePath(file) {
38
+ return path.basename(file) === "error.page.tsx";
39
+ }
40
+ function classifyPageFileChanges(changedFiles, options) {
41
+ const pageRoot = path.resolve(options.appRoot, options.appSrcRoot);
42
+ const installed = new Set(options.installedPageFiles.map((file) => pathKey(normalizePath(options.appRoot, file))));
43
+ const classified = {
44
+ added: [],
45
+ removed: [],
46
+ inspectionNeeded: []
47
+ };
48
+ const seen = /* @__PURE__ */ new Set();
49
+ const fileExists = options.fileExists ?? isFile;
50
+ for (const changedFile of changedFiles) {
51
+ const file = normalizePath(options.appRoot, changedFile);
52
+ const key = pathKey(file);
53
+ if (seen.has(key)) continue;
54
+ seen.add(key);
55
+ if (isPageLayoutFilePath(file, pageRoot)) {
56
+ (fileExists(file) ? classified.added : classified.removed).push(file);
57
+ continue;
58
+ }
59
+ if (!isPageFilePath(file, pageRoot)) continue;
60
+ const isInstalled = installed.has(key);
61
+ const isErrorPage = isErrorPageFilePath(file);
62
+ if (!fileExists(file)) {
63
+ if (isInstalled || isErrorPage) classified.removed.push(file);
64
+ continue;
65
+ }
66
+ if (isInstalled || isErrorPage) classified.inspectionNeeded.push(file);
67
+ else classified.added.push(file);
68
+ }
69
+ return classified;
70
+ }
71
+ function hasPageFileChanges(changes) {
72
+ return changes.added.length > 0 || changes.removed.length > 0 || changes.inspectionNeeded.length > 0;
73
+ }
74
+
75
+ //#endregion
76
+ export { classifyPageFileChanges, hasPageFileChanges, isErrorPageFilePath };
77
+ //# sourceMappingURL=page-file-change.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-file-change.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/page-file-change.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nexport const PAGE_FILE_SUFFIX = \".page.tsx\";\n\nexport type PageFileChanges = {\n added: string[];\n removed: string[];\n inspectionNeeded: string[];\n};\n\nexport type PageFileChangeOptions = {\n appRoot: string;\n appSrcRoot: string;\n installedPageFiles: readonly string[];\n fileExists?: (file: string) => boolean;\n};\n\nfunction isFile(file: string): boolean {\n try {\n return fs.statSync(file).isFile();\n } catch {\n return false;\n }\n}\n\nfunction normalizePath(appRoot: string, file: string): string {\n return path.resolve(appRoot, file.replace(/[\\\\/]+/g, path.sep));\n}\n\nfunction pathKey(file: string): string {\n const normalized = file.replace(/\\\\/g, \"/\");\n\n return process.platform === \"win32\" ? normalized.toLowerCase() : normalized;\n}\n\nexport function isPageFilePath(file: string, appSrcRoot: string): boolean {\n if (!file.endsWith(PAGE_FILE_SUFFIX)) {\n return false;\n }\n\n const relative = path.relative(appSrcRoot, file);\n\n return (\n relative !== \"\" &&\n !relative.startsWith(`..${path.sep}`) &&\n relative !== \"..\" &&\n !path.isAbsolute(relative) &&\n relative.split(path.sep)[0] === \"web\"\n );\n}\n\n/**\n * Layouts contribute to every descendant page's derived route identity, but\n * never own a route themselves. Keep this predicate aligned with\n * `page-registry-plugin.ts` so a layout lifecycle event re-derives the whole\n * page table rather than being discarded as an ordinary Vite update.\n */\nexport function isPageLayoutFilePath(file: string, appSrcRoot: string): boolean {\n const relative = path.relative(appSrcRoot, file);\n const base = path.basename(file);\n\n return (\n relative !== \"\" &&\n !relative.startsWith(`..${path.sep}`) &&\n relative !== \"..\" &&\n !path.isAbsolute(relative) &&\n relative.split(path.sep)[0] === \"web\" &&\n (base === \"layout.ts\" || base === \"layout.tsx\" || /\\.layout\\.tsx?$/.test(base))\n );\n}\n\n/** Error boundaries participate in reload tracking but never in router ownership. */\nexport function isErrorPageFilePath(file: string): boolean {\n return path.basename(file) === \"error.page.tsx\";\n}\n\nexport function classifyPageFileChanges(\n changedFiles: readonly string[],\n options: PageFileChangeOptions,\n): PageFileChanges {\n const pageRoot = path.resolve(options.appRoot, options.appSrcRoot);\n const installed = new Set(\n options.installedPageFiles.map((file) => pathKey(normalizePath(options.appRoot, file))),\n );\n const classified: PageFileChanges = { added: [], removed: [], inspectionNeeded: [] };\n const seen = new Set<string>();\n const fileExists = options.fileExists ?? isFile;\n\n for (const changedFile of changedFiles) {\n const file = normalizePath(options.appRoot, changedFile);\n const key = pathKey(file);\n\n if (seen.has(key)) {\n continue;\n }\n\n seen.add(key);\n\n // A layout has no individual router owner. Its addition, removal, or\n // in-place edit can change every descendant page's prefix/layout chain, so\n // force the existing full page-table derivation transaction. `added` is\n // deliberately the transaction's \"replacement required\" bucket here;\n // it does not claim the layout itself owns a newly-added route.\n if (isPageLayoutFilePath(file, pageRoot)) {\n (fileExists(file) ? classified.added : classified.removed).push(file);\n continue;\n }\n\n if (!isPageFilePath(file, pageRoot)) {\n continue;\n }\n\n const isInstalled = installed.has(key);\n const isErrorPage = isErrorPageFilePath(file);\n if (!fileExists(file)) {\n if (isInstalled || isErrorPage) {\n classified.removed.push(file);\n }\n continue;\n }\n\n if (isInstalled || isErrorPage) {\n classified.inspectionNeeded.push(file);\n } else {\n classified.added.push(file);\n }\n }\n\n return classified;\n}\n\nexport function hasPageFileChanges(changes: PageFileChanges): boolean {\n return (\n changes.added.length > 0 ||\n changes.removed.length > 0 ||\n changes.inspectionNeeded.length > 0\n );\n}\n"],"mappings":";;;;AAGA,MAAa,mBAAmB;AAehC,SAAS,OAAO,MAAuB;CACrC,IAAI;EACF,OAAO,GAAG,SAAS,IAAI,EAAE,OAAO;CAClC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,cAAc,SAAiB,MAAsB;CAC5D,OAAO,KAAK,QAAQ,SAAS,KAAK,QAAQ,WAAW,KAAK,GAAG,CAAC;AAChE;AAEA,SAAS,QAAQ,MAAsB;CACrC,MAAM,aAAa,KAAK,QAAQ,OAAO,GAAG;CAE1C,OAAO,QAAQ,aAAa,UAAU,WAAW,YAAY,IAAI;AACnE;AAEA,SAAgB,eAAe,MAAc,YAA6B;CACxE,IAAI,CAAC,KAAK,oBAAyB,GACjC,OAAO;CAGT,MAAM,WAAW,KAAK,SAAS,YAAY,IAAI;CAE/C,OACE,aAAa,MACb,CAAC,SAAS,WAAW,KAAK,KAAK,KAAK,KACpC,aAAa,QACb,CAAC,KAAK,WAAW,QAAQ,KACzB,SAAS,MAAM,KAAK,GAAG,EAAE,OAAO;AAEpC;;;;;;;AAQA,SAAgB,qBAAqB,MAAc,YAA6B;CAC9E,MAAM,WAAW,KAAK,SAAS,YAAY,IAAI;CAC/C,MAAM,OAAO,KAAK,SAAS,IAAI;CAE/B,OACE,aAAa,MACb,CAAC,SAAS,WAAW,KAAK,KAAK,KAAK,KACpC,aAAa,QACb,CAAC,KAAK,WAAW,QAAQ,KACzB,SAAS,MAAM,KAAK,GAAG,EAAE,OAAO,UAC/B,SAAS,eAAe,SAAS,gBAAgB,kBAAkB,KAAK,IAAI;AAEjF;;AAGA,SAAgB,oBAAoB,MAAuB;CACzD,OAAO,KAAK,SAAS,IAAI,MAAM;AACjC;AAEA,SAAgB,wBACd,cACA,SACiB;CACjB,MAAM,WAAW,KAAK,QAAQ,QAAQ,SAAS,QAAQ,UAAU;CACjE,MAAM,YAAY,IAAI,IACpB,QAAQ,mBAAmB,KAAK,SAAS,QAAQ,cAAc,QAAQ,SAAS,IAAI,CAAC,CAAC,CACxF;CACA,MAAM,aAA8B;EAAE,OAAO,CAAC;EAAG,SAAS,CAAC;EAAG,kBAAkB,CAAC;CAAE;CACnF,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,aAAa,QAAQ,cAAc;CAEzC,KAAK,MAAM,eAAe,cAAc;EACtC,MAAM,OAAO,cAAc,QAAQ,SAAS,WAAW;EACvD,MAAM,MAAM,QAAQ,IAAI;EAExB,IAAI,KAAK,IAAI,GAAG,GACd;EAGF,KAAK,IAAI,GAAG;EAOZ,IAAI,qBAAqB,MAAM,QAAQ,GAAG;GACxC,CAAC,WAAW,IAAI,IAAI,WAAW,QAAQ,WAAW,SAAS,KAAK,IAAI;GACpE;EACF;EAEA,IAAI,CAAC,eAAe,MAAM,QAAQ,GAChC;EAGF,MAAM,cAAc,UAAU,IAAI,GAAG;EACrC,MAAM,cAAc,oBAAoB,IAAI;EAC5C,IAAI,CAAC,WAAW,IAAI,GAAG;GACrB,IAAI,eAAe,aACjB,WAAW,QAAQ,KAAK,IAAI;GAE9B;EACF;EAEA,IAAI,eAAe,aACjB,WAAW,iBAAiB,KAAK,IAAI;OAErC,WAAW,MAAM,KAAK,IAAI;CAE9B;CAEA,OAAO;AACT;AAEA,SAAgB,mBAAmB,SAAmC;CACpE,OACE,QAAQ,MAAM,SAAS,KACvB,QAAQ,QAAQ,SAAS,KACzB,QAAQ,iBAAiB,SAAS;AAEtC"}
@@ -1,4 +1,4 @@
1
- //#region ../web/src/server/page-manifest.d.ts
1
+ //#region web/src/server/page-manifest.d.ts
2
2
  /**
3
3
  * The build→runtime handoff table for production pages.
4
4
  *
@@ -14,18 +14,20 @@
14
14
  * `@warlock.js/web`.
15
15
  *
16
16
  * Entries carry MODULES + FILESYSTEM FACTS ONLY — never route paths. A page's
17
- * route is read off its module (`route` export) at boot and composed with its
18
- * layout `prefix`, exactly as `install-page-routes.ts:123-129,213-221` does in
19
- * dev, so explicit-wins semantics live in one place for both modes.
17
+ * route is read off its module (`route` export) at boot and, when present,
18
+ * composed with its layout `prefix`; when absent, the path is instead derived
19
+ * from the page's own `sourceFile` and its layouts' prefixes, exactly as
20
+ * `install-page-routes.ts:377-382` does in dev and `discover-pages.ts:925-935`
21
+ * does at build — so explicit-wins semantics live in one place for all three.
20
22
  */
21
23
  type PageManifestLayoutEntry = {
22
- /** The imported layout module namespace object (default export = component, optional `prefix`). */module: Record<string, unknown>; /** App-root-relative POSIX source path, e.g. "src/app/main/web/layout.tsx". */
24
+ /** The imported layout module namespace object (default export = component, optional `prefix`). */module: Record<string, unknown>; /** App-root-relative POSIX source path, e.g. "src/web/layout.tsx". */
23
25
  sourceFile: string;
24
26
  };
25
27
  type PageManifestPageEntry = {
26
28
  /** The imported page module namespace object (default export = component, optional `route`). */module: Record<string, unknown>;
27
29
  /**
28
- * App-root-relative POSIX source path, e.g. "src/app/main/web/home.page.tsx"
30
+ * App-root-relative POSIX source path, e.g. "src/web/home.page.tsx"
29
31
  * — feeds FS-derived route/name fallback. The anchor is the APP ROOT, not the
30
32
  * repo root; the two differ the moment the app does not sit at the repo root.
31
33
  * Consumers compare these ids VERBATIM — the barrel that wrote them is the
@@ -35,6 +37,11 @@ type PageManifestPageEntry = {
35
37
  sourceFile: string; /** Layout chain, outermost first. */
36
38
  layouts: readonly PageManifestLayoutEntry[];
37
39
  };
40
+ /** The optional application error boundary, kept outside the routable page table. */
41
+ type PageManifestErrorPageEntry = {
42
+ module: Record<string, unknown>;
43
+ sourceFile: string;
44
+ };
38
45
  type PageManifest = {
39
46
  /**
40
47
  * Where the client bundle was written, as an APP-ROOT-RELATIVE POSIX path
@@ -50,11 +57,18 @@ type PageManifest = {
50
57
  * `WarlockConfig not loaded` during connector boot, taking the whole server
51
58
  * down before it could listen.
52
59
  *
53
- * OPTIONAL for the same reason `app` is: a zero-page build skips the client
54
- * bundle entirely, so there is no directory to name. Present whenever
55
- * `pages` is non-empty.
60
+ * OPTIONAL only when the build has neither pages nor public files. A
61
+ * zero-page build skips the hydration bundle, but still names this directory
62
+ * when it copied app-owned `public/` files below it. Present whenever
63
+ * `pages` or `publicFiles` is non-empty.
56
64
  */
57
65
  clientDir?: string;
66
+ /**
67
+ * App-public file paths copied below `<clientDir>/public`, relative and
68
+ * POSIX. Production registers these exact URLs; it never mounts the client
69
+ * root and therefore never exposes `.vite/manifest.json`.
70
+ */
71
+ publicFiles?: readonly string[];
58
72
  /**
59
73
  * The application root component every page renders inside.
60
74
  *
@@ -69,7 +83,8 @@ type PageManifest = {
69
83
  app?: {
70
84
  module: Record<string, unknown>;
71
85
  sourceFile: string;
72
- };
86
+ }; /** An application-owned error boundary. It has no route name or path. */
87
+ errorPage?: PageManifestErrorPageEntry;
73
88
  /**
74
89
  * The page table. EMPTY IS A REAL STATE, not a missing one: an empty array
75
90
  * means "this bundle was built with web, and it has no pages", which is what
@@ -1,4 +1,4 @@
1
- //#region ../web/src/server/page-manifest.ts
1
+ //#region web/src/server/page-manifest.ts
2
2
  let providedManifest;
3
3
  /**
4
4
  * Called exactly once by the generated production/pages.ts barrel.
@@ -1 +1 @@
1
- {"version":3,"file":"page-manifest.mjs","names":[],"sources":["../../../../../../../web/src/server/page-manifest.ts"],"sourcesContent":["/**\n * The build→runtime handoff table for production pages.\n *\n * The generated `<productionDir>/pages.ts` barrel — written by\n * `web/src/build/generate-pages-barrel.ts` — statically imports every page,\n * layout and the app root, then calls {@link providePageManifest} exactly\n * once. The connector reads it back at boot with {@link consumePageManifest}.\n *\n * DEPENDENCY-FREE ON PURPOSE: this module is re-exported from web's root\n * barrel (`web/src/index.ts`) so the generated barrel — which lives in the\n * consuming app's production dir — can reach it by package specifier. Adding\n * an import here would add that import to every app that loads\n * `@warlock.js/web`.\n *\n * Entries carry MODULES + FILESYSTEM FACTS ONLY — never route paths. A page's\n * route is read off its module (`route` export) at boot and composed with its\n * layout `prefix`, exactly as `install-page-routes.ts:123-129,213-221` does in\n * dev, so explicit-wins semantics live in one place for both modes.\n */\n\nexport type PageManifestLayoutEntry = {\n /** The imported layout module namespace object (default export = component, optional `prefix`). */\n module: Record<string, unknown>;\n /** App-root-relative POSIX source path, e.g. \"src/app/main/web/layout.tsx\". */\n sourceFile: string;\n};\n\nexport type PageManifestPageEntry = {\n /** The imported page module namespace object (default export = component, optional `route`). */\n module: Record<string, unknown>;\n /**\n * App-root-relative POSIX source path, e.g. \"src/app/main/web/home.page.tsx\"\n * — feeds FS-derived route/name fallback. The anchor is the APP ROOT, not the\n * repo root; the two differ the moment the app does not sit at the repo root.\n * Consumers compare these ids VERBATIM — the barrel that wrote them is the\n * authority on their form, and re-resolving or normalising one on the way in\n * turns a lookup miss into a silent mis-match.\n */\n sourceFile: string;\n /** Layout chain, outermost first. */\n layouts: readonly PageManifestLayoutEntry[];\n};\n\nexport type PageManifest = {\n /**\n * Where the client bundle was written, as an APP-ROOT-RELATIVE POSIX path\n * (`\"dist/client\"`). Resolved against the process cwd at boot, the same\n * anchor `sourceFile` uses.\n *\n * BAKED AT BUILD TIME because the runtime has no other way to learn it. The\n * directory is `<build.outdir>/client`, and `build.outdir` is configurable —\n * but it lives in `warlock.config.ts`, a BUILD-TIME file that is never\n * shipped and cannot be loaded by the plain `node dist/app.js` process\n * `warlock start` spawns. Reading it at runtime through\n * `resolveBuildConfig()` is what used to throw\n * `WarlockConfig not loaded` during connector boot, taking the whole server\n * down before it could listen.\n *\n * OPTIONAL for the same reason `app` is: a zero-page build skips the client\n * bundle entirely, so there is no directory to name. Present whenever\n * `pages` is non-empty.\n */\n clientDir?: string;\n /**\n * The application root component every page renders inside.\n *\n * OPTIONAL because it is only meaningful when pages exist: a build that\n * discovered zero pages provides `{ pages: [] }` and has no root to wrap\n * anything in. The build enforces the other direction — pages without an\n * app root fail generation — so `app` absent means `pages` is empty. Callers\n * must still branch on `pages.length` before reading `app`: the invariant is\n * the generator's, and a consumer that assumes it without checking turns a\n * malformed manifest into a crash deep in rendering instead of a named error.\n */\n app?: { module: Record<string, unknown>; sourceFile: string };\n /**\n * The page table. EMPTY IS A REAL STATE, not a missing one: an empty array\n * means \"this bundle was built with web, and it has no pages\", which is what\n * separates it from {@link consumePageManifest} returning `undefined`.\n */\n pages: readonly PageManifestPageEntry[];\n};\n\nlet providedManifest: PageManifest | undefined;\n\n/**\n * Called exactly once by the generated production/pages.ts barrel.\n *\n * A second call throws: two barrels in one process means either a stale\n * generated file survived into the bundle or the build ran twice, and the\n * second table silently replacing the first is precisely the drift this\n * contract exists to make impossible.\n */\nexport function providePageManifest(manifest: PageManifest): void {\n if (providedManifest !== undefined) {\n throw new Error(\n \"providePageManifest: a page manifest was already provided \" +\n \"(web/src/server/page-manifest.ts). The generated production/pages.ts barrel \" +\n \"must run exactly once per process.\",\n );\n }\n\n providedManifest = manifest;\n}\n\n/**\n * Connector-side read (WebConnector.boot, prod mode).\n *\n * `undefined` = no barrel ran → the app was not built with web. A manifest\n * whose `pages` is empty is the OTHER answer — built with web, no pages — and\n * the two are deliberately distinguishable here rather than collapsed into one\n * falsy result. The registry itself never throws on absence — absence is a\n * fact, not an error; the connector branches on MODE (prod: boot error; dev:\n * normal) so `undefined` never means two things at one call site.\n */\nexport function consumePageManifest(): PageManifest | undefined {\n return providedManifest;\n}\n"],"mappings":";AAmFA,IAAI;;;;;;;;;AAUJ,SAAgB,oBAAoB,UAA8B;CAChE,IAAI,qBAAqB,QACvB,MAAM,IAAI,MACR,0KAGF;CAGF,mBAAmB;AACrB;;;;;;;;;;;AAYA,SAAgB,sBAAgD;CAC9D,OAAO;AACT"}
1
+ {"version":3,"file":"page-manifest.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/page-manifest.ts"],"sourcesContent":["/**\n * The build→runtime handoff table for production pages.\n *\n * The generated `<productionDir>/pages.ts` barrel — written by\n * `web/src/build/generate-pages-barrel.ts` — statically imports every page,\n * layout and the app root, then calls {@link providePageManifest} exactly\n * once. The connector reads it back at boot with {@link consumePageManifest}.\n *\n * DEPENDENCY-FREE ON PURPOSE: this module is re-exported from web's root\n * barrel (`web/src/index.ts`) so the generated barrel — which lives in the\n * consuming app's production dir — can reach it by package specifier. Adding\n * an import here would add that import to every app that loads\n * `@warlock.js/web`.\n *\n * Entries carry MODULES + FILESYSTEM FACTS ONLY — never route paths. A page's\n * route is read off its module (`route` export) at boot and, when present,\n * composed with its layout `prefix`; when absent, the path is instead derived\n * from the page's own `sourceFile` and its layouts' prefixes, exactly as\n * `install-page-routes.ts:377-382` does in dev and `discover-pages.ts:925-935`\n * does at build — so explicit-wins semantics live in one place for all three.\n */\n\nexport type PageManifestLayoutEntry = {\n /** The imported layout module namespace object (default export = component, optional `prefix`). */\n module: Record<string, unknown>;\n /** App-root-relative POSIX source path, e.g. \"src/web/layout.tsx\". */\n sourceFile: string;\n};\n\nexport type PageManifestPageEntry = {\n /** The imported page module namespace object (default export = component, optional `route`). */\n module: Record<string, unknown>;\n /**\n * App-root-relative POSIX source path, e.g. \"src/web/home.page.tsx\"\n * — feeds FS-derived route/name fallback. The anchor is the APP ROOT, not the\n * repo root; the two differ the moment the app does not sit at the repo root.\n * Consumers compare these ids VERBATIM — the barrel that wrote them is the\n * authority on their form, and re-resolving or normalising one on the way in\n * turns a lookup miss into a silent mis-match.\n */\n sourceFile: string;\n /** Layout chain, outermost first. */\n layouts: readonly PageManifestLayoutEntry[];\n};\n\n/** The optional application error boundary, kept outside the routable page table. */\nexport type PageManifestErrorPageEntry = {\n module: Record<string, unknown>;\n sourceFile: string;\n};\n\nexport type PageManifest = {\n /**\n * Where the client bundle was written, as an APP-ROOT-RELATIVE POSIX path\n * (`\"dist/client\"`). Resolved against the process cwd at boot, the same\n * anchor `sourceFile` uses.\n *\n * BAKED AT BUILD TIME because the runtime has no other way to learn it. The\n * directory is `<build.outdir>/client`, and `build.outdir` is configurable —\n * but it lives in `warlock.config.ts`, a BUILD-TIME file that is never\n * shipped and cannot be loaded by the plain `node dist/app.js` process\n * `warlock start` spawns. Reading it at runtime through\n * `resolveBuildConfig()` is what used to throw\n * `WarlockConfig not loaded` during connector boot, taking the whole server\n * down before it could listen.\n *\n * OPTIONAL only when the build has neither pages nor public files. A\n * zero-page build skips the hydration bundle, but still names this directory\n * when it copied app-owned `public/` files below it. Present whenever\n * `pages` or `publicFiles` is non-empty.\n */\n clientDir?: string;\n /**\n * App-public file paths copied below `<clientDir>/public`, relative and\n * POSIX. Production registers these exact URLs; it never mounts the client\n * root and therefore never exposes `.vite/manifest.json`.\n */\n publicFiles?: readonly string[];\n /**\n * The application root component every page renders inside.\n *\n * OPTIONAL because it is only meaningful when pages exist: a build that\n * discovered zero pages provides `{ pages: [] }` and has no root to wrap\n * anything in. The build enforces the other direction — pages without an\n * app root fail generation — so `app` absent means `pages` is empty. Callers\n * must still branch on `pages.length` before reading `app`: the invariant is\n * the generator's, and a consumer that assumes it without checking turns a\n * malformed manifest into a crash deep in rendering instead of a named error.\n */\n app?: { module: Record<string, unknown>; sourceFile: string };\n /** An application-owned error boundary. It has no route name or path. */\n errorPage?: PageManifestErrorPageEntry;\n /**\n * The page table. EMPTY IS A REAL STATE, not a missing one: an empty array\n * means \"this bundle was built with web, and it has no pages\", which is what\n * separates it from {@link consumePageManifest} returning `undefined`.\n */\n pages: readonly PageManifestPageEntry[];\n};\n\nlet providedManifest: PageManifest | undefined;\n\n/**\n * Called exactly once by the generated production/pages.ts barrel.\n *\n * A second call throws: two barrels in one process means either a stale\n * generated file survived into the bundle or the build ran twice, and the\n * second table silently replacing the first is precisely the drift this\n * contract exists to make impossible.\n */\nexport function providePageManifest(manifest: PageManifest): void {\n if (providedManifest !== undefined) {\n throw new Error(\n \"providePageManifest: a page manifest was already provided \" +\n \"(web/src/server/page-manifest.ts). The generated production/pages.ts barrel \" +\n \"must run exactly once per process.\",\n );\n }\n\n providedManifest = manifest;\n}\n\n/**\n * Connector-side read (WebConnector.boot, prod mode).\n *\n * `undefined` = no barrel ran → the app was not built with web. A manifest\n * whose `pages` is empty is the OTHER answer — built with web, no pages — and\n * the two are deliberately distinguishable here rather than collapsed into one\n * falsy result. The registry itself never throws on absence — absence is a\n * fact, not an error; the connector branches on MODE (prod: boot error; dev:\n * normal) so `undefined` never means two things at one call site.\n */\nexport function consumePageManifest(): PageManifest | undefined {\n return providedManifest;\n}\n"],"mappings":";AAoGA,IAAI;;;;;;;;;AAUJ,SAAgB,oBAAoB,UAA8B;CAChE,IAAI,qBAAqB,QACvB,MAAM,IAAI,MACR,0KAGF;CAGF,mBAAmB;AACrB;;;;;;;;;;;AAYA,SAAgB,sBAAgD;CAC9D,OAAO;AACT"}
@@ -0,0 +1,38 @@
1
+ //#region web/src/server/page-render-bundle.ts
2
+ /**
3
+ * The internal, non-serializable marker `renderPageFailure` (render-page.ts)
4
+ * sets on its bundle AND on the document payload it hands to
5
+ * `DocumentContext` — never on anything `finishRender` produces.
6
+ *
7
+ * `renderPageFailure` runs when a module-load or registration throw happens
8
+ * BEFORE any page/layout/app triple exists — there is no trustworthy server
9
+ * composition for the browser to hydrate against, unlike a normal app-owned
10
+ * `error.page.tsx` reached through `finishRender`, which renders inside a
11
+ * real triple and stays hydratable. Marking both the bundle (read by
12
+ * `create-page-route-handler.ts`'s catch path to skip the hydration module)
13
+ * and the payload (read by `<Scripts/>` to skip `__WARLOCK_DATA__`) lets both
14
+ * call sites agree from the SAME flag instead of two independent guesses.
15
+ *
16
+ * A `Symbol`, non-enumerable, and never part of any exported type: nothing
17
+ * that serializes or spreads these objects (`JSON.stringify`, `{ ...x }`) can
18
+ * observe or carry it forward — only `isNonHydrating` reads it, and only
19
+ * `markNonHydrating` writes it.
20
+ */
21
+ const NON_HYDRATING = Symbol("warlock.web/non-hydrating");
22
+ /** Set only by `renderPageFailure` — see the module doc above. */
23
+ function markNonHydrating(target) {
24
+ Object.defineProperty(target, NON_HYDRATING, {
25
+ value: true,
26
+ enumerable: false,
27
+ configurable: true
28
+ });
29
+ return target;
30
+ }
31
+ /** Read by `create-page-route-handler.ts` and `<Scripts/>` — see the module doc above. */
32
+ function isNonHydrating(target) {
33
+ return target !== void 0 && target[NON_HYDRATING] === true;
34
+ }
35
+
36
+ //#endregion
37
+ export { isNonHydrating, markNonHydrating };
38
+ //# sourceMappingURL=page-render-bundle.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-render-bundle.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/page-render-bundle.ts"],"sourcesContent":["/**\n * The internal, non-serializable marker `renderPageFailure` (render-page.ts)\n * sets on its bundle AND on the document payload it hands to\n * `DocumentContext` — never on anything `finishRender` produces.\n *\n * `renderPageFailure` runs when a module-load or registration throw happens\n * BEFORE any page/layout/app triple exists — there is no trustworthy server\n * composition for the browser to hydrate against, unlike a normal app-owned\n * `error.page.tsx` reached through `finishRender`, which renders inside a\n * real triple and stays hydratable. Marking both the bundle (read by\n * `create-page-route-handler.ts`'s catch path to skip the hydration module)\n * and the payload (read by `<Scripts/>` to skip `__WARLOCK_DATA__`) lets both\n * call sites agree from the SAME flag instead of two independent guesses.\n *\n * A `Symbol`, non-enumerable, and never part of any exported type: nothing\n * that serializes or spreads these objects (`JSON.stringify`, `{ ...x }`) can\n * observe or carry it forward — only `isNonHydrating` reads it, and only\n * `markNonHydrating` writes it.\n */\nconst NON_HYDRATING = Symbol(\"warlock.web/non-hydrating\");\n\ntype MarkedNonHydrating = {\n readonly [NON_HYDRATING]?: true;\n};\n\n/** Set only by `renderPageFailure` — see the module doc above. */\nexport function markNonHydrating<T extends object>(target: T): T {\n Object.defineProperty(target, NON_HYDRATING, {\n value: true,\n enumerable: false,\n configurable: true,\n });\n\n return target;\n}\n\n/** Read by `create-page-route-handler.ts` and `<Scripts/>` — see the module doc above. */\nexport function isNonHydrating(target: object | undefined): boolean {\n return target !== undefined && (target as MarkedNonHydrating)[NON_HYDRATING] === true;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAmBA,MAAM,gBAAgB,OAAO,2BAA2B;;AAOxD,SAAgB,iBAAmC,QAAc;CAC/D,OAAO,eAAe,QAAQ,eAAe;EAC3C,OAAO;EACP,YAAY;EACZ,cAAc;CAChB,CAAC;CAED,OAAO;AACT;;AAGA,SAAgB,eAAe,QAAqC;CAClE,OAAO,WAAW,UAAc,OAA8B,mBAAmB;AACnF"}
@@ -0,0 +1,54 @@
1
+ import { isNotFoundPageFile } from "./not-found-page.mjs";
2
+ import { resolvePageRouteIdentity } from "./install-page-routes.mjs";
3
+ import { isErrorPageFilePath } from "./page-file-change.mjs";
4
+ import path from "node:path";
5
+
6
+ //#region web/src/server/page-route-reload.ts
7
+ function pathKey(file) {
8
+ const normalized = path.resolve(file).replace(/\\/g, "/");
9
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
10
+ }
11
+ /**
12
+ * Every real page file currently represented in the route table. This includes
13
+ * a custom `404.page.tsx`, which is intentionally absent from the public page
14
+ * table because it has no linkable URL of its own.
15
+ */
16
+ function registeredPageFiles(routes, appSrcRoot) {
17
+ const sourceRoot = path.dirname(appSrcRoot);
18
+ return [...new Set(routes.filter((route) => route.isPage && route.sourceFile && !route.sourceFile.startsWith("\0")).map((route) => path.resolve(sourceRoot, route.sourceFile)))];
19
+ }
20
+ /** Exact route-source owners replaced by a live page-table transaction. */
21
+ function pageRouteSourceFiles(routes) {
22
+ return [...new Set(routes.filter((route) => route.isPage && route.sourceFile).map((route) => route.sourceFile))];
23
+ }
24
+ /**
25
+ * Inspect only in-place page edits. Vite's SSR graph is invalidated explicitly
26
+ * before evaluation because core's file watcher and Vite's watcher have no
27
+ * ordering contract. Component-only edits compare equal and stay in Fast
28
+ * Refresh; route/name changes request an atomic table replacement.
29
+ */
30
+ async function pageRoutesNeedReplacement(changes, options) {
31
+ let replace = changes.added.length > 0 || changes.removed.length > 0;
32
+ const installedByFile = new Map(options.installedPages.map((page) => [pathKey(page.file), page]));
33
+ for (const file of changes.inspectionNeeded) {
34
+ if (isErrorPageFilePath(file)) return true;
35
+ options.vite.environments.ssr.moduleGraph.onFileChange(file);
36
+ const pageModule = await options.vite.ssrLoadModule(file);
37
+ if (isNotFoundPageFile(file)) {
38
+ if (pageModule.route !== void 0) replace = true;
39
+ continue;
40
+ }
41
+ const installed = installedByFile.get(pathKey(file));
42
+ if (installed === void 0) {
43
+ replace = true;
44
+ continue;
45
+ }
46
+ const next = resolvePageRouteIdentity(pageModule.route, file, options.appSrcRoot);
47
+ if (next.declaredPath !== installed.declaredPath || next.name !== installed.name) replace = true;
48
+ }
49
+ return replace;
50
+ }
51
+
52
+ //#endregion
53
+ export { pageRouteSourceFiles, pageRoutesNeedReplacement, registeredPageFiles };
54
+ //# sourceMappingURL=page-route-reload.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-route-reload.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/page-route-reload.ts"],"sourcesContent":["import path from \"node:path\";\nimport type { Router } from \"@warlock.js/core\";\nimport type { ViteDevServer } from \"vite\";\nimport type { InstalledPageRoute, PageModuleShape } from \"./install-page-routes\";\nimport { resolvePageRouteIdentity } from \"./install-page-routes\";\nimport { isNotFoundPageFile } from \"./not-found-page\";\nimport type { PageFileChanges } from \"./page-file-change\";\nimport { isErrorPageFilePath } from \"./page-file-change\";\n\nfunction pathKey(file: string): string {\n const normalized = path.resolve(file).replace(/\\\\/g, \"/\");\n\n return process.platform === \"win32\" ? normalized.toLowerCase() : normalized;\n}\n\n/**\n * Every real page file currently represented in the route table. This includes\n * a custom `404.page.tsx`, which is intentionally absent from the public page\n * table because it has no linkable URL of its own.\n */\nexport function registeredPageFiles(\n routes: ReturnType<Router[\"list\"]>,\n appSrcRoot: string,\n): string[] {\n const sourceRoot = path.dirname(appSrcRoot);\n\n return [\n ...new Set(\n routes\n .filter((route) => route.isPage && route.sourceFile && !route.sourceFile.startsWith(\"\\0\"))\n .map((route) => path.resolve(sourceRoot, route.sourceFile)),\n ),\n ];\n}\n\n/** Exact route-source owners replaced by a live page-table transaction. */\nexport function pageRouteSourceFiles(routes: ReturnType<Router[\"list\"]>): string[] {\n return [\n ...new Set(\n routes\n .filter((route) => route.isPage && route.sourceFile)\n .map((route) => route.sourceFile),\n ),\n ];\n}\n\n/**\n * Inspect only in-place page edits. Vite's SSR graph is invalidated explicitly\n * before evaluation because core's file watcher and Vite's watcher have no\n * ordering contract. Component-only edits compare equal and stay in Fast\n * Refresh; route/name changes request an atomic table replacement.\n */\nexport async function pageRoutesNeedReplacement(\n changes: PageFileChanges,\n options: {\n vite: ViteDevServer;\n appSrcRoot: string;\n installedPages: readonly InstalledPageRoute[];\n },\n): Promise<boolean> {\n let replace = changes.added.length > 0 || changes.removed.length > 0;\n\n const installedByFile = new Map(\n options.installedPages.map((page) => [pathKey(page.file), page] as const),\n );\n\n for (const file of changes.inspectionNeeded) {\n // An error boundary has no installed router route to compare against. Its\n // namespace is projected into every client composition, so any edit must\n // replace the generated table; do not load it here just to inspect it.\n if (isErrorPageFilePath(file)) return true;\n\n options.vite.environments.ssr.moduleGraph.onFileChange(file);\n const pageModule = (await options.vite.ssrLoadModule(file)) as PageModuleShape;\n\n // A valid 404 page has no route identity. Its component body remains Vite\n // HMR territory; adding an illegal route export must still enter the\n // transaction so the installer can reject it without touching live routes.\n if (isNotFoundPageFile(file)) {\n if (pageModule.route !== undefined) replace = true;\n continue;\n }\n\n const installed = installedByFile.get(pathKey(file));\n if (installed === undefined) {\n replace = true;\n continue;\n }\n\n const next = resolvePageRouteIdentity(pageModule.route, file, options.appSrcRoot);\n if (next.declaredPath !== installed.declaredPath || next.name !== installed.name) {\n replace = true;\n }\n }\n\n return replace;\n}\n"],"mappings":";;;;;;AASA,SAAS,QAAQ,MAAsB;CACrC,MAAM,aAAa,KAAK,QAAQ,IAAI,EAAE,QAAQ,OAAO,GAAG;CAExD,OAAO,QAAQ,aAAa,UAAU,WAAW,YAAY,IAAI;AACnE;;;;;;AAOA,SAAgB,oBACd,QACA,YACU;CACV,MAAM,aAAa,KAAK,QAAQ,UAAU;CAE1C,OAAO,CACL,GAAG,IAAI,IACL,OACG,QAAQ,UAAU,MAAM,UAAU,MAAM,cAAc,CAAC,MAAM,WAAW,WAAW,IAAI,CAAC,EACxF,KAAK,UAAU,KAAK,QAAQ,YAAY,MAAM,UAAU,CAAC,CAC9D,CACF;AACF;;AAGA,SAAgB,qBAAqB,QAA8C;CACjF,OAAO,CACL,GAAG,IAAI,IACL,OACG,QAAQ,UAAU,MAAM,UAAU,MAAM,UAAU,EAClD,KAAK,UAAU,MAAM,UAAU,CACpC,CACF;AACF;;;;;;;AAQA,eAAsB,0BACpB,SACA,SAKkB;CAClB,IAAI,UAAU,QAAQ,MAAM,SAAS,KAAK,QAAQ,QAAQ,SAAS;CAEnE,MAAM,kBAAkB,IAAI,IAC1B,QAAQ,eAAe,KAAK,SAAS,CAAC,QAAQ,KAAK,IAAI,GAAG,IAAI,CAAU,CAC1E;CAEA,KAAK,MAAM,QAAQ,QAAQ,kBAAkB;EAI3C,IAAI,oBAAoB,IAAI,GAAG,OAAO;EAEtC,QAAQ,KAAK,aAAa,IAAI,YAAY,aAAa,IAAI;EAC3D,MAAM,aAAc,MAAM,QAAQ,KAAK,cAAc,IAAI;EAKzD,IAAI,mBAAmB,IAAI,GAAG;GAC5B,IAAI,WAAW,UAAU,QAAW,UAAU;GAC9C;EACF;EAEA,MAAM,YAAY,gBAAgB,IAAI,QAAQ,IAAI,CAAC;EACnD,IAAI,cAAc,QAAW;GAC3B,UAAU;GACV;EACF;EAEA,MAAM,OAAO,yBAAyB,WAAW,OAAO,MAAM,QAAQ,UAAU;EAChF,IAAI,KAAK,iBAAiB,UAAU,gBAAgB,KAAK,SAAS,UAAU,MAC1E,UAAU;CAEd;CAEA,OAAO;AACT"}
@@ -0,0 +1,40 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ //#region web/src/server/register-production-public-files.ts
5
+ var InvalidProductionPublicFileError = class extends Error {
6
+ constructor(publicFile) {
7
+ super(`Cannot register production public file ${JSON.stringify(publicFile)}: the build manifest must contain a non-empty relative POSIX path with no traversal segments.`);
8
+ this.name = "InvalidProductionPublicFileError";
9
+ }
10
+ };
11
+ var MissingProductionPublicFileError = class extends Error {
12
+ constructor(publicFile, absoluteFile) {
13
+ super(`Cannot register production public file ${JSON.stringify(publicFile)}: the successful build manifest recorded it, but ${JSON.stringify(absoluteFile)} is missing or not a file. Run \`warlock build\` again to replace the incomplete artifact.`);
14
+ this.name = "MissingProductionPublicFileError";
15
+ }
16
+ };
17
+ function assertRelativePublicFile(publicFile) {
18
+ const segments = publicFile.split("/");
19
+ if (publicFile.length === 0 || publicFile.startsWith("/") || publicFile.includes("\\") || segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) throw new InvalidProductionPublicFileError(publicFile);
20
+ return segments;
21
+ }
22
+ /** Register the exact app-public files recorded by the successful build. */
23
+ function registerProductionPublicFiles(router, clientDir, publicFiles) {
24
+ const publicRoot = path.join(clientDir, "public");
25
+ for (const publicFile of publicFiles) {
26
+ const absoluteFile = path.join(publicRoot, ...assertRelativePublicFile(publicFile));
27
+ let stat;
28
+ try {
29
+ stat = fs.statSync(absoluteFile);
30
+ } catch {
31
+ throw new MissingProductionPublicFileError(publicFile, absoluteFile);
32
+ }
33
+ if (!stat.isFile()) throw new MissingProductionPublicFileError(publicFile, absoluteFile);
34
+ router.file(`/${publicFile}`, absoluteFile);
35
+ }
36
+ }
37
+
38
+ //#endregion
39
+ export { registerProductionPublicFiles };
40
+ //# sourceMappingURL=register-production-public-files.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register-production-public-files.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/register-production-public-files.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { Router } from \"@warlock.js/core\";\n\nexport class InvalidProductionPublicFileError extends Error {\n public constructor(publicFile: string) {\n super(\n `Cannot register production public file ${JSON.stringify(publicFile)}: ` +\n \"the build manifest must contain a non-empty relative POSIX path with no \" +\n \"traversal segments.\",\n );\n this.name = \"InvalidProductionPublicFileError\";\n }\n}\n\nexport class MissingProductionPublicFileError extends Error {\n public constructor(publicFile: string, absoluteFile: string) {\n super(\n `Cannot register production public file ${JSON.stringify(publicFile)}: ` +\n \"the successful build manifest recorded it, but \" +\n `${JSON.stringify(absoluteFile)} is missing or not a file. ` +\n \"Run `warlock build` again to replace the incomplete artifact.\",\n );\n this.name = \"MissingProductionPublicFileError\";\n }\n}\n\nfunction assertRelativePublicFile(publicFile: string): string[] {\n const segments = publicFile.split(\"/\");\n\n if (\n publicFile.length === 0 ||\n publicFile.startsWith(\"/\") ||\n publicFile.includes(\"\\\\\") ||\n segments.some((segment) => segment.length === 0 || segment === \".\" || segment === \"..\")\n ) {\n throw new InvalidProductionPublicFileError(publicFile);\n }\n\n return segments;\n}\n\n/** Register the exact app-public files recorded by the successful build. */\nexport function registerProductionPublicFiles(\n router: Router,\n clientDir: string,\n publicFiles: readonly string[],\n): void {\n const publicRoot = path.join(clientDir, \"public\");\n\n for (const publicFile of publicFiles) {\n const absoluteFile = path.join(publicRoot, ...assertRelativePublicFile(publicFile));\n\n let stat: fs.Stats;\n\n try {\n stat = fs.statSync(absoluteFile);\n } catch {\n throw new MissingProductionPublicFileError(publicFile, absoluteFile);\n }\n\n if (!stat.isFile()) {\n throw new MissingProductionPublicFileError(publicFile, absoluteFile);\n }\n\n router.file(`/${publicFile}`, absoluteFile);\n }\n}\n"],"mappings":";;;;AAIA,IAAa,mCAAb,cAAsD,MAAM;CAC1D,AAAO,YAAY,YAAoB;EACrC,MACE,0CAA0C,KAAK,UAAU,UAAU,EAAE,8FAGvE;EACA,KAAK,OAAO;CACd;AACF;AAEA,IAAa,mCAAb,cAAsD,MAAM;CAC1D,AAAO,YAAY,YAAoB,cAAsB;EAC3D,MACE,0CAA0C,KAAK,UAAU,UAAU,EAAE,mDAEhE,KAAK,UAAU,YAAY,EAAE,2FAEpC;EACA,KAAK,OAAO;CACd;AACF;AAEA,SAAS,yBAAyB,YAA8B;CAC9D,MAAM,WAAW,WAAW,MAAM,GAAG;CAErC,IACE,WAAW,WAAW,KACtB,WAAW,WAAW,GAAG,KACzB,WAAW,SAAS,IAAI,KACxB,SAAS,MAAM,YAAY,QAAQ,WAAW,KAAK,YAAY,OAAO,YAAY,IAAI,GAEtF,MAAM,IAAI,iCAAiC,UAAU;CAGvD,OAAO;AACT;;AAGA,SAAgB,8BACd,QACA,WACA,aACM;CACN,MAAM,aAAa,KAAK,KAAK,WAAW,QAAQ;CAEhD,KAAK,MAAM,cAAc,aAAa;EACpC,MAAM,eAAe,KAAK,KAAK,YAAY,GAAG,yBAAyB,UAAU,CAAC;EAElF,IAAI;EAEJ,IAAI;GACF,OAAO,GAAG,SAAS,YAAY;EACjC,QAAQ;GACN,MAAM,IAAI,iCAAiC,YAAY,YAAY;EACrE;EAEA,IAAI,CAAC,KAAK,OAAO,GACf,MAAM,IAAI,iCAAiC,YAAY,YAAY;EAGrE,OAAO,KAAK,IAAI,cAAc,YAAY;CAC5C;AACF"}
@@ -1,11 +1,14 @@
1
1
  import { PAYLOAD_SCRIPT_ID, escapePayload } from "../components/document-context.mjs";
2
- import { BufferedCookie } from "./buffered-response.mjs";
2
+ import { BufferedCookie } from "./settle-page-response.mjs";
3
3
  import { ExecutePageRequestOptions, PageDataBundle, PageRouteEntry } from "./execute-page-request.types.mjs";
4
- //#region ../web/src/server/render-page.d.ts
4
+ import { ErrorPageModuleLoader } from "./error-page.mjs";
5
+ import { Request, Response } from "@warlock.js/core";
6
+
7
+ //#region web/src/server/render-page.d.ts
5
8
  /**
6
9
  * Pipeline stages 9–10: RENDER the page tree from the
7
10
  * data bundle stages 1–8 produced, then return finalized { html, status,
8
- * headers, cookies }. Stage 10 happens at the CALL SITE in two halves —
11
+ * headers }. Stage 10 happens at the CALL SITE in two halves —
9
12
  * 10a the caller applies status + headers (the single live-response write,
10
13
  * after render, before anything flushes), 10b it flushes
11
14
  * the document. Nothing in this module writes the live response. It never
@@ -39,7 +42,8 @@ type RenderPageOptions = {
39
42
  */
40
43
  as?: unknown; /** Per-call overrides of the connected registry (tests, mostly). */
41
44
  routes?: readonly PageRouteEntry[];
42
- createHttp?: ExecutePageRequestOptions["createHttp"];
45
+ createHttp?: ExecutePageRequestOptions["createHttp"]; /** Loaded only after the ordinary boundary chain has been exhausted. */
46
+ loadErrorPage?: ErrorPageModuleLoader;
43
47
  };
44
48
  /**
45
49
  * `renderPageRequest` takes the URL itself, so `params`/`query` (the
@@ -50,14 +54,7 @@ type RenderPageRequestOptions = Omit<RenderPageOptions, "params" | "query">;
50
54
  type RenderedPage = {
51
55
  /** The full document ("" when the pipeline short-circuited before render). */html: string;
52
56
  status: number; /** Committed response headers, lowercased key → value. */
53
- headers: Record<string, string>;
54
- /**
55
- * Committed cookies in commit order, attribute-faithful: each entry carries
56
- * the loader's raw value (pre-serialization) AND its options
57
- * (`httpOnly`/`secure`/`sameSite`/`path`/`expires`/…). Never flattened to a
58
- * name→value map — a map cannot express the attributes, and a Set-Cookie
59
- * built without them is a security defect, not a convenience.
60
- */
57
+ headers: Record<string, string>; /** Committed response cookies, in commit order — stage 7's `bundle.commit.cookies`. */
61
58
  cookies: BufferedCookie[];
62
59
  /**
63
60
  * The PAGE loader's data — `data.product.name` reads as the dx story
@@ -72,7 +69,7 @@ type RenderedPage = {
72
69
  */
73
70
  bundle: PageDataBundle | undefined;
74
71
  };
75
- declare function renderPage(routeName: string, options?: RenderPageOptions): Promise<RenderedPage>;
72
+ declare function renderPage(routeName: string, options?: RenderPageOptions): Promise<RenderedPage | Response>;
76
73
  /**
77
74
  * The URL-based sibling of `renderPage` — the production render surface: a
78
75
  * real HTTP server has a URL, not a route name. The url goes STRAIGHT to
@@ -83,7 +80,7 @@ declare function renderPage(routeName: string, options?: RenderPageOptions): Pro
83
80
  * URL matching no route is a legitimate 404, and a server must ANSWER it —
84
81
  * `{ html: "", status: 404 }` with an undefined `bundle` (see RenderedPage).
85
82
  */
86
- declare function renderPageRequest(url: string, options?: RenderPageRequestOptions): Promise<RenderedPage>;
83
+ declare function renderPageRequest(url: string, options?: RenderPageRequestOptions): Promise<RenderedPage | Response>;
87
84
  //#endregion
88
85
  export { PageRoutesRegistry, RenderPageOptions, RenderPageRequestOptions, RenderedPage, connectPageRoutes, renderPage, renderPageRequest };
89
86
  //# sourceMappingURL=render-page.d.mts.map