@warlock.js/web 5.1.0 → 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 +174 -0
  2. package/README.md +126 -12
  3. package/esm/build/contribution.d.mts +11 -14
  4. package/esm/build/contribution.mjs +46 -58
  5. package/esm/build/contribution.mjs.map +1 -1
  6. package/esm/build/discover-pages.mjs +105 -45
  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 +1 -1
  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 +1 -1
  37. package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -1
  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 +1 -1
  73. package/esm/metadata.mjs +1 -1
  74. package/esm/metadata.mjs.map +1 -1
  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 +1 -1
  100. package/esm/routing/url-fragment.mjs.map +1 -1
  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 +31 -6
  113. package/esm/server/create-page-route-handler.mjs +128 -43
  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 +5 -5
  128. package/esm/server/index.mjs +3 -4
  129. package/esm/server/install-page-routes-from-manifest.d.mts +17 -7
  130. package/esm/server/install-page-routes-from-manifest.mjs +71 -10
  131. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  132. package/esm/server/install-page-routes.d.mts +32 -15
  133. package/esm/server/install-page-routes.mjs +91 -39
  134. package/esm/server/install-page-routes.mjs.map +1 -1
  135. package/esm/server/install-production-page-routes.mjs +3 -36
  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 +1 -1
  140. package/esm/server/not-found-page.mjs +2 -2
  141. package/esm/server/not-found-page.mjs.map +1 -1
  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 +7 -6
  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 +138 -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 +2 -2
  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 +48 -14
  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 +220 -56
  208. package/llms.txt +5 -3
  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 +117 -10
  212. package/skills/load-page-data/SKILL.md +92 -12
  213. package/skills/navigate-on-the-client/SKILL.md +1 -1
  214. package/skills/serve-styles/SKILL.md +13 -12
  215. package/skills/use-layouts/SKILL.md +14 -8
  216. package/skills/write-the-root/SKILL.md +3 -1
  217. package/esm/server/buffered-response.d.mts +0 -58
  218. package/esm/server/buffered-response.mjs +0 -115
  219. package/esm/server/buffered-response.mjs.map +0 -1
@@ -1,10 +1,10 @@
1
1
  import { CLIENT_ASSET_URL_PREFIX } from "./client-asset-url-prefix.mjs";
2
- import path from "node:path";
3
2
  import { readFileSync } from "node:fs";
3
+ import path from "node:path";
4
4
 
5
- //#region ../web/src/server/stylesheet-urls.ts
5
+ //#region web/src/server/stylesheet-urls.ts
6
6
  /**
7
- * Which stylesheets a document must link, in each of the two modes.
7
+ * Which stylesheets a REGISTERED HANDLER must link, in each of the two modes.
8
8
  *
9
9
  * WHY THIS EXISTS AT ALL. Nothing used to put CSS into the server-rendered
10
10
  * document. A stylesheet reached the browser only because the CLIENT bundle
@@ -13,13 +13,22 @@ import { readFileSync } from "node:fs";
13
13
  * moment later. The markup was correct the whole time, which is precisely why
14
14
  * it was easy to miss.
15
15
  *
16
+ * THE SCOPE IS PER HANDLER, NOT PER APPLICATION. Every page renders inside
17
+ * `[root, ...outer-to-inner matched layouts, page]` — that is the exact triple
18
+ * (widened to a chain) `create-page-route-handler.ts` loads per request — so a
19
+ * handler's CSS is the ordered, deduped union of what those specific source
20
+ * files pull in, and nothing else. Collecting across the WHOLE application
21
+ * (every page's manifest entry, every root import) is the bug this shape
22
+ * exists to avoid: it ships page B's stylesheet on page A's response, and it
23
+ * only gets worse as an application grows.
24
+ *
16
25
  * The two modes learn the answer from different places, and neither can use
17
26
  * the other's:
18
27
  *
19
- * - PRODUCTION reads Vite's client manifest, the same artifact the hydration
20
- * entry is already resolved from.
28
+ * - PRODUCTION reads Vite's client manifest, matching each source file's own
29
+ * id and walking its recorded `css` and imported chunks.
21
30
  * - DEV has no manifest — Vite serves modules on demand — so the URLs are
22
- * derived from the source files themselves.
31
+ * derived from each source file's own import statements.
23
32
  */
24
33
  /** Stylesheet extensions Vite can serve directly. Mirrors the build's list. */
