@warlock.js/web 5.0.2 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/CHANGELOG.md +250 -0
  2. package/README.md +126 -12
  3. package/esm/build/contribution.d.mts +11 -4
  4. package/esm/build/contribution.mjs +46 -22
  5. package/esm/build/contribution.mjs.map +1 -1
  6. package/esm/build/discover-pages.mjs +318 -44
  7. package/esm/build/discover-pages.mjs.map +1 -1
  8. package/esm/build/generate-client-registry.mjs +47 -13
  9. package/esm/build/generate-client-registry.mjs.map +1 -1
  10. package/esm/build/generate-pages-barrel.mjs +55 -13
  11. package/esm/build/generate-pages-barrel.mjs.map +1 -1
  12. package/esm/build/page-default-export.mjs +48 -0
  13. package/esm/build/page-default-export.mjs.map +1 -0
  14. package/esm/build/page-routes-manifest.mjs +21 -0
  15. package/esm/build/page-routes-manifest.mjs.map +1 -0
  16. package/esm/build/public-files.mjs +45 -0
  17. package/esm/build/public-files.mjs.map +1 -0
  18. package/esm/build/read-route-exports.mjs +1 -1
  19. package/esm/build/read-route-exports.mjs.map +1 -1
  20. package/esm/client/build-hydrated-tree.mjs +40 -9
  21. package/esm/client/build-hydrated-tree.mjs.map +1 -1
  22. package/esm/client/hydrate-page.mjs +1 -1
  23. package/esm/client/hydrate-page.mjs.map +1 -1
  24. package/esm/client/navigation/current-route.d.mts +1 -1
  25. package/esm/client/navigation/current-route.mjs +1 -1
  26. package/esm/client/navigation/current-route.mjs.map +1 -1
  27. package/esm/client/navigation/fetch-page-data.mjs +1 -1
  28. package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
  29. package/esm/client/navigation/navigation-root.mjs +44 -7
  30. package/esm/client/navigation/navigation-root.mjs.map +1 -1
  31. package/esm/client/navigation/prefetch.mjs +1 -1
  32. package/esm/client/navigation/prefetch.mjs.map +1 -1
  33. package/esm/client/navigation/refresh.d.mts +1 -1
  34. package/esm/client/navigation/refresh.mjs +1 -1
  35. package/esm/client/navigation/refresh.mjs.map +1 -1
  36. package/esm/client/navigation/scroll-to-fragment.mjs +26 -0
  37. package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -0
  38. package/esm/client/navigation/verbs.d.mts +1 -1
  39. package/esm/client/navigation/verbs.mjs +1 -1
  40. package/esm/client/navigation/verbs.mjs.map +1 -1
  41. package/esm/client/runtime/index.d.mts +2 -1
  42. package/esm/client/runtime/index.mjs +2 -1
  43. package/esm/client/runtime/manifest.d.mts +1 -1
  44. package/esm/client/runtime/manifest.mjs +3 -2
  45. package/esm/client/runtime/manifest.mjs.map +1 -1
  46. package/esm/client/runtime/matcher.d.mts +13 -1
  47. package/esm/client/runtime/matcher.mjs +13 -1
  48. package/esm/client/runtime/matcher.mjs.map +1 -1
  49. package/esm/client/runtime/types.d.mts +15 -2
  50. package/esm/components/default-app.mjs +1 -1
  51. package/esm/components/default-app.mjs.map +1 -1
  52. package/esm/components/document-context.d.mts +40 -2
  53. package/esm/components/document-context.mjs +1 -1
  54. package/esm/components/document-context.mjs.map +1 -1
  55. package/esm/components/head.d.mts +1 -1
  56. package/esm/components/head.mjs +1 -1
  57. package/esm/components/head.mjs.map +1 -1
  58. package/esm/components/link.d.mts +1 -1
  59. package/esm/components/link.mjs +1 -1
  60. package/esm/components/link.mjs.map +1 -1
  61. package/esm/components/scripts.d.mts +1 -1
  62. package/esm/components/scripts.mjs +4 -2
  63. package/esm/components/scripts.mjs.map +1 -1
  64. package/esm/context.d.mts +6 -102
  65. package/esm/hydration/index.mjs +1 -1
  66. package/esm/hydration/index.mjs.map +1 -1
  67. package/esm/hydration-payload.d.mts +1 -1
  68. package/esm/hydration-payload.mjs +37 -8
  69. package/esm/hydration-payload.mjs.map +1 -1
  70. package/esm/index.d.mts +4 -3
  71. package/esm/loaders.d.mts +7 -36
  72. package/esm/metadata.d.mts +15 -1
  73. package/esm/metadata.mjs +45 -0
  74. package/esm/metadata.mjs.map +1 -0
  75. package/esm/props.d.mts +16 -8
  76. package/esm/route.d.mts +2 -17
  77. package/esm/routing/compose-route-path.d.mts +1 -1
  78. package/esm/routing/compose-route-path.mjs +1 -1
  79. package/esm/routing/compose-route-path.mjs.map +1 -1
  80. package/esm/routing/data-request.mjs +1 -1
  81. package/esm/routing/data-request.mjs.map +1 -1
  82. package/esm/routing/filesystem-route.mjs +45 -0
  83. package/esm/routing/filesystem-route.mjs.map +1 -0
  84. package/esm/routing/layout-policy.mjs +1 -1
  85. package/esm/routing/layout-policy.mjs.map +1 -1
  86. package/esm/routing/navigator.mjs +1 -1
  87. package/esm/routing/navigator.mjs.map +1 -1
  88. package/esm/routing/query-string.d.mts +1 -1
  89. package/esm/routing/query-string.mjs +1 -1
  90. package/esm/routing/query-string.mjs.map +1 -1
  91. package/esm/routing/route-identity.mjs +1 -1
  92. package/esm/routing/route-identity.mjs.map +1 -1
  93. package/esm/routing/route-table.d.mts +1 -1
  94. package/esm/routing/route-table.mjs +1 -1
  95. package/esm/routing/route-table.mjs.map +1 -1
  96. package/esm/routing/router-events.d.mts +1 -1
  97. package/esm/routing/router-events.mjs +1 -1
  98. package/esm/routing/router-events.mjs.map +1 -1
  99. package/esm/routing/url-fragment.mjs +120 -0
  100. package/esm/routing/url-fragment.mjs.map +1 -0
  101. package/esm/runtime/register-modules.d.mts +19 -0
  102. package/esm/runtime/register-modules.mjs +22 -0
  103. package/esm/runtime/register-modules.mjs.map +1 -0
  104. package/esm/server/build-hydration-payload.mjs +2 -1
  105. package/esm/server/build-hydration-payload.mjs.map +1 -1
  106. package/esm/server/client-asset-url-prefix.d.mts +1 -1
  107. package/esm/server/client-asset-url-prefix.mjs +1 -1
  108. package/esm/server/client-asset-url-prefix.mjs.map +1 -1
  109. package/esm/server/create-page-module-loader.d.mts +1 -1
  110. package/esm/server/create-page-module-loader.mjs +2 -1
  111. package/esm/server/create-page-module-loader.mjs.map +1 -1
  112. package/esm/server/create-page-route-handler.d.mts +58 -6
  113. package/esm/server/create-page-route-handler.mjs +127 -40
  114. package/esm/server/create-page-route-handler.mjs.map +1 -1
  115. package/esm/server/dev-server.mjs +2 -13
  116. package/esm/server/dev-server.mjs.map +1 -1
  117. package/esm/server/error-page.d.mts +14 -0
  118. package/esm/server/error-page.mjs +39 -0
  119. package/esm/server/error-page.mjs.map +1 -0
  120. package/esm/server/execute-page-request.d.mts +7 -4
  121. package/esm/server/execute-page-request.mjs +79 -80
  122. package/esm/server/execute-page-request.mjs.map +1 -1
  123. package/esm/server/execute-page-request.types.d.mts +33 -92
  124. package/esm/server/hydration-client-url.d.mts +1 -1
  125. package/esm/server/hydration-client-url.mjs +2 -2
  126. package/esm/server/hydration-client-url.mjs.map +1 -1
  127. package/esm/server/index.d.mts +6 -5
  128. package/esm/server/index.mjs +4 -4
  129. package/esm/server/install-page-routes-from-manifest.d.mts +17 -7
  130. package/esm/server/install-page-routes-from-manifest.mjs +91 -8
  131. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  132. package/esm/server/install-page-routes.d.mts +32 -13
  133. package/esm/server/install-page-routes.mjs +112 -32
  134. package/esm/server/install-page-routes.mjs.map +1 -1
  135. package/esm/server/install-production-page-routes.mjs +3 -31
  136. package/esm/server/install-production-page-routes.mjs.map +1 -1
  137. package/esm/server/match-page-route.mjs +2 -2
  138. package/esm/server/match-page-route.mjs.map +1 -1
  139. package/esm/server/not-found-page.d.mts +126 -0
  140. package/esm/server/not-found-page.mjs +157 -0
  141. package/esm/server/not-found-page.mjs.map +1 -0
  142. package/esm/server/page-context.d.mts +3 -9
  143. package/esm/server/page-context.mjs +3 -16
  144. package/esm/server/page-context.mjs.map +1 -1
  145. package/esm/server/page-file-change.mjs +77 -0
  146. package/esm/server/page-file-change.mjs.map +1 -0
  147. package/esm/server/page-manifest.d.mts +25 -10
  148. package/esm/server/page-manifest.mjs +1 -1
  149. package/esm/server/page-manifest.mjs.map +1 -1
  150. package/esm/server/page-render-bundle.mjs +38 -0
  151. package/esm/server/page-render-bundle.mjs.map +1 -0
  152. package/esm/server/page-route-reload.mjs +54 -0
  153. package/esm/server/page-route-reload.mjs.map +1 -0
  154. package/esm/server/register-production-public-files.mjs +40 -0
  155. package/esm/server/register-production-public-files.mjs.map +1 -0
  156. package/esm/server/render-page.d.mts +11 -14
  157. package/esm/server/render-page.mjs +154 -28
  158. package/esm/server/render-page.mjs.map +1 -1
  159. package/esm/server/resolve-page-metadata.mjs +2 -2
  160. package/esm/server/resolve-page-metadata.mjs.map +1 -1
  161. package/esm/server/resolve-validation-data.mjs +1 -1
  162. package/esm/server/resolve-validation-data.mjs.map +1 -1
  163. package/esm/server/settle-page-response.d.mts +58 -1
  164. package/esm/server/settle-page-response.mjs +111 -46
  165. package/esm/server/settle-page-response.mjs.map +1 -1
  166. package/esm/server/stylesheet-urls.d.mts +41 -30
  167. package/esm/server/stylesheet-urls.mjs +137 -52
  168. package/esm/server/stylesheet-urls.mjs.map +1 -1
  169. package/esm/server/unregistered-pages.mjs +42 -0
  170. package/esm/server/unregistered-pages.mjs.map +1 -0
  171. package/esm/server/web-connector-factory.d.mts +2 -3
  172. package/esm/server/web-connector-factory.mjs +6 -4
  173. package/esm/server/web-connector-factory.mjs.map +1 -1
  174. package/esm/server/web-connector.d.mts +1 -1
  175. package/esm/server/web-connector.mjs +233 -18
  176. package/esm/server/web-connector.mjs.map +1 -1
  177. package/esm/shared.d.mts +18 -3
  178. package/esm/shared.mjs +3 -7
  179. package/esm/shared.mjs.map +1 -1
  180. package/esm/validation.d.mts +1 -1
  181. package/esm/vite/app-convention-aliases.mjs +1 -1
  182. package/esm/vite/app-convention-aliases.mjs.map +1 -1
  183. package/esm/vite/build-client.d.mts +1 -1
  184. package/esm/vite/build-client.mjs +2 -2
  185. package/esm/vite/build-client.mjs.map +1 -1
  186. package/esm/vite/gate-a-resolve.d.mts +1 -1
  187. package/esm/vite/gate-a-resolve.mjs +67 -13
  188. package/esm/vite/gate-a-resolve.mjs.map +1 -1
  189. package/esm/vite/gate-b-secrets.d.mts +1 -1
  190. package/esm/vite/gate-b-secrets.mjs +35 -17
  191. package/esm/vite/gate-b-secrets.mjs.map +1 -1
  192. package/esm/vite/gate-c-verify.d.mts +1 -1
  193. package/esm/vite/gate-c-verify.mjs +6 -6
  194. package/esm/vite/gate-c-verify.mjs.map +1 -1
  195. package/esm/vite/hydration-entries.d.mts +1 -1
  196. package/esm/vite/hydration-entries.mjs +10 -6
  197. package/esm/vite/hydration-entries.mjs.map +1 -1
  198. package/esm/vite/index.d.mts +9 -6
  199. package/esm/vite/index.mjs +124 -11
  200. package/esm/vite/index.mjs.map +1 -1
  201. package/esm/vite/page-registry-plugin.d.mts +22 -3
  202. package/esm/vite/page-registry-plugin.mjs +247 -2
  203. package/esm/vite/page-registry-plugin.mjs.map +1 -1
  204. package/esm/vite/projection.d.mts +2 -2
  205. package/esm/vite/projection.mjs +38 -15
  206. package/esm/vite/projection.mjs.map +1 -1
  207. package/llms-full.txt +241 -49
  208. package/llms.txt +6 -2
  209. package/package.json +3 -3
  210. package/skills/add-web-to-an-app/SKILL.md +3 -3
  211. package/skills/create-a-page/SKILL.md +130 -7
  212. package/skills/load-page-data/SKILL.md +92 -12
  213. package/skills/navigate-on-the-client/SKILL.md +3 -1
  214. package/skills/serve-styles/SKILL.md +13 -12
  215. package/skills/use-layouts/SKILL.md +20 -8
  216. package/skills/write-the-root/SKILL.md +5 -1
  217. package/esm/server/buffered-response.d.mts +0 -58
  218. package/esm/server/buffered-response.mjs +0 -115
  219. package/esm/server/buffered-response.mjs.map +0 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,256 @@
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
+
179
+ ## 5.1.0
180
+
181
+ > **Upgrade if you installed 5.0.0, 5.0.1 or 5.0.2.** React did not execute at all in
182
+ > a published install of any of them — see the first entry under *Fixed*. Every
183
+ > interactive page shipped on those versions was inert in the browser.
184
+
185
+ ### Added
186
+
187
+ - **`404.page.tsx`** — an app-owned not-found page. It renders only when `text/html`
188
+ is explicitly present in the request's `Accept` header, so an unmatched `/api/...`
189
+ path still returns the JSON 404 an API client expects rather than a document. It
190
+ renders with no layouts: discovery reports an empty layout chain for this page only,
191
+ so the client hydration registry matches what the server has always rendered instead
192
+ of wrapping a failure page in chrome that can itself throw or need data. Ordinary
193
+ pages beside it keep their full layout chain, and nested-layout refusal on its path
194
+ is unchanged.
195
+ - **`export const metadata` is typed (`PageMetadata`) and checked at build time.** An
196
+ unannotated object literal with a misspelled key — `{ tittle: "x" }` — now fails the
197
+ build, naming the file, the line and the offending key. It previously typechecked as
198
+ a plain object and was silently ignored at runtime.
199
+ - **Fast Refresh in dev now applies only when an edit is confined to component
200
+ bodies.** Any module-level change — an import, a module-level declaration, or any
201
+ server export, `metadata` included — forces a full page reload instead of a stale hot
202
+ update; a JSX-only edit still hot-updates in place with component state intact.
203
+
204
+ ### Changed
205
+
206
+ - **`warlock dev` now refuses a `*.page.tsx` that exports no route**, throwing
207
+ `MissingRouteExportError` and naming the file. It previously 404'd silently, so a
208
+ missing `export const route` looked like a routing bug at request time. This matches
209
+ what `warlock build` already did — dev and build now reject the same file.
210
+
211
+ ### Removed
212
+
213
+ - **A false comment shipped in 5.0.0 through 5.0.2** claiming that a page's route is
214
+ derived from its file location. No such derivation has ever existed in this package;
215
+ the route comes from the page's `route` export and nothing else. The comment is gone
216
+ from the scaffold emitted by `warlock add web`, but **every app scaffolded on 5.0.0,
217
+ 5.0.1 or 5.0.2 still carries it in its own source** — delete it by hand.
218
+
219
+ ### Fixed
220
+
221
+ - **React did not run at all in published installs of 5.0.0 through 5.0.2.** The dev
222
+ Vite server served `react-dom/client` as raw CJS, so `hydrateRoot` did not exist and
223
+ the hydration module threw while being parsed. This one defect is the cause of all
224
+ four symptoms reported against those versions: `useState` never updated, Fast Refresh
225
+ never ran, metadata never refreshed, and `<Link>` fell back to a full page reload.
226
+ Fixed by declaring the React entries in the dev server's `optimizeDeps` so they are
227
+ pre-bundled to ESM before the browser asks for them. This is not a hydration
228
+ *improvement* — hydration did not happen.
229
+ - **The browser was loading two copies of every `@warlock.js/web` client module.**
230
+ Module-level state (context, the navigation runtime) existed twice, so a value written
231
+ through one copy was invisible to the component reading the other.
232
+
233
+ ## 5.0.2 - 2026-08-25
234
+
235
+ ### Fixed
236
+
237
+ - **`<Head/>` read an empty document context under SSR.** The connector now sets
238
+ `ssr.noExternal: ["@warlock.js/web"]` in `web-connector.ts`. Without it the server
239
+ loaded two instances of this package — one externalised, one bundled — so the context
240
+ the renderer wrote to was not the one `<Head/>` read from. A published 5.0.1 install
241
+ that returned 500 on a page request returns 200 after this fix.
242
+
243
+ ## 5.0.1 - 2026-08-25
244
+
245
+ ### Changed
246
+
247
+ - Narrowed the `vite` peer dependency to `">=7.3.5 <8"`, so a consumer resolving vite
248
+ for this package cannot land on a version outside the range it is built against.
249
+
250
+ ### Fixed
251
+
252
+ - Internal: a test in `gate-b-secrets.spec.ts` depended on the ambient `NODE_ENV` and
253
+ failed depending on how the suite was invoked. No runtime behaviour changed.
254
+
5
255
  ## 5.0.0 - 2026-08-25
