@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,42 @@
1
+ import { matchPath } from "./match-page-route.mjs";
2
+ import { isNotFoundPageFile } from "./not-found-page.mjs";
3
+ import { discoverPages, isDiscoveredRoutablePage, toPosix } from "../build/discover-pages.mjs";
4
+ import path from "node:path";
5
+
6
+ //#region web/src/server/unregistered-pages.ts
7
+ function fileKey(file) {
8
+ const posix = toPosix(file);
9
+ return process.platform === "win32" ? posix.toLowerCase() : posix;
10
+ }
11
+ function findUnregisteredPages(options) {
12
+ const { appRoot, appSrcRoot, registeredPageFiles, discover = discoverPages } = options;
13
+ const registered = new Set(registeredPageFiles().map(fileKey));
14
+ const webRoot = path.resolve(appSrcRoot, "web");
15
+ return discover({
16
+ appRoot,
17
+ srcDir: path.relative(appRoot, appSrcRoot)
18
+ }).filter(isDiscoveredRoutablePage).filter((page) => path.resolve(page.webRoot) === webRoot && !isNotFoundPageFile(page.pageFile) && !registered.has(fileKey(page.pageFile)));
19
+ }
20
+ function describeUnregisteredPages(pageFiles, appRoot, request) {
21
+ const named = pageFiles.map((pageFile) => ` - ${toPosix(path.relative(appRoot, pageFile))}`).join("\n");
22
+ return `[warlock:web] ${request.method} ${request.url} answered 404. These src/web page files exist on disk but are absent from the active route table:
23
+ ` + named;
24
+ }
25
+ function createUnregisteredPageReporter(options) {
26
+ const { appRoot, warn = console.warn } = options;
27
+ const reported = /* @__PURE__ */ new Set();
28
+ return (request) => {
29
+ try {
30
+ const unregistered = findUnregisteredPages(options).find((page) => !reported.has(fileKey(page.pageFile)) && matchPath(page.routePath, request.pathname) !== void 0);
31
+ if (unregistered === void 0) return;
32
+ reported.add(fileKey(unregistered.pageFile));
33
+ warn(describeUnregisteredPages([unregistered.pageFile], appRoot, request));
34
+ } catch {
35
+ return;
36
+ }
37
+ };
38
+ }
39
+
40
+ //#endregion
41
+ export { createUnregisteredPageReporter };
42
+ //# sourceMappingURL=unregistered-pages.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unregistered-pages.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/unregistered-pages.ts"],"sourcesContent":["import path from \"node:path\";\nimport {\n discoverPages,\n isDiscoveredRoutablePage,\n toPosix,\n type DiscoverPagesOptions,\n type DiscoveredPage,\n} from \"../build/discover-pages\";\nimport { matchPath } from \"./match-page-route\";\nimport { isNotFoundPageFile } from \"./not-found-page\";\n\ntype DiscoveredGlobalPage = Extract<DiscoveredPage, { type: \"page\" }>;\ntype DiscoverPages = (options: DiscoverPagesOptions) => readonly DiscoveredPage[];\n\nexport type UnregisteredPagesOptions = {\n appRoot: string;\n appSrcRoot: string;\n registeredPageFiles: () => readonly string[];\n discover?: DiscoverPages;\n warn?: (message: string) => void;\n};\n\nfunction fileKey(file: string): string {\n const posix = toPosix(file);\n\n return process.platform === \"win32\" ? posix.toLowerCase() : posix;\n}\n\nfunction findUnregisteredPages(options: UnregisteredPagesOptions): DiscoveredGlobalPage[] {\n const { appRoot, appSrcRoot, registeredPageFiles, discover = discoverPages } = options;\n const registered = new Set(registeredPageFiles().map(fileKey));\n const webRoot = path.resolve(appSrcRoot, \"web\");\n\n return discover({ appRoot, srcDir: path.relative(appRoot, appSrcRoot) })\n .filter(isDiscoveredRoutablePage)\n .filter(\n (page) =>\n path.resolve(page.webRoot) === webRoot &&\n !isNotFoundPageFile(page.pageFile) &&\n !registered.has(fileKey(page.pageFile)),\n );\n}\n\nexport function findUnregisteredPageFiles(options: UnregisteredPagesOptions): string[] {\n return findUnregisteredPages(options).map((page) => page.pageFile);\n}\n\nexport function describeUnregisteredPages(\n pageFiles: readonly string[],\n appRoot: string,\n request: { method: string; url: string },\n): string {\n const named = pageFiles\n .map((pageFile) => ` - ${toPosix(path.relative(appRoot, pageFile))}`)\n .join(\"\\n\");\n\n return (\n `[warlock:web] ${request.method} ${request.url} answered 404. ` +\n \"These src/web page files exist on disk but are absent from the active route table:\\n\" +\n named\n );\n}\n\nexport function createUnregisteredPageReporter(\n options: UnregisteredPagesOptions,\n): (request: { method: string; url: string; pathname: string }) => void {\n const { appRoot, warn = console.warn } = options;\n const reported = new Set<string>();\n\n return (request) => {\n // `discoverPages()` deliberately refuses malformed pages. Its install/build\n // diagnostic is authoritative, so a response hook must leave this 404 alone.\n try {\n const unregistered = findUnregisteredPages(options).find(\n (page) =>\n !reported.has(fileKey(page.pageFile)) &&\n matchPath(page.routePath, request.pathname) !== undefined,\n );\n\n if (unregistered === undefined) return;\n\n reported.add(fileKey(unregistered.pageFile));\n warn(describeUnregisteredPages([unregistered.pageFile], appRoot, request));\n } catch {\n return;\n }\n };\n}\n"],"mappings":";;;;;;AAsBA,SAAS,QAAQ,MAAsB;CACrC,MAAM,QAAQ,QAAQ,IAAI;CAE1B,OAAO,QAAQ,aAAa,UAAU,MAAM,YAAY,IAAI;AAC9D;AAEA,SAAS,sBAAsB,SAA2D;CACxF,MAAM,EAAE,SAAS,YAAY,qBAAqB,WAAW,kBAAkB;CAC/E,MAAM,aAAa,IAAI,IAAI,oBAAoB,EAAE,IAAI,OAAO,CAAC;CAC7D,MAAM,UAAU,KAAK,QAAQ,YAAY,KAAK;CAE9C,OAAO,SAAS;EAAE;EAAS,QAAQ,KAAK,SAAS,SAAS,UAAU;CAAE,CAAC,EACpE,OAAO,wBAAwB,EAC/B,QACE,SACD,KAAK,QAAQ,KAAK,OAAO,MAAM,WAC/B,CAAC,mBAAmB,KAAK,QAAQ,KACjC,CAAC,WAAW,IAAI,QAAQ,KAAK,QAAQ,CAAC,CACxC;AACJ;AAMA,SAAgB,0BACd,WACA,SACA,SACQ;CACR,MAAM,QAAQ,UACX,KAAK,aAAa,OAAO,QAAQ,KAAK,SAAS,SAAS,QAAQ,CAAC,GAAG,EACpE,KAAK,IAAI;CAEZ,OACE,iBAAiB,QAAQ,OAAO,GAAG,QAAQ,IAAI;IAE/C;AAEJ;AAEA,SAAgB,+BACd,SACsE;CACtE,MAAM,EAAE,SAAS,OAAO,QAAQ,SAAS;CACzC,MAAM,2BAAW,IAAI,IAAY;CAEjC,QAAQ,YAAY;EAGlB,IAAI;GACF,MAAM,eAAe,sBAAsB,OAAO,EAAE,MACjD,SACC,CAAC,SAAS,IAAI,QAAQ,KAAK,QAAQ,CAAC,KACpC,UAAU,KAAK,WAAW,QAAQ,QAAQ,MAAM,MACpD;GAEA,IAAI,iBAAiB,QAAW;GAEhC,SAAS,IAAI,QAAQ,aAAa,QAAQ,CAAC;GAC3C,KAAK,0BAA0B,CAAC,aAAa,QAAQ,GAAG,SAAS,OAAO,CAAC;EAC3E,QAAQ;GACN;EACF;CACF;AACF"}
@@ -2,12 +2,11 @@ import { WebBuildOptions } from "../build/contribution.mjs";
2
2
  import { WebConnectorOptions } from "./web-connector.mjs";
