@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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,180 @@
2
2
 
3
3
  All notable changes to `@warlock.js/web` are documented here.
4
4
 
5
+ ## 5.2.0
6
+
7
+ ### Added
8
+
9
+ - **`error.page.tsx`** — the application's one error boundary. It renders when
10
+ a middleware, loader, or component throws; declares no `route`, exactly like
11
+ `404.page.tsx`; and a second `error.page.tsx` anywhere beneath `src/web` is a
12
+ build error. Its component receives `{ error, status }` — the real thrown
13
+ value during SSR, a JSON-safe `{ name, message, stack? }` after hydration.
14
+ `robots: noindex` is a framework default on this path and cannot be
15
+ overridden away. If the failure happens before any page module could load —
16
+ a module-load or `register()` throw — the response falls back further, to a
17
+ framework-owned boundary with no application code at all, and is served
18
+ without a hydration script rather than risk hydrating against markup nothing
19
+ can vouch for.
20
+ - **A page's `route` export is now optional.** A `*.page.tsx` with no `route`
21
+ derives its path and its name from its location beneath `src/web`:
22
+ directories contribute segments, `(group)` directories contribute nothing,
23
+ `index.page.tsx` claims its own directory, and `[id]` becomes `:id`. An
24
+ explicit `route` still always wins over the derived one. This replaces the
25
+ 5.1 behaviour, where an omitted `route` threw `MissingRouteExportError` at
26
+ install time — that error class no longer exists.
27
+ - **Live page-route re-registration in `warlock dev`.** Creating, deleting, or
28
+ editing a page's `route` export used to require a manual restart to take
29
+ effect — the route table was built once at boot and never again, so a
30
+ renamed route kept serving its old path and a deleted page kept 404-ing at
31
+ its old URL forever. The dev connector now re-registers the affected routes
32
+ in place, atomically, with no dev-server restart and no loss of Vite's HMR
33
+ state. A component-body-only edit still takes the ordinary Fast Refresh
34
+ path; only membership and route-identity changes go through this path.
35
+ - **A dev-only diagnostic for a page file that exists but isn't reachable.**
36
+ When a request 404s, Warlock checks whether an unregistered `*.page.tsx`
37
+ under `src/web` would have matched it, and if so, warns naming the file.
38
+ This is the case that used to be silent: a page created after boot, or one
39
+ whose `route` was edited to a path nothing else claims, previously 404'd
40
+ with no explanation anywhere in the terminal.
41
+ - **`export const register`** — an optional, synchronous, no-argument hook on
42
+ `root.tsx`, `layout.tsx`, and `*.page.tsx`. It runs once per module
43
+ namespace instance, on both the server and the browser, before that
44
+ module's middleware or loader — the seam for one-time setup a page or
45
+ layout needs on both sides of hydration. It must not return a Promise;
46
+ returning one throws.
47
+
48
+ ### Changed
49
+
50
+ - **Page requests now tolerate one trailing slash identically in development
51
+ and production.** `/about` and `/about/` serve the same page; `/` remains the
52
+ root path and case handling is unchanged. Previously the development
53
+ dispatcher accepted the slash while the production Fastify route returned
54
+ 404.
55
+
56
+ - ⚠ **BREAKING — `process.env` is refused entirely in the client/universal
57
+ graph, and there is no `PUBLIC_` exception.** Neither a static key
58
+ (`process.env.PUBLIC_API_URL`) nor a computed one (`process.env[key]`) is
59
+ allowed: `process` does not exist in a browser, so there is no such thing as
60
+ a "public" `process.env` key. **Bare value-reads of the object now fail
61
+ too** — `const { X } = process.env`, `{ ...process.env }`,
62
+ `Object.keys(process.env)`, `JSON.stringify(process.env)`, or passing it as
63
+ an argument — which is the case that previously let an entire server
64
+ environment reach a component in one line while every keyed read was being
65
+ refused. `globalThis.process.env`, `window.process.env` and
66
+ `process["env"]` are matched as well.
67
+
68
+ **Enforcement now covers dev SSR as well as the client bundle, and a
69
+ violation fails the build** rather than being a production-only surprise.
70
+ Files under `node_modules` stay out of scope by design — a dependency's own
71
+ `process.env.NODE_ENV` guard is not the application's problem.
72
+
73
+ ⚠ **`env("PUBLIC_X")` does not work client-side either**, and never did:
74
+ `env` comes from `@warlock.js/core`, which declares itself server-only, so
75
+ the import is refused before the call is ever examined. **The supported
76
+ pattern is to read the value in a page loader — server code — and pass it to
77
+ the page as loader data:**
78
+
79
+ ```tsx
80
+ export const loader = (async () => ({
81
+ siteName: env("PUBLIC_SITE_NAME"),
82
+ })) satisfies PageLoader;
83
+
84
+ export default function HomePage({ data }: PageProps<typeof loader>) {
85
+ return <h1>{data.siteName}</h1>;
86
+ }
87
+ ```
88
+
89
+ If a value must genuinely be inlined into browser code instead of passed as
90
+ loader data, the one supported spelling is `import.meta.env.PUBLIC_*` with a
91
+ static key — Vite's env surface, baked in at build time, so it cannot vary
92
+ per request. Server-side code is unrestricted.
93
+
94
+ - ⚠ **BREAKING — a `*.page.tsx` with no default export is now a hard
95
+ discovery/build failure, naming the file.** It previously built and
96
+ registered, then served a blank `200` at its URL — a page that looked
97
+ deployed, rendered nothing, and produced no error anywhere.
98
+
99
+ ```
100
+ The page "src/web/contact.page.tsx" has no runtime default export. Every
101
+ `*.page.tsx` file must default-export the React component it renders.
102
+ ```
103
+
104
+ `export { Page as default }` satisfies the rule — the check is for a runtime
105
+ default binding, not for the keyword form. `export default interface Page {}`
106
+ does not: a type-only default is erased and leaves no component behind. A
107
+ file that cannot be parsed reports as a parse failure instead, so a syntax
108
+ error never masquerades as a missing export.
109
+
110
+ - **Initial stylesheet links are route-scoped in development and production.**
111
+ Each response now links the ordered, deduplicated CSS chain for its own
112
+ `[root, ...matched layouts, page]`. Production follows those source entries
113
+ and their static imports in Vite's manifest instead of collecting CSS across
114
+ the whole application; development promotes direct stylesheet imports from
115
+ the matched page and layouts as well as the root. Unrelated page CSS no
116
+ longer ships on every response, and page-local critical CSS no longer waits
117
+ for hydration in development.
118
+ - **The production static-asset refusal now names the working 5.2 alternative.**
119
+ Imported non-stylesheet assets still work under Vite in development but are
120
+ refused by the esbuild server bundle rather than risk a server/client URL
121
+ mismatch. The diagnostic now tells the developer to place the file under the
122
+ application's `public/` directory and reference its root URL
123
+ (`public/logo.svg` → `/logo.svg`) instead of waiting for an unspecified future
124
+ server build. Stylesheet imports remain supported.
125
+ - **Loader execution is sequential, root to leaf, and terminal responses stop
126
+ lower work.** The `root.tsx` App loader runs first, followed by every matched
127
+ layout loader from outermost to innermost, then the page loader. The runtime
128
+ has three top-level slots (`app`, `layout`, `page`), but the layout slot
129
+ composes the full matched layout chain. A page still has at most one
130
+ *rendering* layout; loader-only and middleware-only layouts may appear at
131
+ multiple ancestry levels.
132
+
133
+ ⚠ **This package's own documentation previously described the three levels as
134
+ running in parallel, and told you not to rely on ordering between them.** The
135
+ implementation now awaits the App slot, the composed outer-to-inner layout
136
+ slot, and the Page slot in that order.
137
+
138
+ **The first core `Response` a loader returns is terminal**: it stops every
139
+ lower loader from starting, and — because the response is returned whole — it
140
+ also bypasses buffer commit and metadata resolution, discarding the header
141
+ and cookie writes buffered at that same level. A short-circuit
142
+ (`response.redirect()`, `response.notFound()`) commits its own level's buffer
143
+ inclusively and is the right choice when those writes should survive; a throw
144
+ discards the throwing level's buffer and commits only the levels above it.
145
+
146
+ - **Catch-all page routes are documented as unsupported.** `[...slug].page.tsx`
147
+ does not do what it looks like: filesystem routing recognizes only `[name]` as
148
+ a dynamic segment, so `[...slug]` is taken as a **literal** segment and derives
149
+ the path `/docs/[...slug]` and the name `docs.[...slug]` — reachable only at
150
+ the literal URL `/docs/%5B...slug%5D`. ⚠ **Nothing warns about it**: no build
151
+ error, no dev warning, no refusal, just a page that answers a URL nobody will
152
+ request. A real catch-all is deferred; until then use a terminal wildcard with
153
+ an explicit route (`route = { path: "/docs/*" }`). This entry records the gap,
154
+ it does not close it.
155
+
156
+ - **`src/web` is the only page root.** A per-module `src/app/<module>/web/`
157
+ tree is no longer discovered, walked, or installed as a page root by either
158
+ `warlock dev` or `warlock build`. Move any page, layout, or root file that
159
+ lived under `src/app/<module>/web/` into `src/web/` (a subdirectory is
160
+ fine — it still contributes a route segment the same way).
161
+
162
+ ### Fixed
163
+
164
+ - **A custom `404.page.tsx` loader no longer executes.** The not-found page
165
+ still registers and renders its real module namespace, but its request
166
+ triple omits the page loader in both development and production. A missing
167
+ URL therefore cannot trigger application data work, redirect, or fail a
168
+ second time through the fallback itself.
169
+
170
+ Precisely what is skipped, because "the 404 page doesn't run loaders" is a
171
+ useful shorthand and not the whole rule: only the **page-level `loader`** is
172
+ omitted. The real module namespace is still used, so `register()` runs and
173
+ the component renders normally, and the page's **middleware still runs**.
174
+ Layout loaders don't run because this page has an empty layout chain by
175
+ construction, not because loaders are disabled on it. And the **`root.tsx`
176
+ App loader does still run** on a 404 request — keep it cheap, and make sure
177
+ it tolerates a request that matched nothing.
178
+
5
179
  ## 5.1.0
