@warlock.js/web 5.1.0 → 5.2.1

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 (148) hide show
  1. package/CHANGELOG.md +182 -0
  2. package/README.md +126 -12
  3. package/esm/build/contribution.d.mts +10 -13
  4. package/esm/build/contribution.mjs +45 -57
  5. package/esm/build/contribution.mjs.map +1 -1
  6. package/esm/build/discover-pages.mjs +104 -44
  7. package/esm/build/discover-pages.mjs.map +1 -1
  8. package/esm/build/generate-client-registry.mjs +46 -12
  9. package/esm/build/generate-client-registry.mjs.map +1 -1
  10. package/esm/build/generate-pages-barrel.mjs +54 -12
  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.map +1 -1
  19. package/esm/client/build-hydrated-tree.mjs +39 -8
  20. package/esm/client/build-hydrated-tree.mjs.map +1 -1
  21. package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
  22. package/esm/client/navigation/prefetch.mjs.map +1 -1
  23. package/esm/client/runtime/index.d.mts +2 -1
  24. package/esm/client/runtime/index.mjs +2 -1
  25. package/esm/client/runtime/manifest.mjs +2 -1
  26. package/esm/client/runtime/manifest.mjs.map +1 -1
  27. package/esm/client/runtime/matcher.d.mts +12 -0
  28. package/esm/client/runtime/matcher.mjs +12 -0
  29. package/esm/client/runtime/matcher.mjs.map +1 -1
  30. package/esm/client/runtime/types.d.mts +14 -1
  31. package/esm/components/document-context.d.mts +39 -1
  32. package/esm/components/document-context.mjs.map +1 -1
  33. package/esm/components/link.mjs.map +1 -1
  34. package/esm/components/scripts.mjs +3 -1
  35. package/esm/components/scripts.mjs.map +1 -1
  36. package/esm/context.d.mts +5 -101
  37. package/esm/hydration-payload.d.mts +1 -1
  38. package/esm/hydration-payload.mjs +36 -7
  39. package/esm/hydration-payload.mjs.map +1 -1
  40. package/esm/index.d.mts +3 -2
  41. package/esm/loaders.d.mts +6 -35
  42. package/esm/props.d.mts +15 -7
  43. package/esm/route.d.mts +1 -16
  44. package/esm/routing/filesystem-route.mjs +45 -0
  45. package/esm/routing/filesystem-route.mjs.map +1 -0
  46. package/esm/routing/layout-policy.mjs.map +1 -1
  47. package/esm/routing/query-string.mjs.map +1 -1
  48. package/esm/routing/route-table.mjs.map +1 -1
  49. package/esm/runtime/register-modules.d.mts +19 -0
  50. package/esm/runtime/register-modules.mjs +22 -0
  51. package/esm/runtime/register-modules.mjs.map +1 -0
  52. package/esm/server/build-hydration-payload.mjs +1 -0
  53. package/esm/server/build-hydration-payload.mjs.map +1 -1
  54. package/esm/server/create-page-module-loader.mjs +1 -0
  55. package/esm/server/create-page-module-loader.mjs.map +1 -1
  56. package/esm/server/create-page-route-handler.d.mts +30 -5
  57. package/esm/server/create-page-route-handler.mjs +127 -42
  58. package/esm/server/create-page-route-handler.mjs.map +1 -1
  59. package/esm/server/dev-server.mjs +1 -12
  60. package/esm/server/dev-server.mjs.map +1 -1
  61. package/esm/server/error-page.d.mts +14 -0
  62. package/esm/server/error-page.mjs +39 -0
  63. package/esm/server/error-page.mjs.map +1 -0
  64. package/esm/server/execute-page-request.d.mts +6 -3
  65. package/esm/server/execute-page-request.mjs +78 -79
  66. package/esm/server/execute-page-request.mjs.map +1 -1
  67. package/esm/server/execute-page-request.types.d.mts +32 -91
  68. package/esm/server/hydration-client-url.mjs +1 -1
  69. package/esm/server/index.d.mts +5 -5
  70. package/esm/server/index.mjs +3 -4
  71. package/esm/server/install-page-routes-from-manifest.d.mts +16 -6
  72. package/esm/server/install-page-routes-from-manifest.mjs +70 -9
  73. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  74. package/esm/server/install-page-routes.d.mts +31 -14
  75. package/esm/server/install-page-routes.mjs +90 -38
  76. package/esm/server/install-page-routes.mjs.map +1 -1
  77. package/esm/server/install-production-page-routes.mjs +2 -35
  78. package/esm/server/install-production-page-routes.mjs.map +1 -1
  79. package/esm/server/match-page-route.mjs +1 -1
  80. package/esm/server/match-page-route.mjs.map +1 -1
  81. package/esm/server/not-found-page.mjs +1 -1
  82. package/esm/server/not-found-page.mjs.map +1 -1
  83. package/esm/server/page-context.d.mts +2 -8
  84. package/esm/server/page-context.mjs +2 -15
  85. package/esm/server/page-context.mjs.map +1 -1
  86. package/esm/server/page-file-change.mjs +77 -0
  87. package/esm/server/page-file-change.mjs.map +1 -0
  88. package/esm/server/page-manifest.d.mts +24 -9
  89. package/esm/server/page-manifest.mjs.map +1 -1
  90. package/esm/server/page-render-bundle.mjs +38 -0
  91. package/esm/server/page-render-bundle.mjs.map +1 -0
  92. package/esm/server/page-route-reload.mjs +54 -0
  93. package/esm/server/page-route-reload.mjs.map +1 -0
  94. package/esm/server/register-production-public-files.mjs +40 -0
  95. package/esm/server/register-production-public-files.mjs.map +1 -0
  96. package/esm/server/render-page.d.mts +10 -13
  97. package/esm/server/render-page.mjs +153 -27
  98. package/esm/server/render-page.mjs.map +1 -1
  99. package/esm/server/resolve-page-metadata.mjs +1 -1
  100. package/esm/server/resolve-page-metadata.mjs.map +1 -1
  101. package/esm/server/settle-page-response.d.mts +58 -1
  102. package/esm/server/settle-page-response.mjs +110 -45
  103. package/esm/server/settle-page-response.mjs.map +1 -1
  104. package/esm/server/stylesheet-urls.d.mts +40 -29
  105. package/esm/server/stylesheet-urls.mjs +136 -51
  106. package/esm/server/stylesheet-urls.mjs.map +1 -1
  107. package/esm/server/unregistered-pages.mjs +42 -0
  108. package/esm/server/unregistered-pages.mjs.map +1 -0
  109. package/esm/server/web-connector-factory.d.mts +1 -2
  110. package/esm/server/web-connector-factory.mjs +6 -5
  111. package/esm/server/web-connector-factory.mjs.map +1 -1
  112. package/esm/server/web-connector.mjs +137 -17
  113. package/esm/server/web-connector.mjs.map +1 -1
  114. package/esm/shared.d.mts +17 -2
  115. package/esm/shared.mjs +2 -6
  116. package/esm/shared.mjs.map +1 -1
  117. package/esm/vite/build-client.mjs +1 -1
  118. package/esm/vite/build-client.mjs.map +1 -1
  119. package/esm/vite/gate-a-resolve.mjs +66 -12
  120. package/esm/vite/gate-a-resolve.mjs.map +1 -1
  121. package/esm/vite/gate-b-secrets.mjs +34 -16
  122. package/esm/vite/gate-b-secrets.mjs.map +1 -1
  123. package/esm/vite/gate-c-verify.mjs +5 -5
  124. package/esm/vite/gate-c-verify.mjs.map +1 -1
  125. package/esm/vite/hydration-entries.mjs +1 -1
  126. package/esm/vite/hydration-entries.mjs.map +1 -1
  127. package/esm/vite/index.d.mts +8 -5
  128. package/esm/vite/index.mjs +123 -10
  129. package/esm/vite/index.mjs.map +1 -1
  130. package/esm/vite/page-registry-plugin.d.mts +21 -2
  131. package/esm/vite/page-registry-plugin.mjs +47 -13
  132. package/esm/vite/page-registry-plugin.mjs.map +1 -1
  133. package/esm/vite/projection.d.mts +1 -1
  134. package/esm/vite/projection.mjs +37 -14
  135. package/esm/vite/projection.mjs.map +1 -1
  136. package/llms-full.txt +220 -56
  137. package/llms.txt +5 -3
  138. package/package.json +3 -3
  139. package/skills/add-web-to-an-app/SKILL.md +3 -3
  140. package/skills/create-a-page/SKILL.md +117 -10
  141. package/skills/load-page-data/SKILL.md +92 -12
  142. package/skills/navigate-on-the-client/SKILL.md +1 -1
  143. package/skills/serve-styles/SKILL.md +13 -12
  144. package/skills/use-layouts/SKILL.md +14 -8
  145. package/skills/write-the-root/SKILL.md +3 -1
  146. package/esm/server/buffered-response.d.mts +0 -58
  147. package/esm/server/buffered-response.mjs +0 -115
  148. 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":["../../../../../../../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"}
