@warlock.js/web 5.1.0 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/CHANGELOG.md +182 -0
  2. package/README.md +126 -12
  3. package/esm/build/contribution.d.mts +10 -13
  4. package/esm/build/contribution.mjs +45 -57
  5. package/esm/build/contribution.mjs.map +1 -1
  6. package/esm/build/discover-pages.mjs +104 -44
  7. package/esm/build/discover-pages.mjs.map +1 -1
  8. package/esm/build/generate-client-registry.mjs +46 -12
  9. package/esm/build/generate-client-registry.mjs.map +1 -1
  10. package/esm/build/generate-pages-barrel.mjs +54 -12
  11. package/esm/build/generate-pages-barrel.mjs.map +1 -1
  12. package/esm/build/page-default-export.mjs +48 -0
  13. package/esm/build/page-default-export.mjs.map +1 -0
  14. package/esm/build/page-routes-manifest.mjs +21 -0
  15. package/esm/build/page-routes-manifest.mjs.map +1 -0
  16. package/esm/build/public-files.mjs +45 -0
  17. package/esm/build/public-files.mjs.map +1 -0
  18. package/esm/build/read-route-exports.mjs.map +1 -1
  19. package/esm/client/build-hydrated-tree.mjs +39 -8
  20. package/esm/client/build-hydrated-tree.mjs.map +1 -1
  21. package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
  22. package/esm/client/navigation/prefetch.mjs.map +1 -1
  23. package/esm/client/runtime/index.d.mts +2 -1
  24. package/esm/client/runtime/index.mjs +2 -1
  25. package/esm/client/runtime/manifest.mjs +2 -1
  26. package/esm/client/runtime/manifest.mjs.map +1 -1
  27. package/esm/client/runtime/matcher.d.mts +12 -0
  28. package/esm/client/runtime/matcher.mjs +12 -0
  29. package/esm/client/runtime/matcher.mjs.map +1 -1
  30. package/esm/client/runtime/types.d.mts +14 -1
  31. package/esm/components/document-context.d.mts +39 -1
  32. package/esm/components/document-context.mjs.map +1 -1
  33. package/esm/components/link.mjs.map +1 -1
  34. package/esm/components/scripts.mjs +3 -1
  35. package/esm/components/scripts.mjs.map +1 -1
  36. package/esm/context.d.mts +5 -101
  37. package/esm/hydration-payload.d.mts +1 -1
  38. package/esm/hydration-payload.mjs +36 -7
  39. package/esm/hydration-payload.mjs.map +1 -1
  40. package/esm/index.d.mts +3 -2
  41. package/esm/loaders.d.mts +6 -35
  42. package/esm/props.d.mts +15 -7
  43. package/esm/route.d.mts +1 -16
  44. package/esm/routing/filesystem-route.mjs +45 -0
  45. package/esm/routing/filesystem-route.mjs.map +1 -0
  46. package/esm/routing/layout-policy.mjs.map +1 -1
  47. package/esm/routing/query-string.mjs.map +1 -1
  48. package/esm/routing/route-table.mjs.map +1 -1
  49. package/esm/runtime/register-modules.d.mts +19 -0
  50. package/esm/runtime/register-modules.mjs +22 -0
  51. package/esm/runtime/register-modules.mjs.map +1 -0
  52. package/esm/server/build-hydration-payload.mjs +1 -0
  53. package/esm/server/build-hydration-payload.mjs.map +1 -1
  54. package/esm/server/create-page-module-loader.mjs +1 -0
  55. package/esm/server/create-page-module-loader.mjs.map +1 -1
  56. package/esm/server/create-page-route-handler.d.mts +30 -5
  57. package/esm/server/create-page-route-handler.mjs +127 -42
  58. package/esm/server/create-page-route-handler.mjs.map +1 -1
  59. package/esm/server/dev-server.mjs +1 -12
  60. package/esm/server/dev-server.mjs.map +1 -1
  61. package/esm/server/error-page.d.mts +14 -0
  62. package/esm/server/error-page.mjs +39 -0
  63. package/esm/server/error-page.mjs.map +1 -0
  64. package/esm/server/execute-page-request.d.mts +6 -3
  65. package/esm/server/execute-page-request.mjs +78 -79
  66. package/esm/server/execute-page-request.mjs.map +1 -1
  67. package/esm/server/execute-page-request.types.d.mts +32 -91
  68. package/esm/server/hydration-client-url.mjs +1 -1
  69. package/esm/server/index.d.mts +5 -5
  70. package/esm/server/index.mjs +3 -4
  71. package/esm/server/install-page-routes-from-manifest.d.mts +16 -6
  72. package/esm/server/install-page-routes-from-manifest.mjs +70 -9
  73. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  74. package/esm/server/install-page-routes.d.mts +31 -14
  75. package/esm/server/install-page-routes.mjs +90 -38
  76. package/esm/server/install-page-routes.mjs.map +1 -1
  77. package/esm/server/install-production-page-routes.mjs +2 -35
  78. package/esm/server/install-production-page-routes.mjs.map +1 -1
  79. package/esm/server/match-page-route.mjs +1 -1
  80. package/esm/server/match-page-route.mjs.map +1 -1
  81. package/esm/server/not-found-page.mjs +1 -1
  82. package/esm/server/not-found-page.mjs.map +1 -1
  83. package/esm/server/page-context.d.mts +2 -8
  84. package/esm/server/page-context.mjs +2 -15
  85. package/esm/server/page-context.mjs.map +1 -1
  86. package/esm/server/page-file-change.mjs +77 -0
  87. package/esm/server/page-file-change.mjs.map +1 -0
  88. package/esm/server/page-manifest.d.mts +24 -9
  89. package/esm/server/page-manifest.mjs.map +1 -1
  90. package/esm/server/page-render-bundle.mjs +38 -0
  91. package/esm/server/page-render-bundle.mjs.map +1 -0
  92. package/esm/server/page-route-reload.mjs +54 -0
  93. package/esm/server/page-route-reload.mjs.map +1 -0
  94. package/esm/server/register-production-public-files.mjs +40 -0
  95. package/esm/server/register-production-public-files.mjs.map +1 -0
  96. package/esm/server/render-page.d.mts +10 -13
  97. package/esm/server/render-page.mjs +153 -27
  98. package/esm/server/render-page.mjs.map +1 -1
  99. package/esm/server/resolve-page-metadata.mjs +1 -1
  100. package/esm/server/resolve-page-metadata.mjs.map +1 -1
  101. package/esm/server/settle-page-response.d.mts +58 -1
  102. package/esm/server/settle-page-response.mjs +110 -45
  103. package/esm/server/settle-page-response.mjs.map +1 -1
  104. package/esm/server/stylesheet-urls.d.mts +40 -29
  105. package/esm/server/stylesheet-urls.mjs +136 -51
  106. package/esm/server/stylesheet-urls.mjs.map +1 -1
  107. package/esm/server/unregistered-pages.mjs +42 -0
  108. package/esm/server/unregistered-pages.mjs.map +1 -0
  109. package/esm/server/web-connector-factory.d.mts +1 -2
  110. package/esm/server/web-connector-factory.mjs +6 -5
  111. package/esm/server/web-connector-factory.mjs.map +1 -1
  112. package/esm/server/web-connector.mjs +137 -17
  113. package/esm/server/web-connector.mjs.map +1 -1
  114. package/esm/shared.d.mts +17 -2
  115. package/esm/shared.mjs +2 -6
  116. package/esm/shared.mjs.map +1 -1
  117. package/esm/vite/build-client.mjs +1 -1
  118. package/esm/vite/build-client.mjs.map +1 -1
  119. package/esm/vite/gate-a-resolve.mjs +66 -12
  120. package/esm/vite/gate-a-resolve.mjs.map +1 -1
  121. package/esm/vite/gate-b-secrets.mjs +34 -16
  122. package/esm/vite/gate-b-secrets.mjs.map +1 -1
  123. package/esm/vite/gate-c-verify.mjs +5 -5
  124. package/esm/vite/gate-c-verify.mjs.map +1 -1
  125. package/esm/vite/hydration-entries.mjs +1 -1
  126. package/esm/vite/hydration-entries.mjs.map +1 -1
  127. package/esm/vite/index.d.mts +8 -5
  128. package/esm/vite/index.mjs +123 -10
  129. package/esm/vite/index.mjs.map +1 -1
  130. package/esm/vite/page-registry-plugin.d.mts +21 -2
  131. package/esm/vite/page-registry-plugin.mjs +47 -13
  132. package/esm/vite/page-registry-plugin.mjs.map +1 -1
  133. package/esm/vite/projection.d.mts +1 -1
  134. package/esm/vite/projection.mjs +37 -14
  135. package/esm/vite/projection.mjs.map +1 -1
  136. package/llms-full.txt +220 -56
  137. package/llms.txt +5 -3
  138. package/package.json +3 -3
  139. package/skills/add-web-to-an-app/SKILL.md +3 -3
  140. package/skills/create-a-page/SKILL.md +117 -10
  141. package/skills/load-page-data/SKILL.md +92 -12
  142. package/skills/navigate-on-the-client/SKILL.md +1 -1
  143. package/skills/serve-styles/SKILL.md +13 -12
  144. package/skills/use-layouts/SKILL.md +14 -8
  145. package/skills/write-the-root/SKILL.md +3 -1
  146. package/esm/server/buffered-response.d.mts +0 -58
  147. package/esm/server/buffered-response.mjs +0 -115
  148. package/esm/server/buffered-response.mjs.map +0 -1
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  name: create-a-page
3
- description: 'Create an SSR React page under `src/web/**` or `src/app/<module>/web/**` with a literal `route`, a default component, an optional typed `loader`, and page `metadata`. Triggers: `*.page.tsx`, `route`, `PageLoader`, `PageProps`, `PageMetadata`; "create a page", "add an SSR route", "make a React page", "type page loader data"; typical import `import type { PageLoader, PageProps } from "@warlock.js/web"`. Skip: root document shell — `@warlock.js/web/write-the-root/SKILL.md`; layout wrappers and prefixes — `@warlock.js/web/use-layouts/SKILL.md`; loader lifecycle and `shared` — `@warlock.js/web/load-page-data/SKILL.md`; competing frameworks `next`, `remix`, `react-router` file routes.'
3
+ description: 'Create an SSR React page under `src/web/**`, with either a literal `route` or a filesystem-derived one, a default component, an optional typed `loader`, page `metadata`, the `error.page.tsx` boundary, and the universal `register()` hook. Triggers: `*.page.tsx`, `route`, `PageLoader`, `PageProps`, `PageMetadata`, `error.page.tsx`, `register`, `[...slug]`; "create a page", "add an SSR route", "make a React page", "type page loader data", "add an error boundary", "catch-all route", "page renders blank 200", "page has no default export"; typical import `import type { PageLoader, PageProps } from "@warlock.js/web"`. Skip: root document shell — `@warlock.js/web/write-the-root/SKILL.md`; layout wrappers and prefixes — `@warlock.js/web/use-layouts/SKILL.md`; loader lifecycle and `shared` — `@warlock.js/web/load-page-data/SKILL.md`; competing frameworks `next`, `remix`, `react-router` file routes.'
4
4
  ---