3
3
  import { Connector } from "@warlock.js/core";
4
4
 
5
- //#region ../web/src/server/web-connector-factory.d.ts
5
+ //#region web/src/server/web-connector-factory.d.ts
6
6
  type WebConnectorFactoryOptions = WebConnectorOptions & {
7
7
  /**
8
8
  * What web contributes to `warlock build`. Passed straight to
9
- * {@link createWebBuildContribution}; JSON-serializable values only — no
10
- * plugin or pipeline instances, so a config load never pulls Vite in.
9
+ * {@link createWebBuildContribution}; JSON-serializable values only.
11
10
  */
12
11
  build?: WebBuildOptions;
13
12
  };
@@ -1,9 +1,9 @@
1
1
  import { createWebBuildContribution } from "../build/contribution.mjs";
2
+ import { ConnectorLifecyclePhase } from "@warlock.js/core";
2
3
  import path from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
- import { ConnectorLifecyclePhase } from "@warlock.js/core";
5
5
 
6
- //#region ../web/src/server/web-connector-factory.ts
6
+ //#region web/src/server/web-connector-factory.ts
7
7
  /**
8
8
  * `webConnector()` — the ONE thing `warlock.config.ts` imports from
9
9
  * `@warlock.js/web/connector`, and the only value the `./connector` barrel
@@ -74,10 +74,11 @@ function deriveWebRoot() {
74
74
  function webConnector(options = {}) {
75
75
  const { build: buildOptions, ...connectorOptions } = options;
76
76
  const webRoot = connectorOptions.webRoot ?? deriveWebRoot();
77
+ const connectorPlugins = [...connectorOptions.plugins ?? []];
77
78
  const build = createWebBuildContribution({
78
79
  ...buildOptions,
79
80
  webRoot: buildOptions?.webRoot ?? webRoot
80
- });
81
+ }, connectorPlugins);
81
82
  let instance;
82
83
  /**
83
84
  * Load the heavy half on first lifecycle call. `boot()` is always the first
@@ -89,6 +90,7 @@ function webConnector(options = {}) {
89
90
  const { WebConnector: WebConnectorClass } = await import("./web-connector.mjs");
90
91
  instance = new WebConnectorClass({
91
92
  ...connectorOptions,
93
+ plugins: connectorPlugins,
92
94
  webRoot
93
95
  });
94
96
  }
@@ -112,7 +114,7 @@ function webConnector(options = {}) {
112
114
  shutdown: async () => {
113
115
  await instance?.shutdown();
114
116
  },
115
- shouldRestart: () => false
117
+ shouldRestart: (changedFiles) => instance?.shouldRestart(changedFiles) ?? false
116
118
  };
117
119
  }
118
120
 
@@ -1 +1 @@
1
- {"version":3,"file":"web-connector-factory.mjs","names":[],"sources":["../../../../../../../web/src/server/web-connector-factory.ts"],"sourcesContent":["/**\n * `webConnector()` — the ONE thing `warlock.config.ts` imports from\n * `@warlock.js/web/connector`, and the only value the `./connector` barrel\n * adds beyond the build/runtime seams.\n *\n * WHY THIS MODULE EXISTS AT ALL, rather than the barrel exporting\n * `WebConnector` directly: `./web-connector.ts` imports `../vite`\n * (`web-connector.ts:52` → `@babel/parser` + `magic-string`),\n * `../../../core/src/router/router` (`:51`) and `./dev-server` (`:53`, which\n * itself pulls core's http stack and `../vite`) at VALUE level. Re-exporting\n * that class from `web/src/connector/index.ts` would drag every one of those\n * into the static graph of every consuming app's config file — the exact\n * config-load weight the `./connector` subpath was created to prevent.\n *\n * So the factory returns a LAZY DELEGATE: a `Connector` whose identity fields\n * (`name`, `priority`, `lifecyclePhase`, `build`) are plain data available\n * synchronously, and whose lifecycle methods `await import(\"./web-connector\")`\n * on first use. `warlock build` reads `build` off this object and never boots\n * anything, so a build never loads Vite or React through\n * here either. This is the third instance of a pattern the codebase already\n * uses twice — `core/src/connectors/access-connector.ts:39` and\n * `web/src/server/dev-cli.ts:61` — not a new one.\n *\n * KEEP THIS MODULE LIGHT. Its whole value-level static graph is `node:path`,\n * `node:url`, `../../../core/src/connectors/types` (whose own two imports are\n * both `import type` and therefore erased — `core/src/connectors/types.ts:1-2`)\n * and `../build/contribution` (`node:fs` + `node:path` + type-only core).\n * Everything else here is `import type`, which `verbatimModuleSyntax` erases.\n */\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport {\n type Connector,\n type ConnectorBuildContribution,\n ConnectorLifecyclePhase,\n type ConnectorName,\n} from \"@warlock.js/core\";\nimport { createWebBuildContribution, type WebBuildOptions } from \"../build/contribution\";\nimport type { WebConnector, WebConnectorOptions } from \"./web-connector\";\n\n/**\n * Boot/shutdown position relative to core's own connectors.\n *\n * `ConnectorPriority.HTTP` is `5` and `ConnectorPriority.STORAGE` is `6`\n * (`core/src/connectors/types.ts:187-188`), and the manager sorts on a plain\n * numeric compare (`core/src/connectors/connectors-manager.ts:46`) — so `5.5`\n * is \"immediately after http, before everything else\".\n *\n * Defined HERE and re-exported by `./web-connector` rather than the other way\n * round: the delegate must publish `priority` synchronously, and reading it\n * from the heavy module would defeat the whole point of the delegate.\n */\nexport const WEB_CONNECTOR_PRIORITY = 5.5;\n\nexport type WebConnectorFactoryOptions = WebConnectorOptions & {\n /**\n * What web contributes to `warlock build`. Passed straight to\n * {@link createWebBuildContribution}; JSON-serializable values only — no\n * plugin or pipeline instances, so a config load never pulls Vite in.\n */\n build?: WebBuildOptions;\n};\n\n/**\n * The `@warlock.js/web` package root, derived once from THIS module's location\n * and then passed EXPLICITLY to both halves.\n *\n * `web/src/server/web-connector-factory.ts` → `web/src/server` → `web/src` →\n * `web`; published as `web/esm/server/web-connector-factory.js`, two levels up\n * is the package root under both layouts — the same arithmetic\n * `contribution.ts:136-140` documents.\n *\n * Deriving it here and handing it down means the build contribution never falls\n * back to its own `import.meta.url` guess (`contribution.ts:129-141`): one\n * derivation, one place to be wrong, and `assertWebPackageRoot` verifies it\n * against `<root>/package.json`'s `name` either way.\n */\nfunction deriveWebRoot(): string {\n return path.resolve(path.dirname(fileURLToPath(import.meta.url)), \"..\", \"..\");\n}\n\n/**\n * Construct web's connector for `warlock.config.ts > connectors`.\n *\n * @example\n * ```ts\n * export default defineConfig({ connectors: [webConnector()] });\n * ```\n */\nexport function webConnector(options: WebConnectorFactoryOptions = {}): Connector {\n const { build: buildOptions, ...connectorOptions } = options;\n const webRoot = connectorOptions.webRoot ?? deriveWebRoot();\n\n const build: ConnectorBuildContribution = createWebBuildContribution({\n ...buildOptions,\n webRoot: buildOptions?.webRoot ?? webRoot,\n });\n\n let instance: WebConnector | undefined;\n\n /**\n * Load the heavy half on first lifecycle call. `boot()` is always the first\n * of these to run (`core/src/connectors/connectors-manager.ts:87-93`), so the\n * import lands in a process that has already committed to serving pages.\n */\n const load = async (): Promise<WebConnector> => {\n if (!instance) {\n const { WebConnector: WebConnectorClass } = await import(\"./web-connector\");\n\n instance = new WebConnectorClass({ ...connectorOptions, webRoot });\n }\n\n return instance;\n };\n\n return {\n name: \"web\" satisfies ConnectorName,\n priority: WEB_CONNECTOR_PRIORITY,\n lifecyclePhase: ConnectorLifecyclePhase.Late,\n build,\n\n // Never loads the heavy half: a connector that was never booted is not\n // active, and answering that must not cost a Vite import.\n isActive: () => instance?.isActive() ?? false,\n\n boot: async () => {\n await (await load()).boot();\n },\n start: async () => {\n await (await load()).start();\n },\n restart: async () => {\n await (await load()).restart();\n },\n\n // Both of these are asked of every registered connector, including ones\n // that never booted — so neither may force the import.\n shutdown: async () => {\n await instance?.shutdown();\n },\n shouldRestart: () => false,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,MAAa,yBAAyB;;;;;;;;;;;;;;;AAyBtC,SAAS,gBAAwB;CAC/B,OAAO,KAAK,QAAQ,KAAK,QAAQ,cAAc,OAAO,KAAK,GAAG,CAAC,GAAG,MAAM,IAAI;AAC9E;;;;;;;;;AAUA,SAAgB,aAAa,UAAsC,CAAC,GAAc;CAChF,MAAM,EAAE,OAAO,cAAc,GAAG,qBAAqB;CACrD,MAAM,UAAU,iBAAiB,WAAW,cAAc;CAE1D,MAAM,QAAoC,2BAA2B;EACnE,GAAG;EACH,SAAS,cAAc,WAAW;CACpC,CAAC;CAED,IAAI;;;;;;CAOJ,MAAM,OAAO,YAAmC;EAC9C,IAAI,CAAC,UAAU;GACb,MAAM,EAAE,cAAc,sBAAsB,MAAM,OAAO;GAEzD,WAAW,IAAI,kBAAkB;IAAE,GAAG;IAAkB;GAAQ,CAAC;EACnE;EAEA,OAAO;CACT;CAEA,OAAO;EACL,MAAM;EACN,UAAU;EACV,gBAAgB,wBAAwB;EACxC;EAIA,gBAAgB,UAAU,SAAS,KAAK;EAExC,MAAM,YAAY;GAChB,OAAO,MAAM,KAAK,EAAC,CAAE,KAAK;EAC5B;EACA,OAAO,YAAY;GACjB,OAAO,MAAM,KAAK,EAAC,CAAE,MAAM;EAC7B;EACA,SAAS,YAAY;GACnB,OAAO,MAAM,KAAK,EAAC,CAAE,QAAQ;EAC/B;EAIA,UAAU,YAAY;GACpB,MAAM,UAAU,SAAS;EAC3B;EACA,qBAAqB;CACvB;AACF"}
1
+ {"version":3,"file":"web-connector-factory.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/web-connector-factory.ts"],"sourcesContent":["/**\n * `webConnector()` — the ONE thing `warlock.config.ts` imports from\n * `@warlock.js/web/connector`, and the only value the `./connector` barrel\n * adds beyond the build/runtime seams.\n *\n * WHY THIS MODULE EXISTS AT ALL, rather than the barrel exporting\n * `WebConnector` directly: `./web-connector.ts` imports `../vite`\n * (`web-connector.ts:52` → `@babel/parser` + `magic-string`),\n * `../../../core/src/router/router` (`:51`) and `./dev-server` (`:53`, which\n * itself pulls core's http stack and `../vite`) at VALUE level. Re-exporting\n * that class from `web/src/connector/index.ts` would drag every one of those\n * into the static graph of every consuming app's config file — the exact\n * config-load weight the `./connector` subpath was created to prevent.\n *\n * So the factory returns a LAZY DELEGATE: a `Connector` whose identity fields\n * (`name`, `priority`, `lifecyclePhase`, `build`) are plain data available\n * synchronously, and whose lifecycle methods `await import(\"./web-connector\")`\n * on first use. `warlock build` reads `build` off this object and never boots\n * anything, so a build never loads Vite or React through\n * here either. This is the third instance of a pattern the codebase already\n * uses twice — `core/src/connectors/access-connector.ts:39` and\n * `web/src/server/dev-cli.ts:61` — not a new one.\n *\n * KEEP THIS MODULE LIGHT. Its whole value-level static graph is `node:path`,\n * `node:url`, `../../../core/src/connectors/types` (whose own two imports are\n * both `import type` and therefore erased — `core/src/connectors/types.ts:1-2`)\n * and `../build/contribution` (`node:fs` + `node:path` + type-only core).\n * Everything else here is `import type`, which `verbatimModuleSyntax` erases.\n */\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport {\n type Connector,\n type ConnectorBuildContribution,\n ConnectorLifecyclePhase,\n type ConnectorName,\n} from \"@warlock.js/core\";\nimport {\n createWebBuildContribution,\n type WebBuildOptions,\n} from \"../build/contribution\";\nimport type { WebConnector, WebConnectorOptions } from \"./web-connector\";\n\n/**\n * Boot/shutdown position relative to core's own connectors.\n *\n * `ConnectorPriority.HTTP` is `5` and `ConnectorPriority.STORAGE` is `6`\n * (`core/src/connectors/types.ts:187-188`), and the manager sorts on a plain\n * numeric compare (`core/src/connectors/connectors-manager.ts:46`) — so `5.5`\n * is \"immediately after http, before everything else\".\n *\n * Defined HERE and re-exported by `./web-connector` rather than the other way\n * round: the delegate must publish `priority` synchronously, and reading it\n * from the heavy module would defeat the whole point of the delegate.\n */\nexport const WEB_CONNECTOR_PRIORITY = 5.5;\n\nexport type WebConnectorFactoryOptions = WebConnectorOptions & {\n /**\n * What web contributes to `warlock build`. Passed straight to\n * {@link createWebBuildContribution}; JSON-serializable values only.\n */\n build?: WebBuildOptions;\n};\n\n/**\n * The `@warlock.js/web` package root, derived once from THIS module's location\n * and then passed EXPLICITLY to both halves.\n *\n * `web/src/server/web-connector-factory.ts` → `web/src/server` → `web/src` →\n * `web`; published as `web/esm/server/web-connector-factory.js`, two levels up\n * is the package root under both layouts — the same arithmetic\n * `contribution.ts:136-140` documents.\n *\n * Deriving it here and handing it down means the build contribution never falls\n * back to its own `import.meta.url` guess (`contribution.ts:129-141`): one\n * derivation, one place to be wrong, and `assertWebPackageRoot` verifies it\n * against `<root>/package.json`'s `name` either way.\n */\nfunction deriveWebRoot(): string {\n return path.resolve(path.dirname(fileURLToPath(import.meta.url)), \"..\", \"..\");\n}\n\n/**\n * Construct web's connector for `warlock.config.ts > connectors`.\n *\n * @example\n * ```ts\n * export default defineConfig({ connectors: [webConnector()] });\n * ```\n */\nexport function webConnector(\n options: WebConnectorFactoryOptions = {},\n): Connector {\n const { build: buildOptions, ...connectorOptions } = options;\n const webRoot = connectorOptions.webRoot ?? deriveWebRoot();\n // One snapshot feeds both halves. Mutating the caller's array after config\n // construction cannot make dev and `warlock build` observe different lists.\n const connectorPlugins = [...(connectorOptions.plugins ?? [])];\n\n const build: ConnectorBuildContribution = createWebBuildContribution(\n {\n ...buildOptions,\n webRoot: buildOptions?.webRoot ?? webRoot,\n },\n connectorPlugins,\n );\n\n let instance: WebConnector | undefined;\n\n /**\n * Load the heavy half on first lifecycle call. `boot()` is always the first\n * of these to run (`core/src/connectors/connectors-manager.ts:87-93`), so the\n * import lands in a process that has already committed to serving pages.\n */\n const load = async (): Promise<WebConnector> => {\n if (!instance) {\n const { WebConnector: WebConnectorClass } =\n await import(\"./web-connector\");\n\n instance = new WebConnectorClass({\n ...connectorOptions,\n plugins: connectorPlugins,\n webRoot,\n });\n }\n\n return instance;\n };\n\n return {\n name: \"web\" satisfies ConnectorName,\n priority: WEB_CONNECTOR_PRIORITY,\n lifecyclePhase: ConnectorLifecyclePhase.Late,\n build,\n\n // Never loads the heavy half: a connector that was never booted is not\n // active, and answering that must not cost a Vite import.\n isActive: () => instance?.isActive() ?? false,\n\n boot: async () => {\n await (await load()).boot();\n },\n start: async () => {\n await (await load()).start();\n },\n restart: async () => {\n await (await load()).restart();\n },\n\n // Both of these are asked of every registered connector, including ones\n // that never booted — so neither may force the import.\n shutdown: async () => {\n await instance?.shutdown();\n },\n\n // Asked on EVERY watcher batch, so the `instance?.` is load-bearing twice:\n // it keeps the answer free for a connector that never booted, and it is the\n // only reason this delegate can forward the question at all — by the time a\n // watcher batch exists, `boot()` has already loaded the heavy half, so\n // reading the real answer off it costs nothing beyond the call.\n //\n // It used to be a hard `false`, which meant a `*.page.tsx` created while\n // `warlock dev` was running was never noticed: page routes are installed\n // once, in `WebConnector.boot()`, so the new page's URL 404'd in silence.\n // The connector itself now decides: add/remove and route-identity edits are\n // live route-table work; component-body-only edits remain Vite HMR work.\n shouldRestart: (changedFiles: string[]) =>\n instance?.shouldRestart(changedFiles) ?? false,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuDA,MAAa,yBAAyB;;;;;;;;;;;;;;;AAwBtC,SAAS,gBAAwB;CAC/B,OAAO,KAAK,QAAQ,KAAK,QAAQ,cAAc,OAAO,KAAK,GAAG,CAAC,GAAG,MAAM,IAAI;AAC9E;;;;;;;;;AAUA,SAAgB,aACd,UAAsC,CAAC,GAC5B;CACX,MAAM,EAAE,OAAO,cAAc,GAAG,qBAAqB;CACrD,MAAM,UAAU,iBAAiB,WAAW,cAAc;CAG1D,MAAM,mBAAmB,CAAC,GAAI,iBAAiB,WAAW,CAAC,CAAE;CAE7D,MAAM,QAAoC,2BACxC;EACE,GAAG;EACH,SAAS,cAAc,WAAW;CACpC,GACA,gBACF;CAEA,IAAI;;;;;;CAOJ,MAAM,OAAO,YAAmC;EAC9C,IAAI,CAAC,UAAU;GACb,MAAM,EAAE,cAAc,sBACpB,MAAM,OAAO;GAEf,WAAW,IAAI,kBAAkB;IAC/B,GAAG;IACH,SAAS;IACT;GACF,CAAC;EACH;EAEA,OAAO;CACT;CAEA,OAAO;EACL,MAAM;EACN,UAAU;EACV,gBAAgB,wBAAwB;EACxC;EAIA,gBAAgB,UAAU,SAAS,KAAK;EAExC,MAAM,YAAY;GAChB,OAAO,MAAM,KAAK,GAAG,KAAK;EAC5B;EACA,OAAO,YAAY;GACjB,OAAO,MAAM,KAAK,GAAG,MAAM;EAC7B;EACA,SAAS,YAAY;GACnB,OAAO,MAAM,KAAK,GAAG,QAAQ;EAC/B;EAIA,UAAU,YAAY;GACpB,MAAM,UAAU,SAAS;EAC3B;EAaA,gBAAgB,iBACd,UAAU,cAAc,YAAY,KAAK;CAC7C;AACF"}
@@ -1,7 +1,7 @@
1
1
  import { BaseConnector, ConnectorLifecyclePhase } from "@warlock.js/core";