6
256
 
7
257
  **First published release.**
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
  /**
@@ -44,12 +45,18 @@ declare class WebPackageRootResolutionError extends Error {
44
45
  *
45
46
  * `generate` writes the page barrel into `ctx.productionDir` and patches
46
47
  * esbuild; `emit` produces the client bundle esbuild cannot. The two share one
47
- * closure fact — how many pages exist because a build with zero pages must
48
- * 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
49
51
  * NOT skip: the barrel and its entry import are emitted either way, so the
50
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.
51
58
  */
52
- declare function createWebBuildContribution(options?: WebBuildOptions): ConnectorBuildContribution;
59
+ declare function createWebBuildContribution(options?: WebBuildOptions, connectorPlugins?: readonly PluginOption[]): ConnectorBuildContribution;
53
60
  //#endregion
54
61
  export { WebBuildOptions, WebPackageRootResolutionError, createWebBuildContribution };
55
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.
@@ -86,43 +86,67 @@ async function resolveWebPackageRoot(configured) {
86
86
  *
87
87
  * `generate` writes the page barrel into `ctx.productionDir` and patches
88
88
  * esbuild; `emit` produces the client bundle esbuild cannot. The two share one
89
- * closure fact — how many pages exist because a build with zero pages must
90
- * 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
91
92
  * NOT skip: the barrel and its entry import are emitted either way, so the
92
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.
93
99
  */
94
- function createWebBuildContribution(options = {}) {
100
+ function createWebBuildContribution(options = {}, connectorPlugins = []) {
95
101
  if (options.clientOutDir !== void 0) throw new ClientOutDirNotSupportedError();
96
102
  let pageCount = 0;
103
+ let pageRoutes = {
104
+ version: 1,
105
+ routes: []
106
+ };
107
+ let publicFiles = [];
97
108
  return {
98
109
  async generate(context) {
99
110
  const { generatePagesBarrel, WEB_ENTRY_IMPORT, WEB_ESBUILD_PATCH } = await import("./generate-pages-barrel.mjs");
100
- 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({
101
114
  appRoot: context.appRoot,
102
115
  productionDir: context.productionDir,
103
116
  srcDir: options.srcDir,
104
- clientDir: path.relative(context.appRoot, resolveClientOutDir(context)).split(path.sep).join("/")
105
- })).pageCount;
117
+ clientDir: path.relative(context.appRoot, resolveClientOutDir(context)).split(path.sep).join("/"),
118
+ publicFiles
119
+ });
120
+ pageCount = result.pageCount;
121
+ pageRoutes = result.pageRoutes;
106
122
  return {
107
123
  entryImports: [WEB_ENTRY_IMPORT],
108
124
  esbuild: WEB_ESBUILD_PATCH
109
125
  };
110
126
  },
111
127
  async emit(context) {
112
- if (pageCount === 0) return;
113
- const { buildWarlockHydrationClient } = await import("../vite/index.mjs");
114
- const { appConventionAliases } = await import("../vite/app-convention-aliases.mjs");
115
- const callerAliases = Object.entries(options.aliases ?? {}).map(([find, replacement]) => ({
116
- find,
117
- replacement
118
- }));
119
- await buildWarlockHydrationClient({
120
- appRoot: context.appRoot,
121
- webRoot: await resolveWebPackageRoot(options.webRoot),
122
- outDir: resolveClientOutDir(context),
123
- resolveAliases: [...callerAliases, ...appConventionAliases(path.join(context.appRoot, options.srcDir ?? "src"))],
124
- external: options.external
125
- });
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);
126
150
  }