6
180
 
7
181
  > **Upgrade if you installed 5.0.0, 5.0.1 or 5.0.2.** React did not execute at all in
package/README.md CHANGED
@@ -41,7 +41,9 @@ export const route = "/products";
41
41
  export const loader = (async ({ response }) => {
42
42
  response.header("cache-control", "private, max-age=60");
43
43
 
44
- return { products: productResourceCollection(await productsRepository.all()) };
44
+ return {
45
+ products: productResourceCollection(await productsRepository.all()),
46
+ };
45
47
  }) satisfies PageLoader;
46
48
 
47
49
  /** Server-only. Runs after the loader and receives its data. */
@@ -54,7 +56,7 @@ export const metadata = ({ data }) => ({
54
56
  export default function ProductsPage({ data }: PageProps<typeof loader>) {
55
57
  return (
56
58
  <ul>
57
- {data.products.map(product => (
59
+ {data.products.map((product) => (
58
60
  <li key={product.id}>{product.name}</li>
59
61
  ))}
60
62
  </ul>
@@ -76,34 +78,146 @@ production.
76
78
  compile error.
77
79
  - **Loaders that are controllers.** Full request context, guards, DI, and the
78
80
  ability to set headers, cookies and status during the render.
79
- - **`revalidate()`.** POST to your own API, call it, and the page's loaders
80
- re-run.
81
+ - **`refresh()`.** POST to your own API, call it, and the page's loaders
82
+ re-run without pushing history. There is no `revalidate()` export.
81
83
  - **Return values are Resources, never models.** A model does not survive the
82
84
  wire: it carries methods, a connector handle and every column.
83
85
 
84
86
  ## The two halves of a page file
85
87
 
86
- | Server-only | Runs twice (server + browser) |
87
- |---|---|
88
- | `route`, `middleware`, `validation`, `loader`, `metadata` | `Layout`, `ErrorBoundary`, default `Page` |
88
+ | Server-only | Runs twice (server + browser) |
89
+ | ---------------------------------------------------------------------------------- | ------------------------------------- |
90
+ | `route`, `middleware`, `validation`, `loader`, `metadata`, `prefix` (layouts only) | default `Page` / `Layout`, `register` |
89
91
 
90
92
  The server half is stripped before anything reaches the browser. The runs-twice
91
93
  half never receives `request` or `response` — it also executes in a browser,
92
94
  where neither exists — and is never `async`.
93
95
 
96
+ **A `.client.tsx` suffix is a naming convention, not an SSR-isolation boundary
97
+ in 5.2.** A module statically imported by a root, layout, page, or one of their
98
+ imports is still evaluated by the server. Top-level browser globals such as
99
+ `window` therefore crash SSR boot. Warlock 5.2 does not ship a client-only
100
+ component primitive; code that requires browser globals at module scope cannot
101
+ be part of the SSR page graph.
102
+
103
+ **Imported non-stylesheet static assets are also unsupported by the production
104
+ server build in 5.2.** An import such as `import logo from "./logo.svg"` works
105
+ under Vite in development but is refused by `warlock build`. Put the file under
106
+ the application's `public/` directory and reference its root URL instead:
107
+ `public/logo.svg` is `/logo.svg`. Stylesheet imports remain supported.
108
+
109
+ ### Loader order
110
+
111
+ Loaders run **sequentially, root to leaf, each awaited before the next
112
+ starts**: the `root.tsx` App loader, then every matched layout loader from
113
+ outermost to innermost, then the page loader. The runtime has three top-level
114
+ slots (`app`, `layout`, `page`), but the layout slot composes the full matched
115
+ layout chain. Only one layout on that chain may render; loader-only and
116
+ middleware-only layouts still participate.
117
+
118
+ **The first core `Response` returned by a loader is terminal**: it stops every
119
+ lower loader from running, skips metadata, and is sent as-is. Because it is
120
+ returned whole, the buffered header and cookie writes made at that level go
121
+ with it — use `response.redirect()` / `response.notFound()` when you want those
122
+ writes committed, and a raw `Response` only when you mean exactly that
123
+ response.
124
+
125
+ ### Environment variables
126
+
127
+ **`process.env` is refused entirely in the client/universal graph. There is no
128
+ `PUBLIC_` exception to it — static or computed — and `env("PUBLIC_X")` does not
129
+ work client-side either.** The supported pattern is to read the value in a page
130
+ loader, which is server code, and pass it to the page as loader data:
131
+
132
+ ```tsx
133
+ import { env } from "@warlock.js/core";
134
+ import type { PageLoader, PageProps } from "@warlock.js/web";
135
+
136
+ export const loader = (async () => ({
137
+ siteName: env("PUBLIC_SITE_NAME"),
138
+ })) satisfies PageLoader;
139
+
140
+ export default function HomePage({ data }: PageProps<typeof loader>) {
141
+ return <h1>{data.siteName}</h1>;
142
+ }
143
+ ```
144
+
145
+ The loader is server-only, so it may read configuration normally. Its return
146
+ value becomes page data and is serialized to the browser; return only values
147
+ that are safe to expose.
148
+
149
+ The refusal applies to default page and layout components, `register()`, and
150
+ any helper they import. It covers keyed reads (`process.env.X`,
151
+ `process.env.PUBLIC_X`, `process.env[key]`) and **bare value-reads** of the
152
+ object itself — `const { X } = process.env`, `{ ...process.env }`,
153
+ `Object.keys(process.env)`, `JSON.stringify(process.env)`. `process` does not
154
+ exist in a browser, so touching the object at all is already broken, and
155
+ handing the whole object to a component is how a server secret ships in one
156
+ line.
157
+
158
+ **Enforcement covers dev SSR as well as the client bundle, and a violation
159
+ fails the build** — this is not a production-only check you find out about
160
+ late. Server-side code is unrestricted.
161
+
162
+ If a value must genuinely be inlined into browser code rather than passed as
163
+ loader data, the one supported spelling is `import.meta.env.PUBLIC_*` with a
164
+ static key. That is Vite's surface, not Node's; it is baked in at build time,
165
+ so it cannot vary per request — loader data remains the answer for anything
166
+ request-scoped.
167
+
168
+ ## Routing
169
+
170
+ A page's URL is its `route` export when it declares one. A page with no
171
+ `route` derives its URL from its own location beneath `src/web`: directories
172
+ contribute segments, `(group)` directories contribute nothing, `index.page.tsx`
173
+ claims its directory, and `[id]` becomes `:id`. A layout's `prefix` still
174
+ composes in front of either form. `route`, when present, always wins.
175
+
176
+ Every `*.page.tsx` must have a **default export**. A page file with only named
177
+ exports is a hard discovery/build failure naming the file — it used to build
178
+ and then serve a blank `200` at its URL.
179
+
180
+ **Catch-all routes are not supported.** `[...slug].page.tsx` is not a rest
181
+ parameter: only `[name]` is recognized as dynamic, so `[...slug]` is taken as a
182
+ literal segment and derives the unreachable path `/[...slug]`. Nothing warns
183
+ about it. Use a terminal wildcard with an explicit route
184
+ (`route = { path: "/docs/*" }`) until a real catch-all exists.
185
+
186
+ `src/web` is the only page root — a per-module `src/app/<module>/web/` tree is
187
+ not scanned.
188
+
189
+ Incoming URLs may carry one trailing slash: `/about` and `/about/` match the
190
+ same page in development and production. Route declarations remain canonical
191
+ and slash-free, `/` stays the root path, and case handling is unchanged.
192
+
193
+ Exactly two page filenames are special: `404.page.tsx` (the not-found page,
194
+ reached by not matching, never declares `route`, renders with no layout) and
195
+ `error.page.tsx` (the application's one error boundary, also declares no
196
+ `route`). There is no `500.page.tsx`; an unmatched URL is not an error.
197
+
198
+ **`404.page.tsx` never runs its own loader.** The module is registered and
199
+ rendered for real — `register()` and its middleware still run — but the page
200
+ loader is omitted from the request in both development and production, so a
201
+ missing URL cannot trigger application data work, redirect, or fail a second
202
+ time through the fallback. Layout loaders do not run either, because the page
203
+ has an empty layout chain by construction. The `root.tsx` App loader **does**
204
+ still run, so keep it cheap and make sure it tolerates a request that matched
205
+ nothing.
206
+
207
+ In development, creating, deleting, or editing a page's `route` export updates
208
+ the live route table without restarting `warlock dev`.
209
+
94
210
  ## Where things live
95
211
 
96
212
  ```
97
- src/web/ app-level web layer, and a page root
213
+ src/web/ app-level web layer and page root
98
214
  root.tsx owns <html>, renders #root
99
215
  layouts/ shared layouts
100
216
  middleware/
101
- src/app/<module>/web/ that module's own pages
102
217
  ```
103
218
 
104
- Both are page roots. All client code lives in a `web/` folder, and nothing
105
- outside one may import from one — so `rm -rf src/web src/app/*/web` removes the
106
- page layer and leaves a working API.
219
+ All client code lives in `src/web/`, so `rm -rf src/web` removes the page layer
220
+ and leaves a working API.
107
221
 
108
222
  **`web/` means the web layer, not the browser.** Loaders and page middleware
109
223
  inside it are server code.
@@ -1,6 +1,7 @@
1
1
  import { ConnectorBuildContribution } from "@warlock.js/core";
2
+ import { PluginOption } from "vite";
2
3
 
3
- //#region ../web/src/build/contribution.d.ts
4
+ //#region web/src/build/contribution.d.ts
4
5
  type WebBuildOptions = {
5
6
  /** Source directory name under the app root. Default: `"src"`. */srcDir?: string;
6
7
  /**
@@ -27,16 +28,6 @@ type WebBuildOptions = {
27
28
  clientOutDir?: string; /** Client-build resolve aliases: specifier -> absolute path. */
28
29
  aliases?: Record<string, string>; /** Extra package names to keep external to the client bundle. */
29
30
  external?: string[];
30
- /**
31
- * NOT AN APP-FACING OPTION. Set by `webConnector()` from the length of its own
32
- * `plugins` array; a COUNT rather than the array itself, because this options
33
- * object is JSON-serializable-values-only (constraint B) and a plugin instance
34
- * here would drag Vite into every config load.
35
- *
36
- * Its only consumer is the refusal in `generate` — see
37
- * {@link ConnectorPluginsNotSupportedError}.
38
- */
39
- connectorPluginCount?: number;
40
31
  };
41
32
  /**
42
33
  * The resolved `@warlock.js/web` root is not that package.
@@ -54,12 +45,18 @@ declare class WebPackageRootResolutionError extends Error {
54
45
  *
55
46
  * `generate` writes the page barrel into `ctx.productionDir` and patches
56
47
  * esbuild; `emit` produces the client bundle esbuild cannot. The two share one
57
- * closure fact — how many pages exist because a build with zero pages must
58
- * skip the client bundle rather than emit an orphan one. `generate` itself does
48
+ * closure facts — how many pages exist, the discovered route table, and the
49
+ * exact app-public file list. A build with zero pages skips the hydration
50
+ * bundle but still copies public files when present. `generate` itself does
59
51
  * NOT skip: the barrel and its entry import are emitted either way, so the
60
52
  * runtime can tell a page-free web build from a build without web.
53
+ *
54
+ * Connector plugins are a separate argument rather than a `WebBuildOptions`
55
+ * field: the app constructs them once, the factory snapshots that array for
56
+ * both dev and build, and this module keeps Vite as a type-only import until
57
+ * `emit` dynamically loads the real client builder.
61
58
  */
62
- declare function createWebBuildContribution(options?: WebBuildOptions): ConnectorBuildContribution;
59
+ declare function createWebBuildContribution(options?: WebBuildOptions, connectorPlugins?: readonly PluginOption[]): ConnectorBuildContribution;
63
60
  //#endregion
64
61
  export { WebBuildOptions, WebPackageRootResolutionError, createWebBuildContribution };
65
62
  //# sourceMappingURL=contribution.d.mts.map
@@ -1,7 +1,7 @@
1
- import path from "node:path";
2
1
  import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
3
 
4
- //#region ../web/src/build/contribution.ts
4
+ //#region web/src/build/contribution.ts
5
5
  /**
6
6
  * Web's build-time half — the object a `WebConnector` hands to
7
7
  * `warlock build` as its `build` contribution.
@@ -37,40 +37,6 @@ var ClientOutDirNotSupportedError = class extends Error {
37
37
  this.name = "ClientOutDirNotSupportedError";
38
38
  }
39
39
  };
40
- /**
41
- * `webConnector({ plugins })` was given plugins, and `warlock build` cannot
42
- * apply them.
43
- *
44
- * THE SILENT FAILURE THIS REPLACES. `plugins` reaches exactly one place: the
45
- * dev server's `createServer({ plugins: [...] })`. The production client bundle
46
- * is built by `buildWarlockHydrationClient`, which composes its own pipeline
47
- * (`web/src/vite/index.ts` — projection and the boundary gates) and is not
48
- * handed the connector's array by anyone. So a plugin worked in `warlock dev`
49
- * and was absent from `warlock build`, with no warning and a green build log:
50
- * the site shipped unstyled, or unprocessed in whatever way the plugin
51
- * mattered, and the build claimed success. That is worse than any loud failure.
52
- *
53
- * WHY REFUSE RATHER THAN WIRE IT THROUGH. Plugin ORDER is part of a pipeline's
54
- * behavior, and a plugin authored against a dev server can misbehave inside a
55
- * production build; threading the array in is a change to build behavior that
56
- * has to be verified, not assumed. Refusing removes the silent failure now and
57
- * is strictly additive to reverse — wiring the plugins through later breaks no
58
- * app that this error currently stops, while shipping a half-verified plugin
59
- * pipeline could break every one of them.
60
- *
61
- * The message names the option and says what to do instead, because an error
62
- * that only says "no" costs the reader the same hour it took to find this.
63
- */
64
- var ConnectorPluginsNotSupportedError = class extends Error {
65
- constructor(pluginCount) {
66
- super(`"plugins" on the web connector is not supported by \`warlock build\`: webConnector({ plugins }) was given ${pluginCount} plugin${pluginCount === 1 ? "" : "s"}, and they reach the dev server ONLY. The production client bundle is built with the framework's own pipeline and would silently ship without them, so this build refuses rather than emit an artifact that differs from what you saw in \`warlock dev\`.
67
-
68
- What to do instead: express the transform somewhere BOTH dev and build already read.
69
- - CSS/PostCSS (Tailwind, autoprefixer, ...): put a \`postcss.config.mjs\` at your app root. Vite loads it automatically in dev and in build — this is how Tailwind is supported today.
70
- - Otherwise: remove \`plugins\` from webConnector() and open an issue describing what the plugin does, so the production pipeline can support it deliberately.`);
71
- this.name = "ConnectorPluginsNotSupportedError";
72
- }
73
- };
74
40
  /** The package name every candidate web root must declare to be one. */
75
41
  const WEB_PACKAGE_NAME = "@warlock.js/web";
76
42
  /**
@@ -120,45 +86,67 @@ async function resolveWebPackageRoot(configured) {
120
86
  *
121
87
  * `generate` writes the page barrel into `ctx.productionDir` and patches
122
88
  * esbuild; `emit` produces the client bundle esbuild cannot. The two share one
123
- * closure fact — how many pages exist because a build with zero pages must
124
- * skip the client bundle rather than emit an orphan one. `generate` itself does
89
+ * closure facts — how many pages exist, the discovered route table, and the
90
+ * exact app-public file list. A build with zero pages skips the hydration
91
+ * bundle but still copies public files when present. `generate` itself does
125
92
  * NOT skip: the barrel and its entry import are emitted either way, so the
126
93
  * runtime can tell a page-free web build from a build without web.
94
+ *
95
+ * Connector plugins are a separate argument rather than a `WebBuildOptions`
96
+ * field: the app constructs them once, the factory snapshots that array for
97
+ * both dev and build, and this module keeps Vite as a type-only import until
98
+ * `emit` dynamically loads the real client builder.
127
99
  */
128
- function createWebBuildContribution(options = {}) {
100
+ function createWebBuildContribution(options = {}, connectorPlugins = []) {
129
101
  if (options.clientOutDir !== void 0) throw new ClientOutDirNotSupportedError();
130
102
  let pageCount = 0;
103
+ let pageRoutes = {
104
+ version: 1,
105
+ routes: []
106
+ };
107
+ let publicFiles = [];
131
108
  return {
132
109
  async generate(context) {
133
- const connectorPluginCount = options.connectorPluginCount ?? 0;
134
- if (connectorPluginCount > 0) throw new ConnectorPluginsNotSupportedError(connectorPluginCount);
135
110
  const { generatePagesBarrel, WEB_ENTRY_IMPORT, WEB_ESBUILD_PATCH } = await import("./generate-pages-barrel.mjs");
136
- pageCount = (await generatePagesBarrel({
111
+ const { collectPublicFiles } = await import("./public-files.mjs");
112
+ publicFiles = await collectPublicFiles(path.join(context.appRoot, "public"));
113
+ const result = await generatePagesBarrel({
137
114
  appRoot: context.appRoot,
138
115
  productionDir: context.productionDir,
139
116
  srcDir: options.srcDir,
140
- clientDir: path.relative(context.appRoot, resolveClientOutDir(context)).split(path.sep).join("/")
141
- })).pageCount;
117
+ clientDir: path.relative(context.appRoot, resolveClientOutDir(context)).split(path.sep).join("/"),
118
+ publicFiles
119
+ });
120
+ pageCount = result.pageCount;
121
+ pageRoutes = result.pageRoutes;
142
122
  return {
143
123
  entryImports: [WEB_ENTRY_IMPORT],
144
124
  esbuild: WEB_ESBUILD_PATCH
145
125
  };
146
126
  },
147
127
  async emit(context) {
148
- if (pageCount === 0) return;
149
- const { buildWarlockHydrationClient } = await import("../vite/index.mjs");
150
- const { appConventionAliases } = await import("../vite/app-convention-aliases.mjs");
151
- const callerAliases = Object.entries(options.aliases ?? {}).map(([find, replacement]) => ({
152
- find,
153
- replacement
154
- }));
155
- await buildWarlockHydrationClient({
156
- appRoot: context.appRoot,
157
- webRoot: await resolveWebPackageRoot(options.webRoot),
158
- outDir: resolveClientOutDir(context),
159
- resolveAliases: [...callerAliases, ...appConventionAliases(path.join(context.appRoot, options.srcDir ?? "src"))],
160
- external: options.external
161
- });
128
+ if (pageCount > 0) {
129
+ const { buildWarlockHydrationClient } = await import("../vite/index.mjs");
130
+ const { appConventionAliases } = await import("../vite/app-convention-aliases.mjs");
131
+ const callerAliases = Object.entries(options.aliases ?? {}).map(([find, replacement]) => ({
132
+ find,
133
+ replacement
134
+ }));
135
+ await buildWarlockHydrationClient({
136
+ appRoot: context.appRoot,
137
+ webRoot: await resolveWebPackageRoot(options.webRoot),
138
+ outDir: resolveClientOutDir(context),
139
+ resolveAliases: [...callerAliases, ...appConventionAliases(path.join(context.appRoot, options.srcDir ?? "src"))],
140
+ external: options.external,
141
+ plugins: connectorPlugins
142
+ });
143
+ }
144
+ if (publicFiles.length > 0) {
145
+ const { copyPublicFiles } = await import("./public-files.mjs");
146
+ await copyPublicFiles(path.join(context.appRoot, "public"), path.join(resolveClientOutDir(context), "public"), publicFiles);
147
+ }
148
+ const { writePageRoutesManifest } = await import("./page-routes-manifest.mjs");
149
+ await writePageRoutesManifest(path.resolve(context.appRoot, context.options.outdir), pageRoutes);
162
150
  }
163
151
  };
164
152
  }
@@ -1 +1 @@
1
- {"version":3,"file":"contribution.mjs","names":[],"sources":["../../../../../../../web/src/build/contribution.ts"],"sourcesContent":["/**\n * Web's build-time half — the object a `WebConnector` hands to\n * `warlock build` as its `build` contribution.\n *\n * KEPT DELIBERATELY LIGHT: everything this\n * module imports statically is a type or `node:path`. The heavy halves — the\n * filesystem discovery/barrel generator, and the Vite client build — are\n * `await import(...)`ed INSIDE the hooks, so a config file that merely\n * constructs the connector never drags Vite, React or the page graph into its\n * static import graph.\n *\n * {@link WebBuildOptions} carries JSON-SERIALIZABLE VALUES ONLY (constraint\n * B): no plugin instances, no functions, no class instances. Anything heavy is\n * constructed inside a hook after that hook's dynamic import.\n */\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type {\n ConnectorBuildContext,\n ConnectorBuildContribution,\n ConnectorBuildGenerateResult,\n} from \"@warlock.js/core\";\n\nexport type WebBuildOptions = {\n /** Source directory name under the app root. Default: `\"src\"`. */\n srcDir?: string;\n /**\n * Absolute path to the `@warlock.js/web` package root, which holds the\n * hydration entry (packaged as `esm/hydration/index.mjs`, with a source\n * fallback at `src/hydration/index.ts` for this checkout).\n *\n * Defaults to this module's own package root, derived from `import.meta.url`\n * at emit time. Set it explicitly when the build process loads this module\n * through a transform that rewrites `import.meta` (e.g. a CJS config\n * pipeline) — the derivation is the only thing here that depends on it.\n *\n * Derived or configured, the root is VERIFIED at emit time against\n * `<root>/package.json`'s `name`; a root that is not `@warlock.js/web`\n * throws {@link WebPackageRootResolutionError} rather than being guessed at.\n */\n webRoot?: string;\n /**\n * NOT SUPPORTED. The client bundle always lands at `<build.outdir>/client`,\n * the layout the runtime half reads (`resolveHydrationClientUrl`) — the\n * runtime does not yet consult a relocated path. Setting this option throws\n * {@link ClientOutDirNotSupportedError}.\n */\n clientOutDir?: string;\n /** Client-build resolve aliases: specifier -> absolute path. */\n aliases?: Record<string, string>;\n /** Extra package names to keep external to the client bundle. */\n external?: string[];\n /**\n * NOT AN APP-FACING OPTION. Set by `webConnector()` from the length of its own\n * `plugins` array; a COUNT rather than the array itself, because this options\n * object is JSON-serializable-values-only (constraint B) and a plugin instance\n * here would drag Vite into every config load.\n *\n * Its only consumer is the refusal in `generate` — see\n * {@link ConnectorPluginsNotSupportedError}.\n */\n connectorPluginCount?: number;\n};\n\nfunction resolveClientOutDir(context: ConnectorBuildContext): string {\n return path.resolve(context.appRoot, context.options.outdir, \"client\");\n}\n\n/**\n * `build.clientOutDir` was set.\n *\n * The production runtime hardcodes where it reads the client bundle from\n * (`<outdir>/client`, via `resolveHydrationClientUrl`) — it does not yet\n * consult the build config for a relocated path. A build that honored a\n * custom `clientOutDir` would therefore produce an artifact the runtime\n * cannot find, silently. Rejected at build start, before any work happens,\n * rather than left to surface later as a missing-bundle failure in\n * production.\n */\nexport class ClientOutDirNotSupportedError extends Error {\n public constructor() {\n super(\n '\"build.clientOutDir\" is not supported: the production server reads the client bundle ' +\n \"from its default location and does not consult this option, so a build honoring it \" +\n \"would produce an artifact production cannot serve correctly. Remove \" +\n '\"build.clientOutDir\" from the build config.',\n );\n this.name = \"ClientOutDirNotSupportedError\";\n }\n}\n\n/**\n * `webConnector({ plugins })` was given plugins, and `warlock build` cannot\n * apply them.\n *\n * THE SILENT FAILURE THIS REPLACES. `plugins` reaches exactly one place: the\n * dev server's `createServer({ plugins: [...] })`. The production client bundle\n * is built by `buildWarlockHydrationClient`, which composes its own pipeline\n * (`web/src/vite/index.ts` — projection and the boundary gates) and is not\n * handed the connector's array by anyone. So a plugin worked in `warlock dev`\n * and was absent from `warlock build`, with no warning and a green build log:\n * the site shipped unstyled, or unprocessed in whatever way the plugin\n * mattered, and the build claimed success. That is worse than any loud failure.\n *\n * WHY REFUSE RATHER THAN WIRE IT THROUGH. Plugin ORDER is part of a pipeline's\n * behavior, and a plugin authored against a dev server can misbehave inside a\n * production build; threading the array in is a change to build behavior that\n * has to be verified, not assumed. Refusing removes the silent failure now and\n * is strictly additive to reverse — wiring the plugins through later breaks no\n * app that this error currently stops, while shipping a half-verified plugin\n * pipeline could break every one of them.\n *\n * The message names the option and says what to do instead, because an error\n * that only says \"no\" costs the reader the same hour it took to find this.\n */\nexport class ConnectorPluginsNotSupportedError extends Error {\n public constructor(pluginCount: number) {\n super(\n `\"plugins\" on the web connector is not supported by \\`warlock build\\`: ` +\n `webConnector({ plugins }) was given ${pluginCount} plugin${pluginCount === 1 ? \"\" : \"s\"}, ` +\n \"and they reach the dev server ONLY. The production client bundle is built with the \" +\n \"framework's own pipeline and would silently ship without them, so this build refuses \" +\n \"rather than emit an artifact that differs from what you saw in `warlock dev`.\\n\\n\" +\n \"What to do instead: express the transform somewhere BOTH dev and build already read.\\n\" +\n \" - CSS/PostCSS (Tailwind, autoprefixer, ...): put a `postcss.config.mjs` at your app \" +\n \"root. Vite loads it automatically in dev and in build — this is how Tailwind is \" +\n \"supported today.\\n\" +\n \" - Otherwise: remove `plugins` from webConnector() and open an issue describing what \" +\n \"the plugin does, so the production pipeline can support it deliberately.\",\n );\n this.name = \"ConnectorPluginsNotSupportedError\";\n }\n}\n\n/** The package name every candidate web root must declare to be one. */\nconst WEB_PACKAGE_NAME = \"@warlock.js/web\";\n\n/**\n * The resolved `@warlock.js/web` root is not that package.\n *\n * Same posture as the manifest resolver's three named errors: a root that\n * cannot be proven is REJECTED, never quietly\n * repaired by walking upwards or falling back to `process.cwd()`. A wrong root\n * would otherwise surface much later as an unintelligible Vite entry failure.\n */\nexport class WebPackageRootResolutionError extends Error {\n public constructor(webRoot: string, reason: string, cause?: unknown) {\n super(\n `Cannot resolve the \"${WEB_PACKAGE_NAME}\" package root: \"${webRoot}\" ${reason}. ` +\n \"Pass `webRoot` explicitly in the web build options to point at the package root \" +\n `whose package.json declares \"name\": \"${WEB_PACKAGE_NAME}\".`,\n { cause },\n );\n this.name = \"WebPackageRootResolutionError\";\n }\n}\n\n/**\n * Proves that `webRoot` really is the `@warlock.js/web` package root by reading\n * `<webRoot>/package.json` and matching its `name`. Returns the path unchanged.\n */\nexport function assertWebPackageRoot(webRoot: string): string {\n const manifestPath = path.join(webRoot, \"package.json\");\n\n let raw: string;\n\n try {\n raw = readFileSync(manifestPath, \"utf-8\");\n } catch (error) {\n throw new WebPackageRootResolutionError(webRoot, \"has no readable package.json\", error);\n }\n\n let parsed: unknown;\n\n try {\n parsed = JSON.parse(raw);\n } catch (error) {\n throw new WebPackageRootResolutionError(\n webRoot,\n \"has a package.json that is not valid JSON\",\n error,\n );\n }\n\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new WebPackageRootResolutionError(\n webRoot,\n \"has a package.json whose root is not a JSON object\",\n );\n }\n\n const name = (parsed as { name?: unknown }).name;\n\n if (name !== WEB_PACKAGE_NAME) {\n throw new WebPackageRootResolutionError(\n webRoot,\n `declares package name ${JSON.stringify(name)}, not \"${WEB_PACKAGE_NAME}\"`,\n );\n }\n\n return webRoot;\n}\n\nexport async function resolveWebPackageRoot(configured: string | undefined): Promise<string> {\n if (configured !== undefined) {\n return assertWebPackageRoot(path.resolve(configured));\n }\n\n const { fileURLToPath } = await import(\"node:url\");\n\n // `web/src/build/contribution.ts` published as `web/esm/build/contribution.js`\n // — two levels up is the package root under both layouts.\n return assertWebPackageRoot(\n path.resolve(path.dirname(fileURLToPath(import.meta.url)), \"..\", \"..\"),\n );\n}\n\n/**\n * Builds web's `ConnectorBuildContribution`.\n *\n * `generate` writes the page barrel into `ctx.productionDir` and patches\n * esbuild; `emit` produces the client bundle esbuild cannot. The two share one\n * closure fact — how many pages exist — because a build with zero pages must\n * skip the client bundle rather than emit an orphan one. `generate` itself does\n * NOT skip: the barrel and its entry import are emitted either way, so the\n * runtime can tell a page-free web build from a build without web.\n */\nexport function createWebBuildContribution(\n options: WebBuildOptions = {},\n): ConnectorBuildContribution {\n if (options.clientOutDir !== undefined) {\n throw new ClientOutDirNotSupportedError();\n }\n\n let pageCount = 0;\n\n return {\n async generate(context: ConnectorBuildContext): Promise<ConnectorBuildGenerateResult | void> {\n // REFUSED HERE, NOT IN THE CONSTRUCTOR ABOVE, and the difference is the\n // whole of `warlock dev` still working.\n //\n // `webConnector()` — and therefore `createWebBuildContribution()` — runs\n // on every config load, dev included. Throwing at construction the way\n // `clientOutDir` does would make a connector with dev-only plugins fail\n // to boot the dev server those plugins exist for. `generate` is the first\n // hook `warlock build` calls, and nothing but a build calls it: refusing\n // here fails the build before any barrel is written or any bundle emitted,\n // and is unreachable from dev.\n //\n // Unconditional on page count. `emit` skips the client bundle when the app\n // has zero pages, but \"your plugins will not be applied\" is true either\n // way, and a refusal that depends on how many pages you happen to have is\n // a worse contract than one that does not.\n const connectorPluginCount = options.connectorPluginCount ?? 0;\n\n if (connectorPluginCount > 0) {\n throw new ConnectorPluginsNotSupportedError(connectorPluginCount);\n }\n\n const { generatePagesBarrel, WEB_ENTRY_IMPORT, WEB_ESBUILD_PATCH } = await import(\n \"./generate-pages-barrel\"\n );\n\n const result = await generatePagesBarrel({\n appRoot: context.appRoot,\n productionDir: context.productionDir,\n srcDir: options.srcDir,\n // Derived from `resolveClientOutDir` — the SAME function `emit` passes\n // to the Vite build below — so the path baked into the manifest and\n // the path the bundle is written to are one expression, not two that\n // happen to agree today.\n // POSIX-normalised inline rather than via the generator's `toPosix`:\n // this module's static graph is `node:path` and types only, and the\n // generator is reached by `await import` precisely to keep it that way.\n clientDir: path\n .relative(context.appRoot, resolveClientOutDir(context))\n .split(path.sep)\n .join(\"/\"),\n });\n\n pageCount = result.pageCount;\n\n // Contributed unconditionally, zero pages included: the barrel is always\n // written, and the entry has to IMPORT it for the empty table to reach\n // the runtime. Withholding this line on zero pages would leave the\n // manifest absent, which is the runtime's signal for \"never built with\n // web\" — the exact confusion the always-written barrel removes.\n return { entryImports: [WEB_ENTRY_IMPORT], esbuild: WEB_ESBUILD_PATCH };\n },\n\n async emit(context: ConnectorBuildContext): Promise<void> {\n if (pageCount === 0) {\n return;\n }\n\n const { buildWarlockHydrationClient } = await import(\"../vite\");\n const { appConventionAliases } = await import(\"../vite/app-convention-aliases\");\n\n // The app-tree convention (`web/*`, `app/*`) the dev server installs must\n // also reach the production build, from the SAME definition — see\n // `app-convention-aliases.ts`. Caller-supplied aliases come first so a\n // caller can win a conflict, matching the dev server's ordering.\n const callerAliases = Object.entries(options.aliases ?? {}).map(([find, replacement]) => ({\n find,\n replacement,\n }));\n\n await buildWarlockHydrationClient({\n appRoot: context.appRoot,\n webRoot: await resolveWebPackageRoot(options.webRoot),\n outDir: resolveClientOutDir(context),\n resolveAliases: [\n ...callerAliases,\n ...appConventionAliases(path.join(context.appRoot, options.srcDir ?? \"src\")),\n ],\n external: options.external,\n });\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"contribution.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/build/contribution.ts"],"sourcesContent":["/**\r\n * Web's build-time half — the object a `WebConnector` hands to\r\n * `warlock build` as its `build` contribution.\r\n *\r\n * KEPT DELIBERATELY LIGHT: everything this\r\n * module imports statically is a type or `node:path`. The heavy halves — the\r\n * filesystem discovery/barrel generator, and the Vite client build — are\r\n * `await import(...)`ed INSIDE the hooks, so a config file that merely\r\n * constructs the connector never drags Vite, React or the page graph into its\r\n * static import graph.\r\n *\r\n * {@link WebBuildOptions} carries JSON-SERIALIZABLE VALUES ONLY (constraint\r\n * B): no plugin instances, no functions, no class instances. Anything heavy is\r\n * constructed inside a hook after that hook's dynamic import.\r\n */\r\nimport { readFileSync } from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport type {\r\n ConnectorBuildContext,\r\n ConnectorBuildContribution,\r\n ConnectorBuildGenerateResult,\r\n} from \"@warlock.js/core\";\r\nimport type { PluginOption } from \"vite\";\r\n\r\nexport type WebBuildOptions = {\r\n /** Source directory name under the app root. Default: `\"src\"`. */\r\n srcDir?: string;\r\n /**\r\n * Absolute path to the `@warlock.js/web` package root, which holds the\r\n * hydration entry (packaged as `esm/hydration/index.mjs`, with a source\r\n * fallback at `src/hydration/index.ts` for this checkout).\r\n *\r\n * Defaults to this module's own package root, derived from `import.meta.url`\r\n * at emit time. Set it explicitly when the build process loads this module\r\n * through a transform that rewrites `import.meta` (e.g. a CJS config\r\n * pipeline) — the derivation is the only thing here that depends on it.\r\n *\r\n * Derived or configured, the root is VERIFIED at emit time against\r\n * `<root>/package.json`'s `name`; a root that is not `@warlock.js/web`\r\n * throws {@link WebPackageRootResolutionError} rather than being guessed at.\r\n */\r\n webRoot?: string;\r\n /**\r\n * NOT SUPPORTED. The client bundle always lands at `<build.outdir>/client`,\r\n * the layout the runtime half reads (`resolveHydrationClientUrl`) — the\r\n * runtime does not yet consult a relocated path. Setting this option throws\r\n * {@link ClientOutDirNotSupportedError}.\r\n */\r\n clientOutDir?: string;\r\n /** Client-build resolve aliases: specifier -> absolute path. */\r\n aliases?: Record<string, string>;\r\n /** Extra package names to keep external to the client bundle. */\r\n external?: string[];\r\n};\r\n\r\nfunction resolveClientOutDir(context: ConnectorBuildContext): string {\r\n return path.resolve(context.appRoot, context.options.outdir, \"client\");\r\n}\r\n\r\n/**\r\n * `build.clientOutDir` was set.\r\n *\r\n * The production runtime hardcodes where it reads the client bundle from\r\n * (`<outdir>/client`, via `resolveHydrationClientUrl`) — it does not yet\r\n * consult the build config for a relocated path. A build that honored a\r\n * custom `clientOutDir` would therefore produce an artifact the runtime\r\n * cannot find, silently. Rejected at build start, before any work happens,\r\n * rather than left to surface later as a missing-bundle failure in\r\n * production.\r\n */\r\nexport class ClientOutDirNotSupportedError extends Error {\r\n public constructor() {\r\n super(\r\n '\"build.clientOutDir\" is not supported: the production server reads the client bundle ' +\r\n \"from its default location and does not consult this option, so a build honoring it \" +\r\n \"would produce an artifact production cannot serve correctly. Remove \" +\r\n '\"build.clientOutDir\" from the build config.',\r\n );\r\n this.name = \"ClientOutDirNotSupportedError\";\r\n }\r\n}\r\n\r\n/** The package name every candidate web root must declare to be one. */\r\nconst WEB_PACKAGE_NAME = \"@warlock.js/web\";\r\n\r\n/**\r\n * The resolved `@warlock.js/web` root is not that package.\r\n *\r\n * Same posture as the manifest resolver's three named errors: a root that\r\n * cannot be proven is REJECTED, never quietly\r\n * repaired by walking upwards or falling back to `process.cwd()`. A wrong root\r\n * would otherwise surface much later as an unintelligible Vite entry failure.\r\n */\r\nexport class WebPackageRootResolutionError extends Error {\r\n public constructor(webRoot: string, reason: string, cause?: unknown) {\r\n super(\r\n `Cannot resolve the \"${WEB_PACKAGE_NAME}\" package root: \"${webRoot}\" ${reason}. ` +\r\n \"Pass `webRoot` explicitly in the web build options to point at the package root \" +\r\n `whose package.json declares \"name\": \"${WEB_PACKAGE_NAME}\".`,\r\n { cause },\r\n );\r\n this.name = \"WebPackageRootResolutionError\";\r\n }\r\n}\r\n\r\n/**\r\n * Proves that `webRoot` really is the `@warlock.js/web` package root by reading\r\n * `<webRoot>/package.json` and matching its `name`. Returns the path unchanged.\r\n */\r\nexport function assertWebPackageRoot(webRoot: string): string {\r\n const manifestPath = path.join(webRoot, \"package.json\");\r\n\r\n let raw: string;\r\n\r\n try {\r\n raw = readFileSync(manifestPath, \"utf-8\");\r\n } catch (error) {\r\n throw new WebPackageRootResolutionError(webRoot, \"has no readable package.json\", error);\r\n }\r\n\r\n let parsed: unknown;\r\n\r\n try {\r\n parsed = JSON.parse(raw);\r\n } catch (error) {\r\n throw new WebPackageRootResolutionError(\r\n webRoot,\r\n \"has a package.json that is not valid JSON\",\r\n error,\r\n );\r\n }\r\n\r\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\r\n throw new WebPackageRootResolutionError(\r\n webRoot,\r\n \"has a package.json whose root is not a JSON object\",\r\n );\r\n }\r\n\r\n const name = (parsed as { name?: unknown }).name;\r\n\r\n if (name !== WEB_PACKAGE_NAME) {\r\n throw new WebPackageRootResolutionError(\r\n webRoot,\r\n `declares package name ${JSON.stringify(name)}, not \"${WEB_PACKAGE_NAME}\"`,\r\n );\r\n }\r\n\r\n return webRoot;\r\n}\r\n\r\nexport async function resolveWebPackageRoot(configured: string | undefined): Promise<string> {\r\n if (configured !== undefined) {\r\n return assertWebPackageRoot(path.resolve(configured));\r\n }\r\n\r\n const { fileURLToPath } = await import(\"node:url\");\r\n\r\n // `web/src/build/contribution.ts` published as `web/esm/build/contribution.js`\r\n // — two levels up is the package root under both layouts.\r\n return assertWebPackageRoot(\r\n path.resolve(path.dirname(fileURLToPath(import.meta.url)), \"..\", \"..\"),\r\n );\r\n}\r\n\r\n/**\r\n * Builds web's `ConnectorBuildContribution`.\r\n *\r\n * `generate` writes the page barrel into `ctx.productionDir` and patches\r\n * esbuild; `emit` produces the client bundle esbuild cannot. The two share one\r\n * closure facts — how many pages exist, the discovered route table, and the\n * exact app-public file list. A build with zero pages skips the hydration\n * bundle but still copies public files when present. `generate` itself does\n * NOT skip: the barrel and its entry import are emitted either way, so the\n * runtime can tell a page-free web build from a build without web.\n *\r\n * Connector plugins are a separate argument rather than a `WebBuildOptions`\r\n * field: the app constructs them once, the factory snapshots that array for\r\n * both dev and build, and this module keeps Vite as a type-only import until\r\n * `emit` dynamically loads the real client builder.\r\n */\r\nexport function createWebBuildContribution(\r\n options: WebBuildOptions = {},\r\n connectorPlugins: readonly PluginOption[] = [],\r\n): ConnectorBuildContribution {\r\n if (options.clientOutDir !== undefined) {\r\n throw new ClientOutDirNotSupportedError();\r\n }\r\n\r\n let pageCount = 0;\r\n let pageRoutes: import(\"./generate-pages-barrel\").PageRoutesManifest = {\n version: 1,\n routes: [],\n };\n let publicFiles: string[] = [];\n\r\n return {\r\n async generate(context: ConnectorBuildContext): Promise<ConnectorBuildGenerateResult | void> {\r\n const { generatePagesBarrel, WEB_ENTRY_IMPORT, WEB_ESBUILD_PATCH } =\n await import(\"./generate-pages-barrel\");\n const { collectPublicFiles } = await import(\"./public-files\");\n\n publicFiles = await collectPublicFiles(path.join(context.appRoot, \"public\"));\n\n const result = await generatePagesBarrel({\n appRoot: context.appRoot,\r\n productionDir: context.productionDir,\r\n srcDir: options.srcDir,\r\n // Derived from `resolveClientOutDir` — the SAME function `emit` passes\r\n // to the Vite build below — so the path baked into the manifest and\r\n // the path the bundle is written to are one expression, not two that\r\n // happen to agree today.\r\n // POSIX-normalised inline rather than via the generator's `toPosix`:\r\n // this module's static graph is `node:path` and types only, and the\r\n // generator is reached by `await import` precisely to keep it that way.\r\n clientDir: path\r\n .relative(context.appRoot, resolveClientOutDir(context))\r\n .split(path.sep)\n .join(\"/\"),\n publicFiles,\n });\n\r\n pageCount = result.pageCount;\r\n pageRoutes = result.pageRoutes;\r\n\r\n // Contributed unconditionally, zero pages included: the barrel is always\r\n // written, and the entry has to IMPORT it for the empty table to reach\r\n // the runtime. Withholding this line on zero pages would leave the\r\n // manifest absent, which is the runtime's signal for \"never built with\r\n // web\" — the exact confusion the always-written barrel removes.\r\n return { entryImports: [WEB_ENTRY_IMPORT], esbuild: WEB_ESBUILD_PATCH };\r\n },\r\n\r\n async emit(context: ConnectorBuildContext): Promise<void> {\r\n if (pageCount > 0) {\n const { buildWarlockHydrationClient } = await import(\"../vite\");\r\n const { appConventionAliases } = await import(\"../vite/app-convention-aliases\");\r\n\r\n // The app-tree convention (`web/*`, `app/*`) the dev server installs must\r\n // also reach the production build, from the SAME definition — see\r\n // `app-convention-aliases.ts`. Caller-supplied aliases come first so a\r\n // caller can win a conflict, matching the dev server's ordering.\r\n const callerAliases = Object.entries(options.aliases ?? {}).map(([find, replacement]) => ({\r\n find,\r\n replacement,\r\n }));\r\n\r\n await buildWarlockHydrationClient({\r\n appRoot: context.appRoot,\r\n webRoot: await resolveWebPackageRoot(options.webRoot),\r\n outDir: resolveClientOutDir(context),\r\n resolveAliases: [\r\n ...callerAliases,\r\n ...appConventionAliases(path.join(context.appRoot, options.srcDir ?? \"src\")),\r\n ],\r\n external: options.external,\r\n plugins: connectorPlugins,\r\n });\n }\n\n if (publicFiles.length > 0) {\n const { copyPublicFiles } = await import(\"./public-files\");\n await copyPublicFiles(\n path.join(context.appRoot, \"public\"),\n path.join(resolveClientOutDir(context), \"public\"),\n publicFiles,\n );\n }\n\n const { writePageRoutesManifest } = await import(\"./page-routes-manifest\");\n await writePageRoutesManifest(\r\n path.resolve(context.appRoot, context.options.outdir),\r\n pageRoutes,\r\n );\r\n },\r\n };\r\n}\r\n"],"mappings":"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"}