@@ -6,8 +6,7 @@ import { Connector } from "@warlock.js/core";
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,7 +1,7 @@
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
6
  //#region ../web/src/server/web-connector-factory.ts
7
7
  /**
@@ -74,11 +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
- webRoot: buildOptions?.webRoot ?? webRoot,
80
- connectorPluginCount: connectorOptions.plugins?.length ?? 0
81
- });
80
+ webRoot: buildOptions?.webRoot ?? webRoot
81
+ }, connectorPlugins);
82
82
  let instance;
83
83
  /**
84
84
  * Load the heavy half on first lifecycle call. `boot()` is always the first
@@ -90,6 +90,7 @@ function webConnector(options = {}) {
90
90
  const { WebConnector: WebConnectorClass } = await import("./web-connector.mjs");
91
91
  instance = new WebConnectorClass({
92
92
  ...connectorOptions,
93
+ plugins: connectorPlugins,
93
94
  webRoot
94
95
  });
95
96
  }
@@ -113,7 +114,7 @@ function webConnector(options = {}) {
113
114
  shutdown: async () => {
114
115
  await instance?.shutdown();
115
116
  },
116
- shouldRestart: () => false
117
+ shouldRestart: (changedFiles) => instance?.shouldRestart(changedFiles) ?? false
117
118
  };
118
119
  }
119
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 // The COUNT, never the array. `connectorOptions.plugins` are dev-server\n // plugins and the production build cannot apply them; the build contribution\n // refuses the build rather than let them vanish silently\n // (`ConnectorPluginsNotSupportedError`). Passing a number keeps this options\n // object JSON-serializable — handing the plugin instances over would pull\n // Vite into every config load, which is the one thing this module exists to\n // prevent — and the refusal needs nothing more than \"how many\".\n connectorPluginCount: connectorOptions.plugins?.length ?? 0,\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;EAQlC,sBAAsB,iBAAiB,SAAS,UAAU;CAC5D,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":["../../../../../../../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"}
@@ -4,14 +4,19 @@ 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
21
  //#region ../web/src/server/web-connector.ts
17
22
  /**
@@ -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.
355
410
  */
356
- shouldRestart() {
357
- return false;
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.
469
+ */
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
  ],