127
151
  };
128
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\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/** 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 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":";;;;;;;;;;;;;;;;;;;AAsDA,SAAS,oBAAoB,SAAwC;CACnE,OAAO,KAAK,QAAQ,QAAQ,SAAS,QAAQ,QAAQ,QAAQ,QAAQ;AACvE;;;;;;;;;;;;AAaA,IAAa,gCAAb,cAAmD,MAAM;CACvD,AAAO,cAAc;EACnB,MACE,6RAIF;EACA,KAAK,OAAO;CACd;AACF;;AAGA,MAAM,mBAAmB;;;;;;;;;AAUzB,IAAa,gCAAb,cAAmD,MAAM;CACvD,AAAO,YAAY,SAAiB,QAAgB,OAAiB;EACnE,MACE,uBAAuB,iBAAiB,mBAAmB,QAAQ,IAAI,OAAO,2HAEpC,iBAAiB,KAC3D,EAAE,MAAM,CACV;EACA,KAAK,OAAO;CACd;AACF;;;;;AAMA,SAAgB,qBAAqB,SAAyB;CAC5D,MAAM,eAAe,KAAK,KAAK,SAAS,cAAc;CAEtD,IAAI;CAEJ,IAAI;EACF,MAAM,aAAa,cAAc,OAAO;CAC1C,SAAS,OAAO;EACd,MAAM,IAAI,8BAA8B,SAAS,gCAAgC,KAAK;CACxF;CAEA,IAAI;CAEJ,IAAI;EACF,SAAS,KAAK,MAAM,GAAG;CACzB,SAAS,OAAO;EACd,MAAM,IAAI,8BACR,SACA,6CACA,KACF;CACF;CAEA,IAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,MAAM,GACvE,MAAM,IAAI,8BACR,SACA,oDACF;CAGF,MAAM,OAAQ,OAA8B;CAE5C,IAAI,SAAS,kBACX,MAAM,IAAI,8BACR,SACA,yBAAyB,KAAK,UAAU,IAAI,EAAE,SAAS,iBAAiB,EAC1E;CAGF,OAAO;AACT;AAEA,eAAsB,sBAAsB,YAAiD;CAC3F,IAAI,eAAe,QACjB,OAAO,qBAAqB,KAAK,QAAQ,UAAU,CAAC;CAGtD,MAAM,EAAE,kBAAkB,MAAM,OAAO;CAIvC,OAAO,qBACL,KAAK,QAAQ,KAAK,QAAQ,cAAc,OAAO,KAAK,GAAG,CAAC,GAAG,MAAM,IAAI,CACvE;AACF;;;;;;;;;;;AAYA,SAAgB,2BACd,UAA2B,CAAC,GACA;CAC5B,IAAI,QAAQ,iBAAiB,QAC3B,MAAM,IAAI,8BAA8B;CAG1C,IAAI,YAAY;CAEhB,OAAO;EACL,MAAM,SAAS,SAA8E;GAC3F,MAAM,EAAE,qBAAqB,kBAAkB,sBAAsB,MAAM,OACzE;GAoBF,aAAY,MAjBS,oBAAoB;IACvC,SAAS,QAAQ;IACjB,eAAe,QAAQ;IACvB,QAAQ,QAAQ;IAQhB,WAAW,KACR,SAAS,QAAQ,SAAS,oBAAoB,OAAO,CAAC,CAAC,CACvD,MAAM,KAAK,GAAG,CAAC,CACf,KAAK,GAAG;GACb,CAAC,EAEiB,CAAC;GAOnB,OAAO;IAAE,cAAc,CAAC,gBAAgB;IAAG,SAAS;GAAkB;EACxE;EAEA,MAAM,KAAK,SAA+C;GACxD,IAAI,cAAc,GAChB;GAGF,MAAM,EAAE,gCAAgC,MAAM,OAAO;GACrD,MAAM,EAAE,yBAAyB,MAAM,OAAO;GAM9C,MAAM,gBAAgB,OAAO,QAAQ,QAAQ,WAAW,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,kBAAkB;IACxF;IACA;GACF,EAAE;GAEF,MAAM,4BAA4B;IAChC,SAAS,QAAQ;IACjB,SAAS,MAAM,sBAAsB,QAAQ,OAAO;IACpD,QAAQ,oBAAoB,OAAO;IACnC,gBAAgB,CACd,GAAG,eACH,GAAG,qBAAqB,KAAK,KAAK,QAAQ,SAAS,QAAQ,UAAU,KAAK,CAAC,CAC7E;IACA,UAAU,QAAQ;GACpB,CAAC;EACH;CACF;AACF"}
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":";;;;;;;;;;;;;;;;;;;AAuDA,SAAS,oBAAoB,SAAwC;CACnE,OAAO,KAAK,QAAQ,QAAQ,SAAS,QAAQ,QAAQ,QAAQ,QAAQ;AACvE;;;;;;;;;;;;AAaA,IAAa,gCAAb,cAAmD,MAAM;CACvD,AAAO,cAAc;EACnB,MACE,6RAIF;EACA,KAAK,OAAO;CACd;AACF;;AAGA,MAAM,mBAAmB;;;;;;;;;AAUzB,IAAa,gCAAb,cAAmD,MAAM;CACvD,AAAO,YAAY,SAAiB,QAAgB,OAAiB;EACnE,MACE,uBAAuB,iBAAiB,mBAAmB,QAAQ,IAAI,OAAO,2HAEpC,iBAAiB,KAC3D,EAAE,MAAM,CACV;EACA,KAAK,OAAO;CACd;AACF;;;;;AAMA,SAAgB,qBAAqB,SAAyB;CAC5D,MAAM,eAAe,KAAK,KAAK,SAAS,cAAc;CAEtD,IAAI;CAEJ,IAAI;EACF,MAAM,aAAa,cAAc,OAAO;CAC1C,SAAS,OAAO;EACd,MAAM,IAAI,8BAA8B,SAAS,gCAAgC,KAAK;CACxF;CAEA,IAAI;CAEJ,IAAI;EACF,SAAS,KAAK,MAAM,GAAG;CACzB,SAAS,OAAO;EACd,MAAM,IAAI,8BACR,SACA,6CACA,KACF;CACF;CAEA,IAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,MAAM,GACvE,MAAM,IAAI,8BACR,SACA,oDACF;CAGF,MAAM,OAAQ,OAA8B;CAE5C,IAAI,SAAS,kBACX,MAAM,IAAI,8BACR,SACA,yBAAyB,KAAK,UAAU,IAAI,EAAE,SAAS,iBAAiB,EAC1E;CAGF,OAAO;AACT;AAEA,eAAsB,sBAAsB,YAAiD;CAC3F,IAAI,eAAe,QACjB,OAAO,qBAAqB,KAAK,QAAQ,UAAU,CAAC;CAGtD,MAAM,EAAE,kBAAkB,MAAM,OAAO;CAIvC,OAAO,qBACL,KAAK,QAAQ,KAAK,QAAQ,cAAc,OAAO,KAAK,GAAG,CAAC,GAAG,MAAM,IAAI,CACvE;AACF;;;;;;;;;;;;;;;;;AAkBA,SAAgB,2BACd,UAA2B,CAAC,GAC5B,mBAA4C,CAAC,GACjB;CAC5B,IAAI,QAAQ,iBAAiB,QAC3B,MAAM,IAAI,8BAA8B;CAG1C,IAAI,YAAY;CAChB,IAAI,aAAmE;EACrE,SAAS;EACT,QAAQ,CAAC;CACX;CACA,IAAI,cAAwB,CAAC;CAE7B,OAAO;EACL,MAAM,SAAS,SAA8E;GAC3F,MAAM,EAAE,qBAAqB,kBAAkB,sBAC7C,MAAM,OAAO;GACf,MAAM,EAAE,uBAAuB,MAAM,OAAO;GAE5C,cAAc,MAAM,mBAAmB,KAAK,KAAK,QAAQ,SAAS,QAAQ,CAAC;GAE3E,MAAM,SAAS,MAAM,oBAAoB;IACvC,SAAS,QAAQ;IACjB,eAAe,QAAQ;IACvB,QAAQ,QAAQ;IAQhB,WAAW,KACR,SAAS,QAAQ,SAAS,oBAAoB,OAAO,CAAC,EACtD,MAAM,KAAK,GAAG,EACd,KAAK,GAAG;IACX;GACF,CAAC;GAED,YAAY,OAAO;GACnB,aAAa,OAAO;GAOpB,OAAO;IAAE,cAAc,CAAC,gBAAgB;IAAG,SAAS;GAAkB;EACxE;EAEA,MAAM,KAAK,SAA+C;GACxD,IAAI,YAAY,GAAG;IACjB,MAAM,EAAE,gCAAgC,MAAM,OAAO;IACrD,MAAM,EAAE,yBAAyB,MAAM,OAAO;IAM9C,MAAM,gBAAgB,OAAO,QAAQ,QAAQ,WAAW,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,kBAAkB;KACxF;KACA;IACF,EAAE;IAEF,MAAM,4BAA4B;KAChC,SAAS,QAAQ;KACjB,SAAS,MAAM,sBAAsB,QAAQ,OAAO;KACpD,QAAQ,oBAAoB,OAAO;KACnC,gBAAgB,CACd,GAAG,eACH,GAAG,qBAAqB,KAAK,KAAK,QAAQ,SAAS,QAAQ,UAAU,KAAK,CAAC,CAC7E;KACA,UAAU,QAAQ;KAClB,SAAS;IACX,CAAC;GACH;GAEA,IAAI,YAAY,SAAS,GAAG;IAC1B,MAAM,EAAE,oBAAoB,MAAM,OAAO;IACzC,MAAM,gBACJ,KAAK,KAAK,QAAQ,SAAS,QAAQ,GACnC,KAAK,KAAK,oBAAoB,OAAO,GAAG,QAAQ,GAChD,WACF;GACF;GAEA,MAAM,EAAE,4BAA4B,MAAM,OAAO;GACjD,MAAM,wBACJ,KAAK,QAAQ,QAAQ,SAAS,QAAQ,QAAQ,MAAM,GACpD,UACF;EACF;CACF;AACF"}