2
2
  import { Alias, PluginOption, ViteDevServer } from "vite";
3
3
 
4
- //#region ../web/src/server/web-connector.d.ts
4
+ //#region web/src/server/web-connector.d.ts
5
5
  type WebConnectorOptions = {
6
6
  /**
7
7
  * Vite's `root` — the application directory that owns `src/`, `package.json`
@@ -4,16 +4,21 @@ import { consumePageManifest } from "./page-manifest.mjs";
4
4
  import { createHydrationClientEntry } from "../vite/hydration-entries.mjs";
5
5
  import { resolveHydrationClientUrl } from "./hydration-client-url.mjs";
6
6
  import { WEB_CONNECTOR_PRIORITY } from "./web-connector-factory.mjs";
7
+ import { invalidateClientPageRegistry } from "../vite/page-registry-plugin.mjs";
7
8
  import { warlockClientBoundary } from "../vite/index.mjs";
8
9
  import { appConventionAliases } from "../vite/app-convention-aliases.mjs";
9
- import { applyBufferedCookie, devErrorTransportPlugin, sendCapturedDevError } from "./dev-server.mjs";
10
+ import { devErrorTransportPlugin, sendCapturedDevError } from "./dev-server.mjs";
10
11
  import { installProductionPageRoutes } from "./install-production-page-routes.mjs";
11
- import path from "node:path";
12
+ import { classifyPageFileChanges, hasPageFileChanges } from "./page-file-change.mjs";
13
+ import { pageRouteSourceFiles, pageRoutesNeedReplacement, registeredPageFiles } from "./page-route-reload.mjs";
14
+ import { registerProductionPublicFiles } from "./register-production-public-files.mjs";
15
+ import { createUnregisteredPageReporter } from "./unregistered-pages.mjs";
16
+ import { Application, BaseConnector, ConnectorLifecyclePhase, container, requestContext, router } from "@warlock.js/core";
12
17
  import fs from "node:fs";
18
+ import path from "node:path";
13
19
  import { fileURLToPath } from "node:url";
14
- import { Application, BaseConnector, ConnectorLifecyclePhase, container, requestContext, router } from "@warlock.js/core";
15
20
 
16
- //#region ../web/src/server/web-connector.ts
21
+ //#region web/src/server/web-connector.ts
17
22
  /**
18
23
  * `WebConnector` — the SSR page surface as a first-class Warlock connector.
19
24
  *
@@ -89,6 +94,23 @@ import { Application, BaseConnector, ConnectorLifecyclePhase, container, request
89
94
  function isProductionRuntime() {
90
95
  return Application.runtimeStrategy === "production";
91
96
  }
97
+ function pageFileVersion(file) {
98
+ try {
99
+ return `present:${fs.readFileSync(file, "utf8")}`;
100
+ } catch {
101
+ return "absent";
102
+ }
103
+ }
104
+ function pageChangeVersions(changes) {
105
+ return new Map([
106
+ ...changes.added,
107
+ ...changes.removed,
108
+ ...changes.inspectionNeeded
109
+ ].map((file) => {
110
+ const absoluteFile = path.resolve(file);
111
+ return [absoluteFile, pageFileVersion(absoluteFile)];
112
+ }));
113
+ }
92
114
  var WebPageManifestMissingError = class extends Error {
93
115
  constructor() {
94
116
  super("WebConnector booted in production without a page manifest. The generated `pages.ts` barrel never ran, so no page modules were handed to the connector and there is nothing to serve. Run `warlock build` with the web connector registered in `warlock.config.ts > connectors`, and start the artifact that build produced.");
@@ -96,7 +118,7 @@ var WebPageManifestMissingError = class extends Error {
96
118
  }
97
119
  };
98
120
  /**
99
- * The manifest carried pages but no `clientDir`.
121
+ * The manifest carried browser artifacts but no `clientDir`.
100
122
  *
101
123
  * The build bakes that field in beside the page table, so the only way to
102
124
  * observe this is a VERSION SPLIT: an artifact produced by a `@warlock.js/web`
@@ -106,7 +128,7 @@ var WebPageManifestMissingError = class extends Error {
106
128
  */
107
129
  var WebClientDirMissingError = class extends Error {
108
130
  constructor() {
109
- super("The page manifest carries pages but no `clientDir`, so the connector cannot locate the client bundle. This artifact was built by an older @warlock.js/web than the one now running it. Re-run `warlock build` to regenerate the `pages.ts` barrel against the current version.");
131
+ super("The page manifest carries pages or public files but no `clientDir`, so the connector cannot locate the browser artifacts. This artifact was built by an older @warlock.js/web than the one now running it. Re-run `warlock build` to regenerate the `pages.ts` barrel against the current version.");
110
132
  this.name = "WebClientDirMissingError";
111
133
  }
112
134
  };
@@ -194,8 +216,9 @@ var WebConnector = class extends BaseConnector {
194
216
  priority = WEB_CONNECTOR_PRIORITY;
195
217
  lifecyclePhase = ConnectorLifecyclePhase.Late;
196
218
  /**
197
- * Nothing. Page, layout and component edits are Vite's HMR to own — a
198
- * connector restart would tear down the module graph Vite is keeping warm.
219
+ * Nothing. Core already supplies each watcher batch to `shouldRestart`; page
220
+ * membership and route identity are classified there, while component and
221
+ * layout body edits remain Vite's HMR domain.
199
222
  */
200
223
  watchedFiles = [];
201
224
  options;
@@ -209,6 +232,21 @@ var WebConnector = class extends BaseConnector {
209
232
  * production reading is a hard error and why the branch is on MODE.
210
233
  */
211
234
  pageManifest;
235
+ /**
236
+ * The paths the DEVELOPMENT boot resolved, kept so `shouldRestart` can decide
237
+ * whether a changed file is a page without re-deriving (and re-proving) the
238
+ * web package root on every watcher batch. `undefined` in production and
239
+ * before boot, which is exactly when `shouldRestart` must answer `false`.
240
+ */
241
+ resolvedPaths;
242
+ /** Synchronous watcher classification handed to the async reload phase. */
243
+ pendingPageChanges;
244
+ /** Reuses Vite's pipeline-barrel module instance for every dev reinstall. */
245
+ installDevPageRoutes;
246
+ /** Serializes Vite and core watcher callbacks that can observe the same edit. */
247
+ pageRouteReloadQueue = Promise.resolve();
248
+ /** Committed file versions awaiting the overlapping watcher callback. */
249
+ pendingHotUpdateSuppressions = /* @__PURE__ */ new Map();
212
250
  constructor(options = {}) {
213
251
  super();
214
252
  this.options = options;
@@ -231,12 +269,12 @@ var WebConnector = class extends BaseConnector {
231
269
  this.pageManifest = consumePageManifest();
232
270
  if (isProductionRuntime() && !this.pageManifest) throw new WebPageManifestMissingError();
233
271
  if (isProductionRuntime() && this.pageManifest) {
272
+ if ((this.pageManifest.publicFiles?.length ?? 0) > 0) registerProductionPublicFiles(router, this.resolveClientDir(), this.pageManifest.publicFiles ?? []);
234
273
  this.installedPages = await installProductionPageRoutes({
235
274
  router,
236
275
  manifest: this.pageManifest,
237
276
  pageContext: requestContext,
238
277
  sharedStore: () => requestContext.getStore(),
239
- applyBufferedCookie,
240
278
  resolveHydrationClientModuleUrl: () => this.resolveHydrationClientModuleUrl(),
241
279
  clientDir: this.pageManifest.clientDir
242
280
  });
@@ -248,6 +286,7 @@ var WebConnector = class extends BaseConnector {
248
286
  }
249
287
  const fastify = this.resolveFastify();
250
288
  const paths = await this.resolvePaths();
289
+ this.resolvedPaths = paths;
251
290
  this.vite = await this.createViteServer(fastify, paths);
252
291
  const webServerSsr = await this.vite.ssrLoadModule(paths.webServerBarrel);
253
292
  webServerSsr.connectSharedStore(() => requestContext.getStore());
@@ -258,15 +297,28 @@ var WebConnector = class extends BaseConnector {
258
297
  done(error);
259
298
  });
260
299
  });
261
- this.installedPages = await webServerSsr.installPageRoutes({
300
+ const reportUnregisteredPages = createUnregisteredPageReporter({
301
+ appRoot: paths.appRoot,
302
+ appSrcRoot: paths.appSrcRoot,
303
+ registeredPageFiles: () => registeredPageFiles(router.list(), paths.appSrcRoot)
304
+ });
305
+ fastify.addHook("onResponse", (request, reply, done) => {
306
+ if (reply.statusCode === 404) reportUnregisteredPages({
307
+ method: request.method,
308
+ url: request.url,
309
+ pathname: new URL(request.url, "http://warlock.local").pathname
310
+ });
311
+ done();
312
+ });
313
+ this.installDevPageRoutes = () => webServerSsr.installPageRoutes({
262
314
  router,
263
315
  vite: this.vite,
264
316
  appSrcRoot: paths.appSrcRoot,
265
317
  appFile: paths.appFile,
266
318
  hydrationClientModuleUrl: this.resolveHydrationClientModuleUrl(paths.webRoot),
267
- stylesheetUrls: webServerSsr.devStylesheetUrls(paths.appRoot, paths.appFile),
268
- applyBufferedCookie
319
+ stylesheetUrls: webServerSsr.devStylesheetUrls(paths.appRoot, paths.appFile)
269
320
  });
321
+ this.installedPages = await this.installDevPageRoutes();
270
322
  }
271
323
  /**
272
324
  * Where the browser fetches the hydration entry from — the one line that
@@ -345,16 +397,81 @@ var WebConnector = class extends BaseConnector {
345
397
  await this.vite?.close();
346
398
  this.vite = void 0;
347
399
  this.installedPages = [];
400
+ this.installDevPageRoutes = void 0;
401
+ this.pendingPageChanges = void 0;
402
+ this.pendingHotUpdateSuppressions.clear();
348
403
  this.pageManifest = void 0;
349
404
  this.active = false;
350
405
  }
351
406
  /**
352
- * Never restart on a file change. Pages, layouts and components are Vite's
353
- * HMR domain; rebooting this connector would drop Vite's module graph and
354
- * re-register every page route on a Fastify instance that is already serving.
407
+ * Queue page add/remove/edit candidates for asynchronous live routing work.
408
+ * Classification stays synchronous because core's connector interface is;
409
+ * edited route exports are evaluated later through Vite's fresh SSR graph.
410
+ */
411
+ shouldRestart(changedFiles = []) {
412
+ if (this.vite === void 0 || this.resolvedPaths === void 0) return false;
413
+ this.pendingPageChanges = this.classifyPageChanges(changedFiles);
414
+ return this.pendingPageChanges !== void 0;
415
+ }
416
+ classifyPageChanges(changedFiles) {
417
+ if (this.resolvedPaths === void 0) return void 0;
418
+ const changes = classifyPageFileChanges(changedFiles, {
419
+ appRoot: this.resolvedPaths.appRoot,
420
+ appSrcRoot: this.resolvedPaths.appSrcRoot,
421
+ installedPageFiles: registeredPageFiles(router.list(), this.resolvedPaths.appSrcRoot)
422
+ });
423
+ return hasPageFileChanges(changes) ? changes : void 0;
424
+ }
425
+ enqueuePageRouteReload(changes) {
426
+ const eventVersions = pageChangeVersions(changes);
427
+ const run = this.pageRouteReloadQueue.catch(() => void 0).then(async () => {
428
+ const vite = this.vite;
429
+ const install = this.installDevPageRoutes;
430
+ const paths = this.resolvedPaths;
431
+ if (vite === void 0 || install === void 0 || paths === void 0) return false;
432
+ const matchingCommittedFiles = /* @__PURE__ */ new Set();
433
+ for (const [file, eventVersion] of eventVersions) {
434
+ const committedVersion = this.pendingHotUpdateSuppressions.get(file);
435
+ if (committedVersion === void 0) continue;
436
+ this.pendingHotUpdateSuppressions.delete(file);
437
+ if (committedVersion === eventVersion) matchingCommittedFiles.add(file);
438
+ }
439
+ if (eventVersions.size > 0 && matchingCommittedFiles.size === eventVersions.size) return true;
440
+ if (!await pageRoutesNeedReplacement(changes, {
441
+ vite,
442
+ appSrcRoot: paths.appSrcRoot,
443
+ installedPages: this.installedPages
444
+ })) return false;
445
+ const nextInstalledPages = await router.replaceRoutesBySourceFiles(pageRouteSourceFiles(router.list()), install);
446
+ this.installedPages = nextInstalledPages;
447
+ invalidateClientPageRegistry(vite);
448
+ for (const [file, eventVersion] of eventVersions) if (!matchingCommittedFiles.has(file)) this.pendingHotUpdateSuppressions.set(file, eventVersion);
449
+ return true;
450
+ });
451
+ this.pageRouteReloadQueue = run;
452
+ return run;
453
+ }
454
+ /**
455
+ * Vite-side ordering barrier. It publishes a changed route graph before the
456
+ * page-registry plugin can reload the document; a matching change already
457
+ * handled by core is consumed once, preventing a duplicate full reload.
458
+ */
459
+ async handlePageHotUpdate(file) {
460
+ const absoluteFile = path.resolve(file);
461
+ const changes = this.classifyPageChanges([absoluteFile]);
462
+ if (changes === void 0) return false;
463
+ return this.enqueuePageRouteReload(changes);
464
+ }
465
+ /**
466
+ * Live page routing update. Despite the connector API name, this never closes
467
+ * Vite: it atomically replaces page-owned routes only when membership or the
468
+ * canonical route identity changed, then refreshes the client registry.
355
469
  */
356
- shouldRestart() {
357
- return false;
470
+ async restart() {
471
+ const changes = this.pendingPageChanges;
472
+ this.pendingPageChanges = void 0;
473
+ if (changes === void 0 || this.resolvedPaths === void 0 || this.vite === void 0 || this.installDevPageRoutes === void 0) return;
474
+ await this.enqueuePageRouteReload(changes);
358
475
  }
359
476
  /** The pages this connector registered on the router, in registration order. */
360
477
  getInstalledPages() {
@@ -481,7 +598,10 @@ var WebConnector = class extends BaseConnector {
481
598
  isProductionRuntime,
482
599
  buildErrorMessage
483
600
  }),
484
- ...warlockClientBoundary({ appRoot: paths.appRoot }),
601
+ ...warlockClientBoundary({
602
+ appRoot: paths.appRoot,
603
+ beforePageHotUpdate: ({ file }) => this.handlePageHotUpdate(file)
604
+ }),
485
605
  ...await this.resolveReactFastRefreshPlugins(paths),
486
606
  ...this.options.plugins ?? []
487
607
  ],
@@ -498,6 +618,101 @@ var WebConnector = class extends BaseConnector {
498
618
  target: "es2022",
499
619
  jsx: "automatic"
500
620
  },
621
+ /**
622
+ * REACT MUST BE PRE-BUNDLED, and naming it here is the only thing that
623
+ * makes that happen for the framework's own client graph.
624
+ *
625
+ * `react-dom/client` is CommonJS. A browser cannot import a named export
626
+ * from it, so Vite's dep optimizer normally rewrites it into an ESM shim
627
+ * under `/node_modules/.vite/deps/`. Whether that rewrite happens is
628
+ * decided in `tryNodeResolve`, and one of the conditions that SKIPS it is
629
+ * `importer && isInNodeModules(importer)`
630
+ * (`vite/dist/node/chunks/config.js:32822`). Vite's reasoning is sound in
631
+ * general — a dependency's own internal imports are the optimizer's job,
632
+ * not the resolver's — but it is exactly wrong here:
633
+ *
634
+ * `<Scripts />` points the browser at the hydration entry, and in an
635
+ * INSTALLED app that entry is
636
+ * `<app>/node_modules/@warlock.js/web/esm/hydration/index.mjs`.
637
+ * Every module it reaches is therefore inside `node_modules`, so every
638
+ * bare import it makes takes the skip branch and is served as the raw
639
+ * file with a `?v=<browserHash>` cache key bolted on.
640
+ *
641
+ * For `esm/**.mjs` that is harmless — they are already ESM. For
642
+ * `react-dom/client` it is fatal, and it is the whole defect: the browser
643
+ * receives `"use strict"; function checkDCE()…` and refuses the module
644
+ * with
645
+ *
646
+ * SyntaxError: The requested module '/node_modules/react-dom/client.js?v=…'
647
+ * does not provide an export named 'hydrateRoot'
648
+ *
649
+ * Nothing in the client runtime then runs at all: no hydration, so no
650
+ * `useState`, no Fast Refresh, and `<Link>` degrades to a full document
651
+ * load because the navigation listener was never attached. Measured on a
652
+ * published 5.0.2 install (fresh app, no symlinks): all four symptoms
653
+ * before this block, none after.
654
+ *
655
+ * `include` is the fix rather than `entries` because it does not depend
656
+ * on the scanner reaching the entry: an included id is pre-bundled at
657
+ * server start and `tryOptimizedResolve` matches it by NAME
658
+ * (`config.js:32633`) before `tryNodeResolve` — and that lookup has no
659
+ * importer condition, so a node_modules importer resolves to the shim
660
+ * like anyone else. Pointing `optimizeDeps.entries` at the hydration
661
+ * entry would not work: the scanner classifies a resolution inside
662
+ * `node_modules` as a dependency to externalize rather than a source to
663
+ * walk.
664
+ *
665
+ * All four names are listed even though `react` and `react/jsx-runtime`
666
+ * usually get discovered anyway — they are discovered only because the
667
+ * APP's own pages import them, which is a fact about the app and not
668
+ * something the framework may rely on. `react-dom/client` is imported by
669
+ * `client/hydrate-page.tsx` and by nothing a normal app writes, which is
670
+ * why it was the one that broke.
671
+ *
672
+ * This is INVISIBLE from the monorepo checkout: there the hydration entry
673
+ * resolves to `web/src/hydration/index.ts`, a path with no `node_modules`
674
+ * segment, so the skip branch never fires and React optimizes normally.
675
+ * Canon: nothing measured inside the checkout is evidence about a
676
+ * published install.
677
+ */
678
+ optimizeDeps: {
679
+ include: [
680
+ "react",
681
+ "react-dom",
682
+ "react-dom/client",
683
+ "react/jsx-runtime"
684
+ ],
685
+ /**
686
+ * The SECOND instance problem, and the reason this is `exclude` and not
687
+ * another `include`.
688
+ *
689
+ * An app page's `import { Link } from "@warlock.js/web"` has an importer
690
+ * OUTSIDE `node_modules`, so it takes the opposite branch above and the
691
+ * whole package is pre-bundled into
692
+ * `/node_modules/.vite/deps/@warlock__js_web.js`. The hydration entry
693
+ * cannot reach that bundle — it is loaded by absolute path through
694
+ * `/@fs/` and its imports are relative, so they resolve to the raw
695
+ * `esm/*.mjs` files. The browser then holds TWO copies of every client
696
+ * module in this package: the app renders `<Link>` from the bundled one
697
+ * while the hydration entry publishes routes and mounts the document
698
+ * context on the raw one. Module-level state does not cross that line.
699
+ *
700
+ * `routing/route-table` survives it by accident — it keeps its table in
701
+ * a `Symbol.for` slot on `globalThis`, which is realm-wide — but
702
+ * `components/document-context` is a React context OBJECT, and two
703
+ * `createContext()` calls are two different contexts no provider can
704
+ * bridge.
705
+ *
706
+ * Excluding the package makes the app's import resolve to
707
+ * `esm/index.mjs` with the same `?v=` key the hydration entry's imports
708
+ * carry, so there is one instance again. It costs the pre-bundle (a
709
+ * handful of extra dev requests for files that are already valid ESM)
710
+ * and nothing else — the grep across every client-reachable module in
711
+ * this package finds exactly three bare imports, all of them React, all
712
+ * of them covered by `include` above.
713
+ */
714
+ exclude: ["@warlock.js/web"]
715
+ },
501
716
  ssr: {
502
717
  external: [
503
718
  ...CORE_OPTIONAL_PEERS,