25
34
  const STYLE_EXTENSIONS = [
@@ -30,39 +39,53 @@ const STYLE_EXTENSIONS = [
30
39
  ".styl"
31
40
  ];
32
41
  /**
33
- * The stylesheets the ROOT document imports, as dev URLs.
34
- *
35
- * Dev has no manifest, so the source is the root file itself: whatever
36
- * `root.tsx` imports with a stylesheet extension is what the document needs.
37
- * That is deliberately narrow it answers "what CSS does this application
38
- * set up globally", which is where `app.css` lives and where Tailwind is
39
- * wired, and it does NOT try to reproduce Vite's per-route CSS graph.
40
- *
41
- * The narrowness is the honest part: production splits CSS per chunk, dev
42
- * links the root's stylesheets on every page. A page whose own module imports
43
- * its own stylesheet still gets it in dev — Vite's client graph injects it as
44
- * before — it simply is not render-blocking the way the root's is. That is a
45
- * smaller gap than the flash this removes, and it is stated rather than
46
- * hidden.
47
- *
48
- * Specifiers are resolved against the root file and expressed relative to the
49
- * app root, because that is the shape Vite's dev server serves from.
42
+ * A stylesheet Vite serves in DEV must be requested with `?direct`.
43
+ *
44
+ * Without it Vite answers the same URL with `text/javascript` its CSS-as-JS
45
+ * module transform, meant for `import "./app.css"` and a
46
+ * `<link rel="stylesheet">` pointing at a JavaScript response applies
47
+ * NOTHING, silently. No console error, no network failure, just an unstyled
48
+ * page. `?direct` is what makes Vite reply with real `text/css`.
50
49
  */
51
- function devStylesheetUrls(appRoot, appFile) {
50
+ const VITE_DIRECT_CSS_QUERY = "?direct";
51
+ /**
52
+ * ONE source file's own directly imported stylesheets, as dev URLs.
53
+ *
54
+ * Dev has no manifest, so the source is the file itself: whatever it imports
55
+ * with a stylesheet extension is what it needs. This is deliberately narrow —
56
+ * it answers "what CSS does THIS file set up", not "what CSS does the whole
57
+ * module graph this file eventually reaches end up pulling in" — because the
58
+ * latter would require reproducing Vite's module graph, which is precisely
59
+ * what dev has no manifest to shortcut.
60
+ *
61
+ * The narrowness is the honest part: production splits CSS per chunk and can
62
+ * follow imported chunks (`productionStylesheetUrls` below); dev can only read
63
+ * the ONE file handed to it. A page whose own module imports its own
64
+ * stylesheet still gets it in dev — Vite's client graph injects it as before —
65
+ * it simply is not render-blocking the way a chain member's own import is.
66
+ *
67
+ * Called once per chain member — root, then every matched layout outer to
68
+ * inner, then the page — by the installers below, which is what turns "one
69
+ * file's own imports" into a handler's whole CSS chain.
70
+ *
71
+ * Specifiers are resolved against `sourceFile` and expressed relative to
72
+ * `appRoot`, because that is the shape Vite's dev server serves from.
73
+ */
74
+ function devStylesheetUrls(appRoot, sourceFile) {
52
75
  let source;
53
76
  try {
54
- source = readFileSync(appFile, "utf-8");
77
+ source = readFileSync(sourceFile, "utf-8");
55
78
  } catch {
56
79
  return [];
57
80
  }
58
81
  const urls = [];
59
- const pattern = /\bimport\s*["']([^"']+)["']/g;
82
+ const pattern = /\bimport\s*(?:\(\s*)?["']([^"']+)["']/g;
60
83
  let match = pattern.exec(source);
61
84
  while (match !== null) {
62
85
  const specifier = match[1];
63
86
  const lowered = specifier.toLowerCase();
64
87
  if (STYLE_EXTENSIONS.some((extension) => lowered.endsWith(extension))) {
65
- const absolute = path.resolve(path.dirname(appFile), specifier);
88
+ const absolute = path.resolve(path.dirname(sourceFile), specifier);
66
89
  const relative = path.relative(appRoot, absolute).split(path.sep).join("/");
67
90
  if (!relative.startsWith("..")) {
68
91
  const url = `/${relative}${VITE_DIRECT_CSS_QUERY}`;
@@ -74,33 +97,93 @@ function devStylesheetUrls(appRoot, appFile) {
74
97
  return urls;
75
98
  }
76
99
  /**
77
- * A stylesheet Vite serves in DEV must be requested with `?direct`.
100
+ * ONE handler's whole dev CSS chain: every `sourceFiles` member's own direct
101
+ * stylesheet imports (`devStylesheetUrls`), in the order given — the caller
102
+ * passes `[root, ...outer-to-inner matched layouts, page]` — concatenated and
103
+ * deduped across the WHOLE chain, not just within one file.
78
104
  *
79
- * Without it Vite answers the same URL with `text/javascript` — its CSS-as-JS
80
- * module transform, meant for `import "./app.css"` and a
81
- * `<link rel="stylesheet">` pointing at a JavaScript response applies
82
- * NOTHING, silently. No console error, no network failure, just an unstyled
83
- * page. `?direct` is what makes Vite reply with real `text/css`.
105
+ * Cross-file dedup matters as much as within-file dedup: an application-wide
106
+ * `app.css` imported by both the root and a page must still produce one
107
+ * `<link>`, not two.
84
108
  */
85
- const VITE_DIRECT_CSS_QUERY = "?direct";
109
+ function devHandlerStylesheetUrls(appRoot, sourceFiles) {
110
+ const urls = [];
111
+ for (const sourceFile of sourceFiles) for (const url of devStylesheetUrls(appRoot, sourceFile)) if (!urls.includes(url)) urls.push(url);
112
+ return urls;
113
+ }
86
114
  /**
87
- * Every stylesheet the client build emitted, as URLs the asset route serves.
115
+ * Find the manifest key for an app-root-relative POSIX source id.
88
116
  *
89
- * Vite records CSS against the CHUNK that imported it an app whose
90
- * `root.tsx` imports `app.css` produces a `root.tsx` entry carrying
91
- * `css: ["assets/root-<hash>.css"]`, not a hydration entry carrying it. So
92
- * this collects across every entry rather than looking under one name, which
93
- * would silently find nothing the moment a stylesheet moved file.
117
+ * VITE KEYS BY SOURCE PATH RELATIVE TO ITS OWN `root`, not to the app's
118
+ * `appRoot` the client build's `root` is the framework's own package
119
+ * (`build-client.ts`), so a key for an app source file carries a `../`-laden
120
+ * prefix (`"../my-app/src/web/root.tsx"`) rather than matching `sourceFile`
121
+ * (`"src/web/root.tsx"`) byte for byte. The two forms always share the same
122
+ * TAIL, though — both are anchored at the same file — so an exact match is
123
+ * tried first (the case where the client build's root IS the app root, which
124
+ * every fixture and every test below uses) and a `/`-boundary suffix match
125
+ * second, rather than trying to reconstruct the build's own root here, which
126
+ * this runtime read has no way to independently confirm.
127
+ */
128
+ function findManifestKey(manifest, sourceFile) {
129
+ if (manifest[sourceFile] !== void 0) return sourceFile;
130
+ const suffix = `/${sourceFile}`;
131
+ for (const key of Object.keys(manifest)) if (key.endsWith(suffix)) return key;
132
+ }
133
+ /**
134
+ * Every stylesheet reachable from ONE manifest entry: its own recorded `css`,
135
+ * plus the same walk repeated over every chunk it STATICALLY `imports`.
136
+ *
137
+ * `imports` only, never `dynamicImports`. Vite's manifest records
138
+ * `dynamicImports` on shared entry points (the hydration entry names every
139
+ * page as one) precisely because the browser must NOT download them eagerly —
140
+ * walking that array here would pull every other page's CSS onto this one,
141
+ * which is the exact "unrelated pages" leak this module exists to end.
142
+ * `imports`, by contrast, are chunks THIS module synchronously depends on:
143
+ * code Vite split out of it but that loads whenever it does, so their CSS is
144
+ * this handler's CSS too.
94
145
  *
95
- * Duplicates are collapsed and order is preserved: two chunks importing the
96
- * same stylesheet must not emit two `<link>` tags.
146
+ * `visited` guards against a chunk graph cycle; sharing one set across the
147
+ * whole walk from a single entry is enough; a shared chunk revisited from a
148
+ * SEPARATE top-level entry (root vs. a layout vs. the page) is deliberately
149
+ * walked again — the final merge in `productionStylesheetUrls` dedupes by URL,
150
+ * and a fresh `visited` set per entry is simpler to reason about than one
151
+ * threaded across unrelated chains.
152
+ */
153
+ function collectManifestCss(manifest, key, visited) {
154
+ if (visited.has(key)) return [];
155
+ visited.add(key);
156
+ const entry = manifest[key];
157
+ if (entry === void 0) return [];
158
+ const ownCss = Array.isArray(entry.css) ? entry.css.filter((file) => typeof file === "string" && file !== "") : [];
159
+ const imports = Array.isArray(entry.imports) ? entry.imports.filter((id) => typeof id === "string") : [];
160
+ return [...ownCss, ...imports.flatMap((importedKey) => collectManifestCss(manifest, importedKey, visited))];
161
+ }
162
+ /**
163
+ * ONE handler's whole production CSS chain.
164
+ *
165
+ * `sourceFiles` is `[root, ...outer-to-inner matched layouts, page]`, each an
166
+ * app-root-relative POSIX source id — the SAME id
167
+ * `install-page-routes-from-manifest.ts` already carries as `sourceFile` on
168
+ * every manifest entry, because that identity is what lets this function match
169
+ * EXPLICITLY rather than guess: every id is looked up on its own
170
+ * (`findManifestKey`), its own chunk's CSS is collected
171
+ * (`collectManifestCss`), and an id with no matching entry contributes
172
+ * nothing — it is never treated as license to fall back to scanning the whole
173
+ * manifest, which is what let an unrelated page's CSS leak onto this handler
174
+ * before.
175
+ *
176
+ * Duplicates are collapsed and order is preserved across the WHOLE chain, in
177
+ * the order `sourceFiles` was given — root's own CSS first, then each
178
+ * layout's outer to inner, then the page's — so cascade order matches the
179
+ * chain's own outer-to-inner rendering order.
97
180
  *
98
181
  * A missing or malformed manifest returns NOTHING rather than throwing. The
99
182
  * hydration resolver already fails loudly on exactly those conditions, from
100
183
  * exactly the same file, and it runs first — a second, worse error for the
101
184
  * same cause helps nobody.
102
185
  */
103
- function productionStylesheetUrls(clientDir) {
186
+ function productionStylesheetUrls(clientDir, sourceFiles) {
104
187
  const manifestPath = path.join(clientDir, ".vite", "manifest.json");
105
188
  let manifest;
106
189
  try {
@@ -109,19 +192,21 @@ function productionStylesheetUrls(clientDir) {
109
192
  return [];
110
193
  }
111
194
  if (typeof manifest !== "object" || manifest === null) return [];
195
+ const files = [];
196
+ for (const sourceFile of sourceFiles) {
197
+ const key = findManifestKey(manifest, sourceFile);
198
+ if (key === void 0) continue;
199
+ files.push(...collectManifestCss(manifest, key, /* @__PURE__ */ new Set()));
200
+ }
112
201
  const urls = [];
113
- for (const entry of Object.values(manifest)) {
114
- if (entry === void 0 || !Array.isArray(entry.css)) continue;
115
- for (const file of entry.css) {
116
- if (typeof file !== "string" || file === "") continue;
117
- const url = `/${file}`;
118
- if (!url.startsWith(`${"/assets"}/`)) continue;
119
- if (!urls.includes(url)) urls.push(url);
120
- }
202
+ for (const file of files) {
203
+ const url = `/${file}`;
204
+ if (!url.startsWith(`${"/assets"}/`)) continue;
205
+ if (!urls.includes(url)) urls.push(url);
121
206
  }
122
207
  return urls;
123
208
  }
124
209
 
125
210
  //#endregion
126
- export { VITE_DIRECT_CSS_QUERY, devStylesheetUrls, productionStylesheetUrls };
211
+ export { VITE_DIRECT_CSS_QUERY, devHandlerStylesheetUrls, devStylesheetUrls, productionStylesheetUrls };
127
212
  //# sourceMappingURL=stylesheet-urls.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"stylesheet-urls.mjs","names":[],"sources":["../../../../../../../web/src/server/stylesheet-urls.ts"],"sourcesContent":["/**\n * Which stylesheets a document must link, in each of the two modes.\n *\n * WHY THIS EXISTS AT ALL. Nothing used to put CSS into the server-rendered\n * document. A stylesheet reached the browser only because the CLIENT bundle\n * imported it, which means JavaScript applied it after the module graph\n * loaded — so every full page load painted unstyled first and restyled a\n * moment later. The markup was correct the whole time, which is precisely why\n * it was easy to miss.\n *\n * The two modes learn the answer from different places, and neither can use\n * the other's:\n *\n * - PRODUCTION reads Vite's client manifest, the same artifact the hydration\n * entry is already resolved from.\n * - DEV has no manifest — Vite serves modules on demand — so the URLs are\n * derived from the source files themselves.\n */\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { CLIENT_ASSET_URL_PREFIX } from \"./client-asset-url-prefix\";\n\n/** Stylesheet extensions Vite can serve directly. Mirrors the build's list. */\nconst STYLE_EXTENSIONS = [\".css\", \".scss\", \".sass\", \".less\", \".styl\"];\n\n/**\n * The stylesheets the ROOT document imports, as dev URLs.\n *\n * Dev has no manifest, so the source is the root file itself: whatever\n * `root.tsx` imports with a stylesheet extension is what the document needs.\n * That is deliberately narrow — it answers \"what CSS does this application\n * set up globally\", which is where `app.css` lives and where Tailwind is\n * wired, and it does NOT try to reproduce Vite's per-route CSS graph.\n *\n * The narrowness is the honest part: production splits CSS per chunk, dev\n * links the root's stylesheets on every page. A page whose own module imports\n * its own stylesheet still gets it in dev — Vite's client graph injects it as\n * before — it simply is not render-blocking the way the root's is. That is a\n * smaller gap than the flash this removes, and it is stated rather than\n * hidden.\n *\n * Specifiers are resolved against the root file and expressed relative to the\n * app root, because that is the shape Vite's dev server serves from.\n */\nexport function devStylesheetUrls(appRoot: string, appFile: string): string[] {\n let source: string;\n\n try {\n source = readFileSync(appFile, \"utf-8\");\n } catch {\n return [];\n }\n\n const urls: string[] = [];\n const pattern = /\\bimport\\s*[\"']([^\"']+)[\"']/g;\n\n let match = pattern.exec(source);\n\n while (match !== null) {\n const specifier = match[1];\n const lowered = specifier.toLowerCase();\n\n if (STYLE_EXTENSIONS.some((extension) => lowered.endsWith(extension))) {\n const absolute = path.resolve(path.dirname(appFile), specifier);\n const relative = path.relative(appRoot, absolute).split(path.sep).join(\"/\");\n\n // Outside the app root Vite would need an `/@fs/` URL and a widened\n // `fs.allow`; a stylesheet living there is unusual enough that guessing\n // is worse than leaving it to the client import.\n if (!relative.startsWith(\"..\")) {\n const url = `/${relative}${VITE_DIRECT_CSS_QUERY}`;\n\n if (!urls.includes(url)) urls.push(url);\n }\n }\n\n match = pattern.exec(source);\n }\n\n return urls;\n}\n\n/**\n * A stylesheet Vite serves in DEV must be requested with `?direct`.\n *\n * Without it Vite answers the same URL with `text/javascript` — its CSS-as-JS\n * module transform, meant for `import \"./app.css\"` — and a\n * `<link rel=\"stylesheet\">` pointing at a JavaScript response applies\n * NOTHING, silently. No console error, no network failure, just an unstyled\n * page. `?direct` is what makes Vite reply with real `text/css`.\n */\nexport const VITE_DIRECT_CSS_QUERY = \"?direct\";\n\ntype ManifestEntry = {\n css?: unknown;\n file?: unknown;\n};\n\n/**\n * Every stylesheet the client build emitted, as URLs the asset route serves.\n *\n * Vite records CSS against the CHUNK that imported it — an app whose\n * `root.tsx` imports `app.css` produces a `root.tsx` entry carrying\n * `css: [\"assets/root-<hash>.css\"]`, not a hydration entry carrying it. So\n * this collects across every entry rather than looking under one name, which\n * would silently find nothing the moment a stylesheet moved file.\n *\n * Duplicates are collapsed and order is preserved: two chunks importing the\n * same stylesheet must not emit two `<link>` tags.\n *\n * A missing or malformed manifest returns NOTHING rather than throwing. The\n * hydration resolver already fails loudly on exactly those conditions, from\n * exactly the same file, and it runs first — a second, worse error for the\n * same cause helps nobody.\n */\nexport function productionStylesheetUrls(clientDir: string): string[] {\n const manifestPath = path.join(clientDir, \".vite\", \"manifest.json\");\n\n let manifest: Record<string, ManifestEntry | undefined>;\n\n try {\n manifest = JSON.parse(readFileSync(manifestPath, \"utf-8\")) as Record<\n string,\n ManifestEntry | undefined\n >;\n } catch {\n return [];\n }\n\n if (typeof manifest !== \"object\" || manifest === null) return [];\n\n const urls: string[] = [];\n\n for (const entry of Object.values(manifest)) {\n if (entry === undefined || !Array.isArray(entry.css)) continue;\n\n for (const file of entry.css) {\n if (typeof file !== \"string\" || file === \"\") continue;\n\n // Built EXACTLY as the hydration entry's URL is built — `/${file}`, then\n // checked against the prefix — rather than reassembled from a basename.\n // The manifest already records `assets/root-<hash>.css`, and rebuilding\n // that path here would be a second expression of a convention\n // `client-asset-url-prefix.ts` owns.\n const url = `/${file}`;\n\n // A stylesheet outside the directory the asset route mounts would 404.\n // Dropped rather than emitted, because a dead <link> in <head> is a\n // silent styling failure — the exact thing this module exists to end.\n if (!url.startsWith(`${CLIENT_ASSET_URL_PREFIX}/`)) continue;\n\n if (!urls.includes(url)) urls.push(url);\n }\n }\n\n return urls;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAM,mBAAmB;CAAC;CAAQ;CAAS;CAAS;CAAS;AAAO;;;;;;;;;;;;;;;;;;;;AAqBpE,SAAgB,kBAAkB,SAAiB,SAA2B;CAC5E,IAAI;CAEJ,IAAI;EACF,SAAS,aAAa,SAAS,OAAO;CACxC,QAAQ;EACN,OAAO,CAAC;CACV;CAEA,MAAM,OAAiB,CAAC;CACxB,MAAM,UAAU;CAEhB,IAAI,QAAQ,QAAQ,KAAK,MAAM;CAE/B,OAAO,UAAU,MAAM;EACrB,MAAM,YAAY,MAAM;EACxB,MAAM,UAAU,UAAU,YAAY;EAEtC,IAAI,iBAAiB,MAAM,cAAc,QAAQ,SAAS,SAAS,CAAC,GAAG;GACrE,MAAM,WAAW,KAAK,QAAQ,KAAK,QAAQ,OAAO,GAAG,SAAS;GAC9D,MAAM,WAAW,KAAK,SAAS,SAAS,QAAQ,CAAC,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG;GAK1E,IAAI,CAAC,SAAS,WAAW,IAAI,GAAG;IAC9B,MAAM,MAAM,IAAI,WAAW;IAE3B,IAAI,CAAC,KAAK,SAAS,GAAG,GAAG,KAAK,KAAK,GAAG;GACxC;EACF;EAEA,QAAQ,QAAQ,KAAK,MAAM;CAC7B;CAEA,OAAO;AACT;;;;;;;;;;AAWA,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;AAwBrC,SAAgB,yBAAyB,WAA6B;CACpE,MAAM,eAAe,KAAK,KAAK,WAAW,SAAS,eAAe;CAElE,IAAI;CAEJ,IAAI;EACF,WAAW,KAAK,MAAM,aAAa,cAAc,OAAO,CAAC;CAI3D,QAAQ;EACN,OAAO,CAAC;CACV;CAEA,IAAI,OAAO,aAAa,YAAY,aAAa,MAAM,OAAO,CAAC;CAE/D,MAAM,OAAiB,CAAC;CAExB,KAAK,MAAM,SAAS,OAAO,OAAO,QAAQ,GAAG;EAC3C,IAAI,UAAU,UAAa,CAAC,MAAM,QAAQ,MAAM,GAAG,GAAG;EAEtD,KAAK,MAAM,QAAQ,MAAM,KAAK;GAC5B,IAAI,OAAO,SAAS,YAAY,SAAS,IAAI;GAO7C,MAAM,MAAM,IAAI;GAKhB,IAAI,CAAC,IAAI,WAAW,aAA2B,EAAE,GAAG;GAEpD,IAAI,CAAC,KAAK,SAAS,GAAG,GAAG,KAAK,KAAK,GAAG;EACxC;CACF;CAEA,OAAO;AACT"}
1
+ {"version":3,"file":"stylesheet-urls.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/stylesheet-urls.ts"],"sourcesContent":["/**\n * Which stylesheets a REGISTERED HANDLER must link, in each of the two modes.\n *\n * WHY THIS EXISTS AT ALL. Nothing used to put CSS into the server-rendered\n * document. A stylesheet reached the browser only because the CLIENT bundle\n * imported it, which means JavaScript applied it after the module graph\n * loaded — so every full page load painted unstyled first and restyled a\n * moment later. The markup was correct the whole time, which is precisely why\n * it was easy to miss.\n *\n * THE SCOPE IS PER HANDLER, NOT PER APPLICATION. Every page renders inside\n * `[root, ...outer-to-inner matched layouts, page]` — that is the exact triple\n * (widened to a chain) `create-page-route-handler.ts` loads per request — so a\n * handler's CSS is the ordered, deduped union of what those specific source\n * files pull in, and nothing else. Collecting across the WHOLE application\n * (every page's manifest entry, every root import) is the bug this shape\n * exists to avoid: it ships page B's stylesheet on page A's response, and it\n * only gets worse as an application grows.\n *\n * The two modes learn the answer from different places, and neither can use\n * the other's:\n *\n * - PRODUCTION reads Vite's client manifest, matching each source file's own\n * id and walking its recorded `css` and imported chunks.\n * - DEV has no manifest — Vite serves modules on demand — so the URLs are\n * derived from each source file's own import statements.\n */\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { CLIENT_ASSET_URL_PREFIX } from \"./client-asset-url-prefix\";\n\n/** Stylesheet extensions Vite can serve directly. Mirrors the build's list. */\nconst STYLE_EXTENSIONS = [\".css\", \".scss\", \".sass\", \".less\", \".styl\"];\n\n/**\n * A stylesheet Vite serves in DEV must be requested with `?direct`.\n *\n * Without it Vite answers the same URL with `text/javascript` — its CSS-as-JS\n * module transform, meant for `import \"./app.css\"` — and a\n * `<link rel=\"stylesheet\">` pointing at a JavaScript response applies\n * NOTHING, silently. No console error, no network failure, just an unstyled\n * page. `?direct` is what makes Vite reply with real `text/css`.\n */\nexport const VITE_DIRECT_CSS_QUERY = \"?direct\";\n\n/**\n * ONE source file's own directly imported stylesheets, as dev URLs.\n *\n * Dev has no manifest, so the source is the file itself: whatever it imports\n * with a stylesheet extension is what it needs. This is deliberately narrow —\n * it answers \"what CSS does THIS file set up\", not \"what CSS does the whole\n * module graph this file eventually reaches end up pulling in\" — because the\n * latter would require reproducing Vite's module graph, which is precisely\n * what dev has no manifest to shortcut.\n *\n * The narrowness is the honest part: production splits CSS per chunk and can\n * follow imported chunks (`productionStylesheetUrls` below); dev can only read\n * the ONE file handed to it. A page whose own module imports its own\n * stylesheet still gets it in dev — Vite's client graph injects it as before —\n * it simply is not render-blocking the way a chain member's own import is.\n *\n * Called once per chain member — root, then every matched layout outer to\n * inner, then the page — by the installers below, which is what turns \"one\n * file's own imports\" into a handler's whole CSS chain.\n *\n * Specifiers are resolved against `sourceFile` and expressed relative to\n * `appRoot`, because that is the shape Vite's dev server serves from.\n */\nexport function devStylesheetUrls(appRoot: string, sourceFile: string): string[] {\n let source: string;\n\n try {\n source = readFileSync(sourceFile, \"utf-8\");\n } catch {\n return [];\n }\n\n const urls: string[] = [];\n const pattern = /\\bimport\\s*(?:\\(\\s*)?[\"']([^\"']+)[\"']/g;\n\n let match = pattern.exec(source);\n\n while (match !== null) {\n const specifier = match[1];\n const lowered = specifier.toLowerCase();\n\n if (STYLE_EXTENSIONS.some((extension) => lowered.endsWith(extension))) {\n const absolute = path.resolve(path.dirname(sourceFile), specifier);\n const relative = path.relative(appRoot, absolute).split(path.sep).join(\"/\");\n\n // Outside the app root Vite would need an `/@fs/` URL and a widened\n // `fs.allow`; a stylesheet living there is unusual enough that guessing\n // is worse than leaving it to the client import.\n if (!relative.startsWith(\"..\")) {\n const url = `/${relative}${VITE_DIRECT_CSS_QUERY}`;\n\n if (!urls.includes(url)) urls.push(url);\n }\n }\n\n match = pattern.exec(source);\n }\n\n return urls;\n}\n\n/**\n * ONE handler's whole dev CSS chain: every `sourceFiles` member's own direct\n * stylesheet imports (`devStylesheetUrls`), in the order given — the caller\n * passes `[root, ...outer-to-inner matched layouts, page]` — concatenated and\n * deduped across the WHOLE chain, not just within one file.\n *\n * Cross-file dedup matters as much as within-file dedup: an application-wide\n * `app.css` imported by both the root and a page must still produce one\n * `<link>`, not two.\n */\nexport function devHandlerStylesheetUrls(\n appRoot: string,\n sourceFiles: readonly string[],\n): string[] {\n const urls: string[] = [];\n\n for (const sourceFile of sourceFiles) {\n for (const url of devStylesheetUrls(appRoot, sourceFile)) {\n if (!urls.includes(url)) urls.push(url);\n }\n }\n\n return urls;\n}\n\ntype ManifestEntry = {\n css?: unknown;\n imports?: unknown;\n};\n\n/**\n * Find the manifest key for an app-root-relative POSIX source id.\n *\n * VITE KEYS BY SOURCE PATH RELATIVE TO ITS OWN `root`, not to the app's\n * `appRoot` — the client build's `root` is the framework's own package\n * (`build-client.ts`), so a key for an app source file carries a `../`-laden\n * prefix (`\"../my-app/src/web/root.tsx\"`) rather than matching `sourceFile`\n * (`\"src/web/root.tsx\"`) byte for byte. The two forms always share the same\n * TAIL, though — both are anchored at the same file — so an exact match is\n * tried first (the case where the client build's root IS the app root, which\n * every fixture and every test below uses) and a `/`-boundary suffix match\n * second, rather than trying to reconstruct the build's own root here, which\n * this runtime read has no way to independently confirm.\n */\nfunction findManifestKey(\n manifest: Record<string, ManifestEntry | undefined>,\n sourceFile: string,\n): string | undefined {\n if (manifest[sourceFile] !== undefined) return sourceFile;\n\n const suffix = `/${sourceFile}`;\n\n for (const key of Object.keys(manifest)) {\n if (key.endsWith(suffix)) return key;\n }\n\n return undefined;\n}\n\n/**\n * Every stylesheet reachable from ONE manifest entry: its own recorded `css`,\n * plus the same walk repeated over every chunk it STATICALLY `imports`.\n *\n * `imports` only, never `dynamicImports`. Vite's manifest records\n * `dynamicImports` on shared entry points (the hydration entry names every\n * page as one) precisely because the browser must NOT download them eagerly —\n * walking that array here would pull every other page's CSS onto this one,\n * which is the exact \"unrelated pages\" leak this module exists to end.\n * `imports`, by contrast, are chunks THIS module synchronously depends on:\n * code Vite split out of it but that loads whenever it does, so their CSS is\n * this handler's CSS too.\n *\n * `visited` guards against a chunk graph cycle; sharing one set across the\n * whole walk from a single entry is enough; a shared chunk revisited from a\n * SEPARATE top-level entry (root vs. a layout vs. the page) is deliberately\n * walked again — the final merge in `productionStylesheetUrls` dedupes by URL,\n * and a fresh `visited` set per entry is simpler to reason about than one\n * threaded across unrelated chains.\n */\nfunction collectManifestCss(\n manifest: Record<string, ManifestEntry | undefined>,\n key: string,\n visited: Set<string>,\n): string[] {\n if (visited.has(key)) return [];\n visited.add(key);\n\n const entry = manifest[key];\n if (entry === undefined) return [];\n\n const ownCss = Array.isArray(entry.css)\n ? entry.css.filter((file): file is string => typeof file === \"string\" && file !== \"\")\n : [];\n\n const imports = Array.isArray(entry.imports)\n ? entry.imports.filter((id): id is string => typeof id === \"string\")\n : [];\n\n return [\n ...ownCss,\n ...imports.flatMap((importedKey) => collectManifestCss(manifest, importedKey, visited)),\n ];\n}\n\n/**\n * ONE handler's whole production CSS chain.\n *\n * `sourceFiles` is `[root, ...outer-to-inner matched layouts, page]`, each an\n * app-root-relative POSIX source id — the SAME id\n * `install-page-routes-from-manifest.ts` already carries as `sourceFile` on\n * every manifest entry, because that identity is what lets this function match\n * EXPLICITLY rather than guess: every id is looked up on its own\n * (`findManifestKey`), its own chunk's CSS is collected\n * (`collectManifestCss`), and an id with no matching entry contributes\n * nothing — it is never treated as license to fall back to scanning the whole\n * manifest, which is what let an unrelated page's CSS leak onto this handler\n * before.\n *\n * Duplicates are collapsed and order is preserved across the WHOLE chain, in\n * the order `sourceFiles` was given — root's own CSS first, then each\n * layout's outer to inner, then the page's — so cascade order matches the\n * chain's own outer-to-inner rendering order.\n *\n * A missing or malformed manifest returns NOTHING rather than throwing. The\n * hydration resolver already fails loudly on exactly those conditions, from\n * exactly the same file, and it runs first — a second, worse error for the\n * same cause helps nobody.\n */\nexport function productionStylesheetUrls(\n clientDir: string,\n sourceFiles: readonly string[],\n): string[] {\n const manifestPath = path.join(clientDir, \".vite\", \"manifest.json\");\n\n let manifest: Record<string, ManifestEntry | undefined>;\n\n try {\n manifest = JSON.parse(readFileSync(manifestPath, \"utf-8\")) as Record<\n string,\n ManifestEntry | undefined\n >;\n } catch {\n return [];\n }\n\n if (typeof manifest !== \"object\" || manifest === null) return [];\n\n const files: string[] = [];\n\n for (const sourceFile of sourceFiles) {\n const key = findManifestKey(manifest, sourceFile);\n if (key === undefined) continue;\n\n files.push(...collectManifestCss(manifest, key, new Set()));\n }\n\n const urls: string[] = [];\n\n for (const file of files) {\n // Built EXACTLY as the hydration entry's URL is built — `/${file}`, then\n // checked against the prefix — rather than reassembled from a basename.\n // The manifest already records `assets/root-<hash>.css`, and rebuilding\n // that path here would be a second expression of a convention\n // `client-asset-url-prefix.ts` owns.\n const url = `/${file}`;\n\n // A stylesheet outside the directory the asset route mounts would 404.\n // Dropped rather than emitted, because a dead <link> in <head> is a\n // silent styling failure — the exact thing this module exists to end.\n if (!url.startsWith(`${CLIENT_ASSET_URL_PREFIX}/`)) continue;\n\n if (!urls.includes(url)) urls.push(url);\n }\n\n return urls;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAM,mBAAmB;CAAC;CAAQ;CAAS;CAAS;CAAS;AAAO;;;;;;;;;;AAWpE,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;AAyBrC,SAAgB,kBAAkB,SAAiB,YAA8B;CAC/E,IAAI;CAEJ,IAAI;EACF,SAAS,aAAa,YAAY,OAAO;CAC3C,QAAQ;EACN,OAAO,CAAC;CACV;CAEA,MAAM,OAAiB,CAAC;CACxB,MAAM,UAAU;CAEhB,IAAI,QAAQ,QAAQ,KAAK,MAAM;CAE/B,OAAO,UAAU,MAAM;EACrB,MAAM,YAAY,MAAM;EACxB,MAAM,UAAU,UAAU,YAAY;EAEtC,IAAI,iBAAiB,MAAM,cAAc,QAAQ,SAAS,SAAS,CAAC,GAAG;GACrE,MAAM,WAAW,KAAK,QAAQ,KAAK,QAAQ,UAAU,GAAG,SAAS;GACjE,MAAM,WAAW,KAAK,SAAS,SAAS,QAAQ,EAAE,MAAM,KAAK,GAAG,EAAE,KAAK,GAAG;GAK1E,IAAI,CAAC,SAAS,WAAW,IAAI,GAAG;IAC9B,MAAM,MAAM,IAAI,WAAW;IAE3B,IAAI,CAAC,KAAK,SAAS,GAAG,GAAG,KAAK,KAAK,GAAG;GACxC;EACF;EAEA,QAAQ,QAAQ,KAAK,MAAM;CAC7B;CAEA,OAAO;AACT;;;;;;;;;;;AAYA,SAAgB,yBACd,SACA,aACU;CACV,MAAM,OAAiB,CAAC;CAExB,KAAK,MAAM,cAAc,aACvB,KAAK,MAAM,OAAO,kBAAkB,SAAS,UAAU,GACrD,IAAI,CAAC,KAAK,SAAS,GAAG,GAAG,KAAK,KAAK,GAAG;CAI1C,OAAO;AACT;;;;;;;;;;;;;;;AAqBA,SAAS,gBACP,UACA,YACoB;CACpB,IAAI,SAAS,gBAAgB,QAAW,OAAO;CAE/C,MAAM,SAAS,IAAI;CAEnB,KAAK,MAAM,OAAO,OAAO,KAAK,QAAQ,GACpC,IAAI,IAAI,SAAS,MAAM,GAAG,OAAO;AAIrC;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAS,mBACP,UACA,KACA,SACU;CACV,IAAI,QAAQ,IAAI,GAAG,GAAG,OAAO,CAAC;CAC9B,QAAQ,IAAI,GAAG;CAEf,MAAM,QAAQ,SAAS;CACvB,IAAI,UAAU,QAAW,OAAO,CAAC;CAEjC,MAAM,SAAS,MAAM,QAAQ,MAAM,GAAG,IAClC,MAAM,IAAI,QAAQ,SAAyB,OAAO,SAAS,YAAY,SAAS,EAAE,IAClF,CAAC;CAEL,MAAM,UAAU,MAAM,QAAQ,MAAM,OAAO,IACvC,MAAM,QAAQ,QAAQ,OAAqB,OAAO,OAAO,QAAQ,IACjE,CAAC;CAEL,OAAO,CACL,GAAG,QACH,GAAG,QAAQ,SAAS,gBAAgB,mBAAmB,UAAU,aAAa,OAAO,CAAC,CACxF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,yBACd,WACA,aACU;CACV,MAAM,eAAe,KAAK,KAAK,WAAW,SAAS,eAAe;CAElE,IAAI;CAEJ,IAAI;EACF,WAAW,KAAK,MAAM,aAAa,cAAc,OAAO,CAAC;CAI3D,QAAQ;EACN,OAAO,CAAC;CACV;CAEA,IAAI,OAAO,aAAa,YAAY,aAAa,MAAM,OAAO,CAAC;CAE/D,MAAM,QAAkB,CAAC;CAEzB,KAAK,MAAM,cAAc,aAAa;EACpC,MAAM,MAAM,gBAAgB,UAAU,UAAU;EAChD,IAAI,QAAQ,QAAW;EAEvB,MAAM,KAAK,GAAG,mBAAmB,UAAU,qBAAK,IAAI,IAAI,CAAC,CAAC;CAC5D;CAEA,MAAM,OAAiB,CAAC;CAExB,KAAK,MAAM,QAAQ,OAAO;EAMxB,MAAM,MAAM,IAAI;EAKhB,IAAI,CAAC,IAAI,WAAW,aAA2B,EAAE,GAAG;EAEpD,IAAI,CAAC,KAAK,SAAS,GAAG,GAAG,KAAK,KAAK,GAAG;CACxC;CAEA,OAAO;AACT"}
@@ -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,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":["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`