5
5
 
6
6
  # Warlock — create a page
7
7
 
8
- A page is any `*.page.tsx` beneath the global `src/web/` tree or a module's `src/app/<module>/web/` tree. Its URL is declared by `route`; its default export renders React.
8
+ A page is any `*.page.tsx` beneath `src/web/` the only page root. (A per-module `src/app/<module>/web/` tree is not scanned; move any page that lived there into `src/web/`, a subdirectory if you like.) Its URL is either a declared `route` or one derived from its own location; its default export renders React.
9
9
 
10
10
  ## The shape
11
11
 
12
- ```tsx title="src/app/products/web/product-details.page.tsx"
12
+ ```tsx title="src/web/products/product-details.page.tsx"
13
13
  import type { PageLoader, PageMetadata, PageProps } from "@warlock.js/web";
14
14
 
15
15
  export const route = {
@@ -62,9 +62,28 @@ export default function ContactPage() {
62
62
  }
63
63
  ```
64
64
 
65
- The `route` export is required, and a page without one is **refused, not skipped**. `warlock dev` and the production discovery pass throw the same `MissingRouteExportError`, naming the file, because a page with no route is a page nothing can ever reach.
65
+ `route` is optional. A page that omits it derives both its path and its name from where the file sits beneath `src/web` ([filesystem routing](#filesystem-routing), below). A page that declares `route` uses that instead — an explicit `route` always wins over the derived one, for both the path and (when it sets `name`) the name.
66
66
 
67
- Changed in 5.1: through 5.0.2 the dev server silently skipped a route-less page, so the file you had just written 404'd with nothing said. Dev and build now reach the same verdict from the same condition.
67
+ Changed in 5.2: through 5.1 an omitted `route` was refused `warlock dev` and the production discovery pass threw `MissingRouteExportError`, naming the file. That error class is gone; the same file now resolves to a real, browsable URL.
68
+
69
+ ### The default export is required
70
+
71
+ `route` is optional; the default export is not. A `*.page.tsx` that exports only named bindings is a **hard discovery/build failure naming the file**:
72
+
73
+ ```
74
+ The page "src/web/contact.page.tsx" has no runtime default export. Every `*.page.tsx`
75
+ file must default-export the React component it renders. For example:
76
+ `export default function Page() { return <main />; }`
77
+ ```
78
+
79
+ Changed in 5.2: such a file used to be discovered and registered anyway, then serve a blank `200` at its URL — a page that looked deployed and rendered nothing, with no error anywhere. It now stops the build.
80
+
81
+ Two details worth knowing:
82
+
83
+ - **`export { Page as default }` satisfies the rule** — the check is for a runtime default binding, not for the `export default` keyword form specifically.
84
+ - **`export default interface Page {}` does not.** A type-only default is erased at compile time, so there is no component at runtime; it is treated as missing.
85
+
86
+ A file that cannot be parsed reports separately — `Cannot inspect the default export of "<file>": the file could not be parsed (…). Fix the syntax error and discovery will continue.` — so a syntax error never masquerades as a missing export.
68
87
 
69
88
  ## Route declarations
70
89
 
@@ -81,18 +100,44 @@ export const route = {
81
100
  } as const;
82
101
  ```
83
102
 
84
- Prefer an explicit stable `name` for links. Without one, Warlock derives a name from the module and declared path: a module page gets `<module>.<path-as-dots>`, a global root page gets `index`, and another global page gets its dotted path.
103
+ Prefer an explicit stable `name` for links. Without one, Warlock derives a name from the declared path a global root page gets `index`, another global page gets its dotted path — the same derivation [filesystem routing](#filesystem-routing) uses when there is no `route` at all.
85
104
 
86
- **Only the route NAME is ever derived — never the route PATH.** There is no filename-to-URL convention in Warlock and there never has been. Omitting `route` does not fall back to the file's location; it fails (see above). Every segment of a page's URL is written down somewhere: the page's own `route.path`, prefixed by the literal `prefix` exports of the positional layouts above it ([use-layouts](../use-layouts/SKILL.md)). Where the file sits decides which layouts are above it — never what the path spells.
105
+ Every segment of a page's URL is written down somewhere: `route.path` (or the derived filesystem path), prefixed by the literal `prefix` exports of the positional layouts above it ([use-layouts](../use-layouts/SKILL.md)). Where the file sits always decides which layouts are above it, and only when `route` is absent — the path segments too.
87
106
 
88
107
  The build reads `route` without executing application code. Declare it directly with `export const` and literal strings. Variables, function calls, computed object keys, spreads, and `export { route }` are refused.
89
108
 
109
+ ## Filesystem routing
110
+
111
+ Omit `route` and the URL comes from the page's own path beneath `src/web`:
112
+
113
+ - Every directory contributes a segment, in order — `src/web/products/featured.page.tsx` derives `/products/featured`.
114
+ - A `(group)` directory — parentheses, not braces — contributes nothing to the URL, only to organization: `src/web/(marketing)/pricing.page.tsx` derives `/pricing`.
115
+ - `index.page.tsx` claims its own directory rather than adding a segment: `src/web/products/index.page.tsx` derives `/products`. This is the ONLY filename with special meaning — `home.page.tsx` is not magic and derives `/home`.
116
+ - `[id]` becomes `:id`: `src/web/products/[id].page.tsx` derives `/products/:id`.
117
+ - A layout `prefix` on the page's ancestry composes in front of the derived path exactly as it does for an explicit `route.path` ([use-layouts](../use-layouts/SKILL.md)).
118
+
119
+ Two pages that derive (or declare) the same effective path is a build error naming both files.
120
+
121
+ ### Catch-all segments are NOT supported
122
+
123
+ **`[...slug].page.tsx` does not do what it looks like.** There is no catch-all / rest-parameter form in filesystem routing. Only `[name]` — a plain identifier in square brackets — is recognized as a dynamic segment; `[...slug]` fails that pattern and is taken as an ordinary **literal** segment.
124
+
125
+ So `src/web/docs/[...slug].page.tsx` derives the route path `/docs/[...slug]` and the route name `docs.[...slug]`. It registers, it builds, and it serves — at the literal URL `/docs/%5B...slug%5D` and nowhere else. `/docs/a/b` does not match it.
126
+
127
+ ⚠ **Nothing warns you.** There is no build error, no dev warning, and no refusal; the page simply answers a URL nobody will ever request. If you came from Next.js or Remix expecting `[...slug]` to work, this is the failure mode to recognize.
128
+
129
+ Until a catch-all exists, use the terminal wildcard with an explicit `route`:
130
+
131
+ ```tsx
132
+ export const route = { path: "/docs/*", name: "docs.catchAll" } as const;
133
+ ```
134
+
90
135
  ## Page-route grammar
91
136
 
92
137
  Page routes deliberately accept less than API routes:
93
138
 
94
139
  - Supported: `/`, static segments, whole-segment params such as `/products/:id`, the exact wildcard `*`, and a terminal wildcard such as `/docs/*`.
95
- - Not supported: regex params, optional params, multiple params in one segment, params mixed with text, doubled or trailing slashes, and non-terminal wildcards.
140
+ - Not supported: regex params, optional params, multiple params in one segment, params mixed with text, doubled or trailing slashes, non-terminal wildcards, and catch-all/rest segments (`[...slug]`).
96
141
 
97
142
  Write two pages for an optional segment. Validate a constrained parameter in the page instead of putting a regex in its path.
98
143
 
@@ -116,6 +161,39 @@ export const metadata: PageMetadata = {
116
161
 
117
162
  Supported fields are `title`, `description`, `keywords`, `canonical`, `robots`, `openGraph`, and `twitter`. Function metadata runs after a successful loader. If a loader fails, Warlock uses error metadata instead of calling the page function with missing data.
118
163
 
164
+ ## The error boundary — `error.page.tsx`
165
+
166
+ `error.page.tsx` anywhere beneath `src/web` is the application's one error boundary — exactly two page filenames are special in Warlock, this and `404.page.tsx`. A second `error.page.tsx` is a build error naming both files. Like `404.page.tsx`, it declares no `route` — it has no URL of its own and is reached only when something throws.
167
+
168
+ ```tsx title="src/web/error.page.tsx"
169
+ import type { ErrorPageProps } from "@warlock.js/web";
170
+
171
+ export default function ErrorPage({ error, status }: ErrorPageProps) {
172
+ return (
173
+ <main>
174
+ <h1>Something went wrong</h1>
175
+ <p>Status: {status}</p>
176
+ </main>
177
+ );
178
+ }
179
+ ```
180
+
181
+ During SSR the component receives the real thrown value in `error`. After hydration it receives the JSON-safe `{ name, message, stack? }` shape instead — the original value does not survive the wire. `robots: noindex` is a framework default on this path that an app-supplied `metadata` cannot remove.
182
+
183
+ If the failure happens before any page module could even load — a module-load or `register()` throw — there is no trustworthy server composition left to hydrate against, so Warlock renders a plain framework fallback (your `error.page.tsx` if it can still be loaded, otherwise a minimal built-in boundary) with no hydration script at all rather than risk hydrating client code against markup nothing can vouch for.
184
+
185
+ ## The `register()` hook
186
+
187
+ `root.tsx`, `layout.tsx`, and `*.page.tsx` may each export `register`: a synchronous, no-argument, side-effect hook that runs once per module namespace instance, on both the server and the browser, before that module's middleware or loader. Unlike `route`/`middleware`/`validation`/`loader`/`metadata`, it is not stripped from the client — it is meant to run on both sides.
188
+
189
+ ```tsx
190
+ export function register() {
191
+ // one-time setup for this module namespace; must not return a Promise
192
+ }
193
+ ```
194
+
195
+ Returning a Promise (or anything thenable) throws — `register()` must finish before the module is usable.
196
+
119
197
  ## The client boundary
120
198
 
121
199
  The browser boundary is decided by the import graph, not by the file's location. A `*.page.tsx` is universal:
@@ -127,6 +205,24 @@ The browser boundary is decided by the import graph, not by the file's location.
127
205
 
128
206
  Keep server-only repository and service reads inside `loader`. Do not read them from module-scope initializers or the default component.
129
207
 
208
+ ### `.client` does not isolate SSR in 5.2
209
+
210
+ A `.client.tsx` suffix is a naming convention, not an SSR-isolation boundary. A
211
+ module statically imported by a page, layout, root, or any of their imports is
212
+ still evaluated by the server. Top-level browser globals such as `window`
213
+ therefore crash SSR boot. Warlock 5.2 does not ship a client-only component
214
+ primitive; code that requires browser globals at module scope cannot be part of
215
+ the SSR page graph.
216
+
217
+ ### Static assets use `public/` in 5.2
218
+
219
+ The production server build does not support imported non-stylesheet assets. An
220
+ import such as `import logo from "./logo.svg"` works under Vite in development
221
+ but `warlock build` refuses it rather than emit a server URL that disagrees with
222
+ the client bundle. Put the file in the application's `public/` directory and
223
+ reference it by root URL: `public/logo.svg` is `/logo.svg`. Stylesheet imports
224
+ are the exception and remain supported.
225
+
130
226
  ## Editing a page in development
131
227
 
132
228
  `warlock dev` decides Fast Refresh vs. a full reload by comparing the module's *skeleton* — its source with every component body masked out — across the edit. Everything outside a component body is part of the skeleton: imports, module-level declarations, and all server exports (`route`, `middleware`, `validation`, `loader`, `metadata`). The skeleton moving, with or without a simultaneous JSX change, forces a full reload; the skeleton holding still defers to Fast Refresh.
@@ -135,13 +231,24 @@ Keep server-only repository and service reads inside `loader`. Do not read them
135
231
  - **A `metadata`-only edit reloads the document.** `metadata` sits outside the skeleton's masked region, so the edit moves it. Warlock sends a full reload, which re-runs SSR and rebuilds `<head>`. Component state is lost — that is the price of seeing the new `<title>` without touching the browser.
136
232
  - **Any module-level change reloads, not just `metadata`.** An edited import, a module-level declaration, or an edit confined to `route`, `middleware`, `validation`, or `loader` all move the skeleton the same way and take the same full-reload path.
137
233
  - **A helper function used only by the JSX still reloads if it is declared at module level.** The rule does not try to prove which half of a shared declaration the edit was "really" for — it over-approximates deliberately, because a false reload only costs component state, while a missed one ships a stale `<head>` and calls it a hot update.
138
- - Creating or deleting a page file is page-graph churn, not an in-place edit; Vite handles it on its own.
234
+ - Creating, deleting, or renaming a page file, or editing its `route` export, is page-GRAPH churn, not a skeleton edit see below, not Fast Refresh.
139
235
 
140
236
  Changed in 5.1: a metadata-only edit previously left a stale `<head>` until you refreshed the browser by hand. The current skeleton-comparison rule replaces that earlier, narrower "metadata-only" special case.
141
237
 
238
+ ## Route-table changes in development
239
+
240
+ Creating a page, deleting one, or editing its `route` export's path is a different kind of dev edit from the skeleton comparison above — it changes which URLs exist, not just how one already-registered URL renders. `warlock dev` re-registers the affected route(s) in the live route table, atomically and with no dev-server restart, so the new file (or new path) is reachable on the very next request with no manual restart.
241
+
242
+ Changed in 5.2: through 5.1 the route table was built once at boot and never again. Creating a page 404'd forever; deleting one kept it reachable at its old URL; editing `route` served the OLD path from the old file and 404'd the new one — the running server silently disagreed with the source on disk. All three are now live.
243
+
142
244
  ## Gotchas
143
245
 
144
- - **Do not expect URLs to be derived from filenames.** No such convention exists. The file's location chooses discovery and which layout prefixes apply; `route` chooses the rest of the public URL. A page with no `route` is refused, not mounted at its path.
246
+ - **A page with no `route` is not unreachable.** It derives a real URL from its file location see [Filesystem routing](#filesystem-routing). Nothing about a route-less page is refused anymore.
247
+ - **A page with no default export IS refused.** Named exports alone fail the build naming the file, instead of serving a blank 200.
248
+ - **`.client.tsx` does not prevent SSR evaluation.** It is a naming convention in 5.2, not a client-only component primitive.
249
+ - **Imported static assets do not build in 5.2.** Put them in `public/` and reference their root URL; CSS imports remain supported.
250
+ - **`[...slug]` is not a catch-all.** It is read as a literal path segment and silently produces an unreachable route — see [Catch-all segments are NOT supported](#catch-all-segments-are-not-supported).
251
+ - **`process.env` is refused in the client/universal graph, `PUBLIC_` prefix included.** Read env values in a loader and return them as page data; see [`load-page-data/SKILL.md`](../load-page-data/SKILL.md).
145
252
  - **Keep `route` literal.** A computed route cannot be discovered without executing app code and is refused.
146
253
  - **Do not annotate the loader with `: PageLoader`.** That erases the return type `PageProps` needs.
147
254
  - **Components receive data, not HTTP objects.** `request` and `response` belong to loaders; the component also renders in the browser.
@@ -1,15 +1,85 @@
1
1
  ---
2
2
  name: load-page-data
3
- description: 'Load App, Layout, and Page data with `AppLoader`, `LayoutLoader`, and `PageLoader`; type component `data`, validate page input, short-circuit with the buffered response, and publish request-scoped browser-safe values through `shared`. Triggers: `PageLoader`, `LayoutLoader`, `AppLoader`, `PageProps`, `shared`, `useShared`, `validation`, `request.validated`; "load page data", "pass server data to React", "share request data", "redirect from a loader". Skip: page module basics — `@warlock.js/web/create-a-page/SKILL.md`; layouts — `@warlock.js/web/use-layouts/SKILL.md`; mutation follow-up — `@warlock.js/web/navigate-on-the-client/SKILL.md`; competing loaders Next data functions, Remix loaders, React Server Components.'
3
+ description: 'Load App, Layout, and Page data with `AppLoader`, `LayoutLoader`, and `PageLoader`; type component `data`, validate page input, short-circuit with the buffered response, and publish request-scoped browser-safe values through `shared`. Triggers: `PageLoader`, `LayoutLoader`, `AppLoader`, `PageProps`, `shared`, `useShared`, `validation`, `request.validated`, `process.env`, `env("PUBLIC_...")`, `import.meta.env.PUBLIC_`; "load page data", "pass server data to React", "share request data", "redirect from a loader", "read an environment variable in a page", "process.env refused in the client build", "loader execution order", "return a Response from a loader". Skip: page module basics — `@warlock.js/web/create-a-page/SKILL.md`; layouts — `@warlock.js/web/use-layouts/SKILL.md`; mutation follow-up — `@warlock.js/web/navigate-on-the-client/SKILL.md`; competing loaders Next data functions, Remix loaders, React Server Components.'
4
4
  ---
5
5
 
6
6
  # Warlock — load page data
7
7
 
8
8
  Loaders run on the server and return serializable data to their own component level. Use the level-specific loader type with `satisfies`, then pass `typeof loader` to the matching props type.
9
9
 
10
+ ## Pass environment values through loader data
11
+
12
+ **`process.env` is refused outright in the client/universal graph, and there is
13
+ no `PUBLIC_` exception to it — static or computed.** `env("PUBLIC_X")` does not
14
+ work client-side either. **The supported pattern is: read the value in a loader,
15
+ which is server code, and return it as page data.**
16
+
17
+ ```tsx
18
+ import { env } from "@warlock.js/core";
19
+ import type { PageLoader, PageProps } from "@warlock.js/web";
20
+
21
+ export const loader = (async () => ({
22
+ siteName: env("PUBLIC_SITE_NAME"),
23
+ })) satisfies PageLoader;
24
+
25
+ export default function HomePage({ data }: PageProps<typeof loader>) {
26
+ return <h1>{data.siteName}</h1>;
27
+ }
28
+ ```
29
+
30
+ Loaders are server-only and may read configuration normally. Their return is
31
+ serialized to the browser, so include only values that are safe to expose.
32
+
33
+ ### What is refused, and where
34
+
35
+ Every one of these fails the build when it is reachable from a default page or
36
+ layout component, `register()`, or any helper they import:
37
+
38
+ ```tsx
39
+ process.env.API_URL; // static key — refused
40
+ process.env.PUBLIC_API_URL; // a PUBLIC_ prefix changes nothing — refused
41
+ process.env[key]; // computed key — refused
42
+ const { API_URL } = process.env; // bare value-read — refused
43
+ const all = { ...process.env }; // bare value-read — refused
44
+ Object.keys(process.env); // bare value-read — refused
45
+ JSON.stringify(process.env); // bare value-read — refused
46
+ env("PUBLIC_API_URL"); // pulls in @warlock.js/core, a server package — refused
47
+ ```
48
+
49
+ Bare value-reads matter as much as keyed ones: `process` does not exist in a
50
+ browser, so referencing the object at all — assigned, destructured, spread, or
51
+ passed as an argument — is already broken, and passing the whole object to a
52
+ component is how a server secret reaches a page in one line.
53
+
54
+ **Enforcement covers dev SSR as well as the client bundle, and a violation
55
+ fails the build.** It is not a production-only check you can discover late: the
56
+ same refusal fires under `warlock dev`.
57
+
58
+ Reads inside `loader`, `route`, `middleware`, `validation`, and `metadata` are
59
+ never affected — those exports are stripped before the client graph is formed.
60
+ Server-side code is unrestricted.
61
+
62
+ ### The one client-side escape hatch
63
+
64
+ If a value genuinely has to be inlined into browser code rather than passed
65
+ through loader data, the supported spelling is `import.meta.env.PUBLIC_*` with a
66
+ **static** key:
67
+
68
+ ```tsx
69
+ export default function HomePage() {
70
+ return <h1>{import.meta.env.PUBLIC_SITE_NAME}</h1>;
71
+ }
72
+ ```
73
+
74
+ `import.meta.env` is the Vite surface, not Node's, and only the `PUBLIC_` prefix
75
+ (plus Vite's own `MODE`, `DEV`, `PROD`, `BASE_URL`, `SSR`) is allowed through. It
76
+ is inlined at build time, so it cannot vary per request or per deployment of the
77
+ same bundle — which is why loader data remains the default answer, and the only
78
+ one for anything request-scoped.
79
+
10
80
  ## The shape
11
81
 
12
- ```tsx title="src/app/products/web/product-details.page.tsx"
82
+ ```tsx title="src/web/products/product-details.page.tsx"
13
83
  import { v } from "@warlock.js/seal";
14
84
  import type { PageLoader, PageProps } from "@warlock.js/web";
15
85
 
@@ -56,11 +126,11 @@ export default function ProductDetailsPage({ data }: PageProps<typeof loader>) {
56
126
 
57
127
  ## Three loader levels
58
128
 
59
- | Module | Contract | Component props |
60
- | --- | --- | --- |
61
- | `src/web/root.tsx` | `AppLoader` | `AppProps<typeof loader>` |
129
+ | Module | Contract | Component props |
130
+ | ----------------------- | -------------- | ---------------------------- |
131
+ | `src/web/root.tsx` | `AppLoader` | `AppProps<typeof loader>` |
62
132
  | positional `layout.tsx` | `LayoutLoader` | `LayoutProps<typeof loader>` |
63
- | `*.page.tsx` | `PageLoader` | `PageProps<typeof loader>` |
133
+ | `*.page.tsx` | `PageLoader` | `PageProps<typeof loader>` |
64
134
 
65
135
  All receive one context object with `request`, `response`, and `shared`. Page loaders add generics that connect their sibling `validation` and `route` exports to `request.validated()` and `request.input()`.
66
136
 
@@ -75,9 +145,17 @@ When `validating` is absent or empty, pages validate query + params, with params
75
145
 
76
146
  `request.validated()` uses the schema's output type, so fields with `.default(...)` are present. `request.input("id")` is narrowed from a literal route path when the loader uses `typeof route`.
77
147
 
148
+ ## Execution order
149
+
150
+ Loaders run **sequentially, root to leaf, each one awaited before the next starts**: `root.tsx`'s `AppLoader`, then every matched `LayoutLoader` from outermost to innermost, then the page's `PageLoader`. The runtime has three top-level slots (`app`, `layout`, `page`), but the layout slot composes the full matched layout chain. A page may have only one _rendering_ layout; prefix-, middleware-, and loader-only layouts may still appear at multiple ancestry levels.
151
+
152
+ **The first core `Response` a loader returns is terminal.** Returning a `Response` object stops the pipeline immediately: no lower loader starts, `metadata` is not resolved, and that response is sent as-is. It is more terminal than a short-circuit — because the response is returned whole, the buffered header/cookie writes made at that same level are discarded along with everything below it. Use `response.redirect()` / `response.notFound()` (which produce a `LoaderShortCircuit`, committing that level's buffer inclusively) when you want your buffered writes to survive; return a raw `Response` only when you mean "this exact response, nothing else."
153
+
154
+ A loader that **throws** is also terminal: it stops lower loaders, discards its own level's buffer, and commits only the levels above it.
155
+
78
156
  ## Loader response surface
79
157
 
80
- Each loader gets its own buffered response because App, Layout, and Page loaders execute in parallel. The public loader methods are:
158
+ Each loader gets its own buffered response never the live one so that a level discarded by a short-circuit or a throw cannot leak half-written headers onto a response it no longer owns. The public loader methods are:
81
159
 
82
160
  ```ts
83
161
  response.header("cache-control", "private, max-age=60");
@@ -142,7 +220,7 @@ The request pipeline is:
142
220
 
143
221
  1. App, layout, and page middleware run outermost first and write `shared`.
144
222
  2. `shared` is normalized, checked, and sealed.
145
- 3. App, layout, and page loaders run in parallel and may only read it.
223
+ 3. App, layout, and page loaders run in order, root to leaf, and may only read it.
146
224
  4. Components receive a readonly snapshot through props or `useShared()`.
147
225
 
148
226
  Use `useShared()` in a deep component that is not already receiving level props:
@@ -159,12 +237,12 @@ export function LocaleLabel() {
159
237
 
160
238
  Only put browser-safe data in `shared`: scalars, arrays, plain objects, or values with a valid `toJSON()` contract. Functions, `Date`, `Map`, `Set`, and arbitrary class instances are rejected. Prefer a narrow Resource output over a model.
161
239
 
162
- ## Parallelism rules
240
+ ## Level isolation rules
163
241
 
164
- - App, Layout, and Page loaders cannot read each other's return values.
242
+ - App, Layout, and Page loaders cannot read each other's return values. They run in order, but no channel is provided between them — a lower loader is not handed what an upper one returned. Put anything a lower level needs into `shared` from middleware instead.
165
243
  - A loader may read `shared` because middleware completed and it was sealed first.
166
244
  - A loader must not write `shared`; a post-seal write throws.
167
- - Loader response mutations are buffered per level and settled deterministically.
245
+ - Loader response mutations are buffered per level and settled deterministically, root to leaf.
168
246
 
169
247
  ## Gotchas
170
248
 
@@ -172,7 +250,9 @@ Only put browser-safe data in `shared`: scalars, arrays, plain objects, or value
172
250
  - **Return client-safe data.** Components render again in the browser; models and server handles do not survive the wire.
173
251
  - **Write `shared` in middleware only.** Loaders run after the seal.
174
252
  - **Required shared keys need unconditional writers.** The type is a promise for every request.
175
- - **Do not use loader return values as cross-level communication.** The loaders run in parallel.
253
+ - **Do not use loader return values as cross-level communication.** Levels run in order but are not wired to each other; use `shared`, written in middleware.
254
+ - **`404.page.tsx` never runs its own loader.** The not-found page's module is registered and rendered for real — `register()` and its middleware still run — but the page loader is omitted from the request in both dev and production, so a missing URL cannot trigger application data work or fail a second time. Its ancestry contributes nothing either: the 404 page renders with an empty layout chain by construction. **The root `AppLoader` in `root.tsx` still runs** for a 404 request, so keep it cheap and make sure it tolerates a request that matched nothing.
255
+ - **`process.env` is refused in the client/universal graph, with no `PUBLIC_` exception.** Read it in a loader and return it as page data.
176
256
  - **Server actions are not supported.** POST to an ordinary Warlock API route and call `refresh()` after success.
177
257
 
178
258
  ## See also
@@ -106,7 +106,7 @@ export function CheckoutButtons() {
106
106
 
107
107
  The public primitive is `refresh()`. There is no `revalidate()` export.
108
108
 
109
- ```tsx title="src/app/products/web/delete-product-button.tsx"
109
+ ```tsx title="src/web/products/delete-product-button.tsx"
110
110
  import { refresh } from "@warlock.js/web";
111
111
 
112
112
  export function DeleteProductButton({ id }: { id: string }) {
@@ -64,7 +64,7 @@ Root imports ending in `.css`, `.scss`, `.sass`, `.less`, or `.styl` are recogni
64
64
 
65
65
  A page may import CSS directly:
66
66
 
67
- ```tsx title="src/app/products/web/products.page.tsx"
67
+ ```tsx title="src/web/products/products.page.tsx"
68
68
  import "./products.css";
69
69
 
70
70
  export const route = {
@@ -81,16 +81,18 @@ export default function ProductsPage() {
81
81
  }
82
82
  ```
83
83
 
84
- ```css title="src/app/products/web/products.css"
84
+ ```css title="src/web/products/products.css"
85
85
  .products-page {
86
86
  display: grid;
87
87
  gap: 1rem;
88
88
  }
89
89
  ```
90
90
 
91
- Recognized asset imports survive the page's client projection. The client boundary is determined by the import graph, not by the file living under `web/`.
91
+ Recognized stylesheet imports survive the page's client projection. The client boundary is determined by the import graph, not by the file living under `web/`.
92
92
 
93
- In development, only root stylesheet imports are converted into render-blocking document links. A page-local import is loaded by Vite's client module graph around hydration, so it still applies but is not guaranteed before first paint. Move above-the-fold or site-wide rules into a stylesheet imported by `root.tsx` when avoiding a flash matters.
93
+ That support is specific to stylesheets in the production page graph. An imported non-stylesheet asset such as `import logo from "./logo.svg"` works under Vite in development but is refused by `warlock build` in 5.2. Put it in the application's `public/` directory and reference its root URL instead: `public/logo.svg` is `/logo.svg`.
94
+
95
+ For each matched handler, Warlock builds one ordered CSS chain: `[root, ...matched layouts, page]`. A stylesheet imported directly by any member of that chain becomes a render-blocking link in the initial document in both development and production. Unrelated pages and layouts do not contribute CSS to this response.
94
96
 
95
97
  ## Production delivery
96
98
 
@@ -100,9 +102,9 @@ The production client build enables Vite's manifest. Vite records emitted CSS ag
100
102
  <build.outdir>/client/.vite/manifest.json
101
103
  ```
102
104
 
103
- It collects every valid `css` asset across all manifest entries, deduplicates them in manifest order, and emits each as a render-blocking link before `</head>`.
105
+ For each source in the matched handler chain, Warlock finds that source's manifest entry, collects its own CSS plus CSS from statically imported chunks, and emits the ordered, deduplicated result as render-blocking links before `</head>`. It does not follow `dynamicImports`, because doing so would pull unrelated lazy pages into the response.
104
106
 
105
- That means both root-imported and page-imported CSS are linked in the initial production document. The current implementation collects across the whole client manifest rather than selecting only the matched route's CSS, so every page receives the emitted stylesheet set.
107
+ That means root-, matched-layout-, and page-imported CSS are linked in the initial production document without shipping another page's stylesheet set.
106
108
 
107
109
  Only manifest URLs under the one client asset prefix are emitted; a stylesheet outside the mounted asset directory is dropped instead of producing a dead link.
108
110
 
@@ -123,17 +125,16 @@ The hydration entry is built from the projected client graph. CSS imports are kn
123
125
 
124
126
  ## Diagnose missing or late CSS
125
127
 
126
- 1. For global CSS in development, confirm `root.tsx` uses a bare import such as `import "./app.css";`.
128
+ 1. In development, confirm the root, matched layout, or page imports the stylesheet directly with a bare import such as `import "./app.css";`.
127
129
  2. Confirm the rendered document contains a closing `</head>` and a stylesheet URL ending in `?direct`.
128
- 3. For page-local CSS in development, expect Vite's client graph to apply it after hydration; move critical rules to the root import if first paint matters.
129
- 4. In production, confirm `.vite/manifest.json` has `css` arrays and the referenced files live under the client asset prefix.
130
+ 3. If CSS is imported indirectly through another JavaScript module in development, Vite's client graph still applies it, but Warlock's source scan cannot promote it to a render-blocking link; import critical CSS directly from a chain member.
131
+ 4. In production, confirm `.vite/manifest.json` has a source entry for the matched root/layout/page and that its `css` arrays reference files under the client asset prefix.
130
132
  5. Do not hand-author a link to a hashed production asset; its name belongs to the Vite manifest.
131
133
 
132
134
  ## Gotchas
133
135
 
134
- - **Root CSS and page CSS differ in dev.** Root imports are render-blocking; page imports are client-injected.
135
- - **Production currently links all emitted CSS on every page.** It is safe for first paint but not route-minimal.
136
- - **Use a bare root import.** `devStylesheetUrls` scans `import "./app.css"`, not a bound CSS-module import.
136
+ - **CSS is scoped to the matched handler chain.** Root CSS is shared; only the current route's matched layouts and page add their direct imports.
137
+ - **Use a bare direct import.** `devStylesheetUrls` scans `import "./app.css"` in each chain member, not a bound CSS-module import or an import hidden behind another JavaScript module.
137
138
  - **Keep a closing `</head>`.** Automatic link installation has nowhere safe to write without it.
138
139
  - **Do not remove `?direct` from a dev stylesheet link.** Vite otherwise responds with JavaScript.
139
140
  - **Do not import the hydration entry yourself.** The connector owns dev serving and production asset URLs.
@@ -9,7 +9,7 @@ A positional `layout.tsx` applies to pages in its directory and descendant direc
9
9
 
10
10
  ## The shape
11
11
 
12
- ```tsx title="src/app/products/web/layout.tsx"
12
+ ```tsx title="src/web/products/layout.tsx"
13
13
  import type { LayoutLoader, LayoutProps } from "@warlock.js/web";
14
14
 
15
15
  export const prefix = "/products";
@@ -44,7 +44,7 @@ export default function ProductsLayout({
44
44
 
45
45
  A page beside it can declare its path relative to the prefix:
46
46
 
47
- ```tsx title="src/app/products/web/index.page.tsx"
47
+ ```tsx title="src/web/products/index.page.tsx"
48
48
  export const route = {
49
49
  path: "/",
50
50
  name: "products.index",
@@ -59,17 +59,17 @@ The effective URL is `/products`. A page with `route.path = "/:id"` under the sa
59
59
 
60
60
  ## Prefix composition
61
61
 
62
- Every positional layout on the page's directory ancestry may export a literal `prefix`. Prefixes compose outermost first, then the page's own route path.
62
+ Every positional layout on the page's directory ancestry may export a literal `prefix`. Prefixes compose outermost first, then the page's own route path — never appended to the directory's own name, so a layout `prefix` OVERRIDES its directory's segment rather than adding to it.
63
63
 
64
- ```tsx title="src/app/users/web/layout.tsx"
64
+ ```tsx title="src/web/users/layout.tsx"
65
65
  export const prefix = "/users";
66
66
  ```
67
67
 
68
- ```tsx title="src/app/users/web/account/layout.tsx"
68
+ ```tsx title="src/web/users/account/layout.tsx"
69
69
  export const prefix = "/account";
70
70
  ```
71
71
 
72
- ```tsx title="src/app/users/web/account/settings.page.tsx"
72
+ ```tsx title="src/web/users/account/settings.page.tsx"
73
73
  export const route = {
74
74
  path: "/settings",
75
75
  name: "users.account.settings",
@@ -98,6 +98,8 @@ A `404.page.tsx` renders with no layouts, even when it sits in a directory with
98
98
 
99
99
  This is scoped to the not-found page: an ordinary page in the same directory still gets its full layout chain, and nested-layout refusal on the 404's own path is still enforced exactly as it is for any other page.
100
100
 
101
+ `error.page.tsx` — the application's other special page, its one error boundary ([create-a-page](../create-a-page/SKILL.md)) — is excluded from discovery's layout-chain analysis the same way: neither special page has a layout chain of its own.
102
+
101
103
  ## Why layout state persists
102
104
 
103
105
  Client navigation rebuilds the Layout + Page element tree at the same `#root` position. When the next page uses the same layout component type in the same position, React reconciles it instead of remounting it. Layout state such as open menus, scroll containers, and media survives.
@@ -106,15 +108,19 @@ Client navigation rebuilds the Layout + Page element tree at the same `#root` po
106
108
 
107
109
  ## Loader data
108
110
 
109
- `LayoutLoader` receives the full `PageContext` and its return reaches `LayoutProps<typeof loader>["data"]`. Layout, App, and Page loaders run in parallel; a layout loader cannot read the page loader's result.
111
+ `LayoutLoader` receives the full `PageContext` and its return reaches `LayoutProps<typeof loader>["data"]`. Loaders run sequentially as App, every matched layout from outermost to innermost, then Page. Each loader's return belongs only to its own component, so a layout loader still cannot read another loader's result.
110
112
 
111
113
  Use `shared` when multiple levels need one request-derived value, and write it in middleware before loaders run. See [load-page-data](../load-page-data/SKILL.md).
112
114
 
115
+ ## The client boundary
116
+
117
+ A layout is projected for the browser the same way a page is: `prefix`, `middleware`, `loader`, and (page-only) `validation`/`route` never reach the client bundle — `prefix` joins that stripped set for a layout the way `route` does for a page. The default export and any other surviving exports form the client graph. `register()` is the one export that is NOT stripped — it runs once per module namespace on both the server and the browser; see [create-a-page](../create-a-page/SKILL.md).
118
+
113
119
  ## Gotchas
114
120
 
115
121
  - **`layout.tsx` is positional.** It applies by directory ancestry; moving a page can change its layout and URL prefix together.
116
122
  - **Only one layout on a path may render.** Multiple prefix/middleware-only layouts are fine; multiple default exports are not.
117
- - **A layout prefix changes the registered URL.** Check the composed path, not only the page's `route.path`.
123
+ - **A layout prefix changes the registered URL, and overrides its directory's segment rather than adding to it.** Check the composed path, not only the page's `route.path`.
118
124
  - **Keep `prefix` literal.** The build parses it without executing the module.
119
125
  - **Use `LayoutProps<typeof loader>`.** Bare `LayoutProps` is for a layout with no loader and gives `data` as `undefined`.
120
126
  - **Layout persistence is type-and-position based.** Changing to another layout component remounts it, as normal React reconciliation requires.
@@ -48,6 +48,8 @@ Because App is outside the hydrated subtree, put client state that must survive
48
48
 
49
49
  **Requires 5.1 in an installed app.** In 5.0.0–5.0.2 the dev server handed the browser `react-dom/client` as raw CommonJS, so `hydrateRoot` was not there as a named export and the hydration entry died on import — no client JavaScript ran at all. The symptoms were a page that server-rendered correctly but had dead `useState`, no HMR, and `<Link>` doing full page loads. The fix pre-bundles React through Vite's `optimizeDeps`. If you are debugging this against a checkout of the framework itself, note that the defect never reproduced there: inside the monorepo the hydration entry resolves outside `node_modules` and React was always optimized normally.
50
50
 
51
+ If `root.tsx` (or any module it needs) fails to load or its `register()` throws, there is no trustworthy Layout+Page composition left to hydrate — Warlock falls back to a plain document with no hydration script at all rather than hydrate the browser against markup nothing can vouch for. See [create-a-page](../create-a-page/SKILL.md) for the app's own `error.page.tsx` boundary, which is tried first.
52
+
51
53
  ## `<Head />`
52
54
 
53
55
  `<Head />` renders the resolved page metadata at that position. It takes no props and emits:
@@ -113,7 +115,7 @@ export default function App({ data, children }: AppProps<typeof loader>) {
113
115
  }
114
116
  ```
115
117
 
116
- The App loader runs in parallel with layout and page loaders. Its return is for the App component; use `shared` for request data that multiple levels need.
118
+ The App loader runs first and is awaited before the outermost layout loader starts; matched layout loaders then run outermost to innermost before the page loader. Its return is for the App component; use `shared` for request data that multiple levels need.
117
119
 
118
120
  ## Gotchas
119
121
 
@@ -1,58 +0,0 @@
1
- import { LoaderShortCircuit, WebResponse } from "../context.mjs";
2
-
3
- //#region ../web/src/server/buffered-response.d.ts
4
- /**
5
- * The per-loader response facade for pipeline stage 6.
6
- *
7
- * Loaders run in PARALLEL and never see the real `Response`: core's
8
- * `Response.header`/`cookie` write straight through to the fastify reply
9
- * (core/src/http/response.ts:919-923, :955-967), so three concurrent loaders
10
- * writing directly would interleave nondeterministically and a discarded
11
- * layer's writes could never be taken back. Instead each loader gets one of
12
- * these facades; every write lands in a buffer, and the pipeline's
13
- * settle/commit stage applies the surviving buffers to
14
- * the real response root→leaf, per cookie name / header key.
15
- */
16
- type BufferedHeader = {
17
- key: string;
18
- value: string;
19
- };
20
- type BufferedCookie = {
21
- name: string;
22
- value: unknown;
23
- options?: Record<string, unknown>;
24
- };
25
- type ResponseBuffer = {
26
- headers: BufferedHeader[];
27
- cookies: BufferedCookie[];
28
- statusCode?: number;
29
- };
30
- /**
31
- * Runtime brand behind `LoaderShortCircuit` (web/src/context.ts:13-17
32
- * declares the compile-time half). A loader RETURNS this from
33
- * `response.redirect()` / `response.notFound()`; the settle stage detects it
34
- * by the symbol, never by shape.
35
- */
36
- declare const LOADER_SHORT_CIRCUIT: unique symbol;
37
- type LoaderShortCircuitSignal = LoaderShortCircuit & {
38
- kind: "redirect" | "notFound";
39
- statusCode: number;
40
- url?: string;
41
- body?: unknown;
42
- };
43
- declare function isLoaderShortCircuit(value: unknown): value is LoaderShortCircuitSignal;
44
- /**
45
- * The loader-facing surface is `WebResponse` (web/src/context.ts:73-88) plus
46
- * `cookie()`, which the commit contract needs so buffers can be applied
47
- * per cookie name.
48
- */
49
- type BufferedWebResponse = WebResponse & {
50
- cookie(name: string, value: unknown, options?: Record<string, unknown>): BufferedWebResponse;
51
- };
52
- declare function createBufferedResponse(): {
53
- response: BufferedWebResponse;
54
- buffer: ResponseBuffer;
55
- };
56
- //#endregion
57
- export { BufferedCookie, BufferedHeader, BufferedWebResponse, LOADER_SHORT_CIRCUIT, LoaderShortCircuitSignal, ResponseBuffer, createBufferedResponse, isLoaderShortCircuit };
58
- //# sourceMappingURL=buffered-response.d.mts.map