@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.
- package/CHANGELOG.md +250 -0
- package/README.md +126 -12
- package/esm/build/contribution.d.mts +11 -4
- package/esm/build/contribution.mjs +46 -22
- package/esm/build/contribution.mjs.map +1 -1
- package/esm/build/discover-pages.mjs +318 -44
- package/esm/build/discover-pages.mjs.map +1 -1
- package/esm/build/generate-client-registry.mjs +47 -13
- package/esm/build/generate-client-registry.mjs.map +1 -1
- package/esm/build/generate-pages-barrel.mjs +55 -13
- package/esm/build/generate-pages-barrel.mjs.map +1 -1
- package/esm/build/page-default-export.mjs +48 -0
- package/esm/build/page-default-export.mjs.map +1 -0
- package/esm/build/page-routes-manifest.mjs +21 -0
- package/esm/build/page-routes-manifest.mjs.map +1 -0
- package/esm/build/public-files.mjs +45 -0
- package/esm/build/public-files.mjs.map +1 -0
- package/esm/build/read-route-exports.mjs +1 -1
- package/esm/build/read-route-exports.mjs.map +1 -1
- package/esm/client/build-hydrated-tree.mjs +40 -9
- package/esm/client/build-hydrated-tree.mjs.map +1 -1
- package/esm/client/hydrate-page.mjs +1 -1
- package/esm/client/hydrate-page.mjs.map +1 -1
- package/esm/client/navigation/current-route.d.mts +1 -1
- package/esm/client/navigation/current-route.mjs +1 -1
- package/esm/client/navigation/current-route.mjs.map +1 -1
- package/esm/client/navigation/fetch-page-data.mjs +1 -1
- package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
- package/esm/client/navigation/navigation-root.mjs +44 -7
- package/esm/client/navigation/navigation-root.mjs.map +1 -1
- package/esm/client/navigation/prefetch.mjs +1 -1
- package/esm/client/navigation/prefetch.mjs.map +1 -1
- package/esm/client/navigation/refresh.d.mts +1 -1
- package/esm/client/navigation/refresh.mjs +1 -1
- package/esm/client/navigation/refresh.mjs.map +1 -1
- package/esm/client/navigation/scroll-to-fragment.mjs +26 -0
- package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -0
- package/esm/client/navigation/verbs.d.mts +1 -1
- package/esm/client/navigation/verbs.mjs +1 -1
- package/esm/client/navigation/verbs.mjs.map +1 -1
- package/esm/client/runtime/index.d.mts +2 -1
- package/esm/client/runtime/index.mjs +2 -1
- package/esm/client/runtime/manifest.d.mts +1 -1
- package/esm/client/runtime/manifest.mjs +3 -2
- package/esm/client/runtime/manifest.mjs.map +1 -1
- package/esm/client/runtime/matcher.d.mts +13 -1
- package/esm/client/runtime/matcher.mjs +13 -1
- package/esm/client/runtime/matcher.mjs.map +1 -1
- package/esm/client/runtime/types.d.mts +15 -2
- package/esm/components/default-app.mjs +1 -1
- package/esm/components/default-app.mjs.map +1 -1
- package/esm/components/document-context.d.mts +40 -2
- package/esm/components/document-context.mjs +1 -1
- package/esm/components/document-context.mjs.map +1 -1
- package/esm/components/head.d.mts +1 -1
- package/esm/components/head.mjs +1 -1
- package/esm/components/head.mjs.map +1 -1
- package/esm/components/link.d.mts +1 -1
- package/esm/components/link.mjs +1 -1
- package/esm/components/link.mjs.map +1 -1
- package/esm/components/scripts.d.mts +1 -1
- package/esm/components/scripts.mjs +4 -2
- package/esm/components/scripts.mjs.map +1 -1
- package/esm/context.d.mts +6 -102
- package/esm/hydration/index.mjs +1 -1
- package/esm/hydration/index.mjs.map +1 -1
- package/esm/hydration-payload.d.mts +1 -1
- package/esm/hydration-payload.mjs +37 -8
- package/esm/hydration-payload.mjs.map +1 -1
- package/esm/index.d.mts +4 -3
- package/esm/loaders.d.mts +7 -36
- package/esm/metadata.d.mts +15 -1
- package/esm/metadata.mjs +45 -0
- package/esm/metadata.mjs.map +1 -0
- package/esm/props.d.mts +16 -8
- package/esm/route.d.mts +2 -17
- package/esm/routing/compose-route-path.d.mts +1 -1
- package/esm/routing/compose-route-path.mjs +1 -1
- package/esm/routing/compose-route-path.mjs.map +1 -1
- package/esm/routing/data-request.mjs +1 -1
- package/esm/routing/data-request.mjs.map +1 -1
- package/esm/routing/filesystem-route.mjs +45 -0
- package/esm/routing/filesystem-route.mjs.map +1 -0
- package/esm/routing/layout-policy.mjs +1 -1
- package/esm/routing/layout-policy.mjs.map +1 -1
- package/esm/routing/navigator.mjs +1 -1
- package/esm/routing/navigator.mjs.map +1 -1
- package/esm/routing/query-string.d.mts +1 -1
- package/esm/routing/query-string.mjs +1 -1
- package/esm/routing/query-string.mjs.map +1 -1
- package/esm/routing/route-identity.mjs +1 -1
- package/esm/routing/route-identity.mjs.map +1 -1
- package/esm/routing/route-table.d.mts +1 -1
- package/esm/routing/route-table.mjs +1 -1
- package/esm/routing/route-table.mjs.map +1 -1
- package/esm/routing/router-events.d.mts +1 -1
- package/esm/routing/router-events.mjs +1 -1
- package/esm/routing/router-events.mjs.map +1 -1
- package/esm/routing/url-fragment.mjs +120 -0
- package/esm/routing/url-fragment.mjs.map +1 -0
- package/esm/runtime/register-modules.d.mts +19 -0
- package/esm/runtime/register-modules.mjs +22 -0
- package/esm/runtime/register-modules.mjs.map +1 -0
- package/esm/server/build-hydration-payload.mjs +2 -1
- package/esm/server/build-hydration-payload.mjs.map +1 -1
- package/esm/server/client-asset-url-prefix.d.mts +1 -1
- package/esm/server/client-asset-url-prefix.mjs +1 -1
- package/esm/server/client-asset-url-prefix.mjs.map +1 -1
- package/esm/server/create-page-module-loader.d.mts +1 -1
- package/esm/server/create-page-module-loader.mjs +2 -1
- package/esm/server/create-page-module-loader.mjs.map +1 -1
- package/esm/server/create-page-route-handler.d.mts +58 -6
- package/esm/server/create-page-route-handler.mjs +127 -40
- package/esm/server/create-page-route-handler.mjs.map +1 -1
- package/esm/server/dev-server.mjs +2 -13
- package/esm/server/dev-server.mjs.map +1 -1
- package/esm/server/error-page.d.mts +14 -0
- package/esm/server/error-page.mjs +39 -0
- package/esm/server/error-page.mjs.map +1 -0
- package/esm/server/execute-page-request.d.mts +7 -4
- package/esm/server/execute-page-request.mjs +79 -80
- package/esm/server/execute-page-request.mjs.map +1 -1
- package/esm/server/execute-page-request.types.d.mts +33 -92
- package/esm/server/hydration-client-url.d.mts +1 -1
- package/esm/server/hydration-client-url.mjs +2 -2
- package/esm/server/hydration-client-url.mjs.map +1 -1
- package/esm/server/index.d.mts +6 -5
- package/esm/server/index.mjs +4 -4
- package/esm/server/install-page-routes-from-manifest.d.mts +17 -7
- package/esm/server/install-page-routes-from-manifest.mjs +91 -8
- package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
- package/esm/server/install-page-routes.d.mts +32 -13
- package/esm/server/install-page-routes.mjs +112 -32
- package/esm/server/install-page-routes.mjs.map +1 -1
- package/esm/server/install-production-page-routes.mjs +3 -31
- package/esm/server/install-production-page-routes.mjs.map +1 -1
- package/esm/server/match-page-route.mjs +2 -2
- package/esm/server/match-page-route.mjs.map +1 -1
- package/esm/server/not-found-page.d.mts +126 -0
- package/esm/server/not-found-page.mjs +157 -0
- package/esm/server/not-found-page.mjs.map +1 -0
- package/esm/server/page-context.d.mts +3 -9
- package/esm/server/page-context.mjs +3 -16
- package/esm/server/page-context.mjs.map +1 -1
- package/esm/server/page-file-change.mjs +77 -0
- package/esm/server/page-file-change.mjs.map +1 -0
- package/esm/server/page-manifest.d.mts +25 -10
- package/esm/server/page-manifest.mjs +1 -1
- package/esm/server/page-manifest.mjs.map +1 -1
- package/esm/server/page-render-bundle.mjs +38 -0
- package/esm/server/page-render-bundle.mjs.map +1 -0
- package/esm/server/page-route-reload.mjs +54 -0
- package/esm/server/page-route-reload.mjs.map +1 -0
- package/esm/server/register-production-public-files.mjs +40 -0
- package/esm/server/register-production-public-files.mjs.map +1 -0
- package/esm/server/render-page.d.mts +11 -14
- package/esm/server/render-page.mjs +154 -28
- package/esm/server/render-page.mjs.map +1 -1
- package/esm/server/resolve-page-metadata.mjs +2 -2
- package/esm/server/resolve-page-metadata.mjs.map +1 -1
- package/esm/server/resolve-validation-data.mjs +1 -1
- package/esm/server/resolve-validation-data.mjs.map +1 -1
- package/esm/server/settle-page-response.d.mts +58 -1
- package/esm/server/settle-page-response.mjs +111 -46
- package/esm/server/settle-page-response.mjs.map +1 -1
- package/esm/server/stylesheet-urls.d.mts +41 -30
- package/esm/server/stylesheet-urls.mjs +137 -52
- package/esm/server/stylesheet-urls.mjs.map +1 -1
- package/esm/server/unregistered-pages.mjs +42 -0
- package/esm/server/unregistered-pages.mjs.map +1 -0
- package/esm/server/web-connector-factory.d.mts +2 -3
- package/esm/server/web-connector-factory.mjs +6 -4
- package/esm/server/web-connector-factory.mjs.map +1 -1
- package/esm/server/web-connector.d.mts +1 -1
- package/esm/server/web-connector.mjs +233 -18
- package/esm/server/web-connector.mjs.map +1 -1
- package/esm/shared.d.mts +18 -3
- package/esm/shared.mjs +3 -7
- package/esm/shared.mjs.map +1 -1
- package/esm/validation.d.mts +1 -1
- package/esm/vite/app-convention-aliases.mjs +1 -1
- package/esm/vite/app-convention-aliases.mjs.map +1 -1
- package/esm/vite/build-client.d.mts +1 -1
- package/esm/vite/build-client.mjs +2 -2
- package/esm/vite/build-client.mjs.map +1 -1
- package/esm/vite/gate-a-resolve.d.mts +1 -1
- package/esm/vite/gate-a-resolve.mjs +67 -13
- package/esm/vite/gate-a-resolve.mjs.map +1 -1
- package/esm/vite/gate-b-secrets.d.mts +1 -1
- package/esm/vite/gate-b-secrets.mjs +35 -17
- package/esm/vite/gate-b-secrets.mjs.map +1 -1
- package/esm/vite/gate-c-verify.d.mts +1 -1
- package/esm/vite/gate-c-verify.mjs +6 -6
- package/esm/vite/gate-c-verify.mjs.map +1 -1
- package/esm/vite/hydration-entries.d.mts +1 -1
- package/esm/vite/hydration-entries.mjs +10 -6
- package/esm/vite/hydration-entries.mjs.map +1 -1
- package/esm/vite/index.d.mts +9 -6
- package/esm/vite/index.mjs +124 -11
- package/esm/vite/index.mjs.map +1 -1
- package/esm/vite/page-registry-plugin.d.mts +22 -3
- package/esm/vite/page-registry-plugin.mjs +247 -2
- package/esm/vite/page-registry-plugin.mjs.map +1 -1
- package/esm/vite/projection.d.mts +2 -2
- package/esm/vite/projection.mjs +38 -15
- package/esm/vite/projection.mjs.map +1 -1
- package/llms-full.txt +241 -49
- package/llms.txt +6 -2
- package/package.json +3 -3
- package/skills/add-web-to-an-app/SKILL.md +3 -3
- package/skills/create-a-page/SKILL.md +130 -7
- package/skills/load-page-data/SKILL.md +92 -12
- package/skills/navigate-on-the-client/SKILL.md +3 -1
- package/skills/serve-styles/SKILL.md +13 -12
- package/skills/use-layouts/SKILL.md +20 -8
- package/skills/write-the-root/SKILL.md +5 -1
- package/esm/server/buffered-response.d.mts +0 -58
- package/esm/server/buffered-response.mjs +0 -115
- package/esm/server/buffered-response.mjs.map +0 -1
package/llms-full.txt
CHANGED
|
@@ -2,14 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
> Package: `@warlock.js/web`
|
|
4
4
|
|
|
5
|
-
> SSR React pages served by the same Warlock HTTP server that serves the API — a backend framework that renders React, not a React framework.
|
|
6
|
-
|
|
7
|
-
> First published release: 5.0.0. `@warlock.js/web` had never been published to npm before this release.
|
|
8
|
-
|
|
9
|
-
> Consumer entry points: `@warlock.js/web`, `@warlock.js/web/client/runtime`, `@warlock.js/web/connector`, and `@warlock.js/web/vite`. The published `@warlock.js/web/hydration` subpath is a framework build input; application code never imports it.
|
|
10
|
-
|
|
11
|
-
> Deliberate non-goals: server actions are a v2 design decision rather than a missing v1 feature; page routes reject regex parameters, optional parameters, and multiple parameters in one segment.
|
|
12
|
-
|
|
13
5
|
> Generated artifact. Concatenates every SKILL.md and reference file under `@warlock.js/web/skills/`. Re-run `node scripts/generate-llms.mjs` after any change.
|
|
14
6
|
|
|
15
7
|
## add-web-to-an-app `@warlock.js/web/add-web-to-an-app/SKILL.md`
|
|
@@ -26,7 +18,7 @@ description: 'Install the SSR page layer with `warlock add web`: add React/Vite
|
|
|
26
18
|
## The shape
|
|
27
19
|
|
|
28
20
|
```bash
|
|
29
|
-
|
|
21
|
+
pnpm warlock add web
|
|
30
22
|
```
|
|
31
23
|
|
|
32
24
|
On the ordinary path it leaves this application-owned shape:
|
|
@@ -97,8 +89,8 @@ On the refusal path, free `/` yourself and create a page with either `route = "/
|
|
|
97
89
|
Read the files rather than assuming the command could patch every application shape:
|
|
98
90
|
|
|
99
91
|
```bash
|
|
100
|
-
|
|
101
|
-
|
|
92
|
+
pnpm warlock routes --method GET --path /
|
|
93
|
+
pnpm warlock routes --method GET --path /welcome
|
|
102
94
|
```
|
|
103
95
|
|
|
104
96
|
The desired result is one page route at `/` and, when the stock JSON route existed, one API route at `/welcome`.
|
|
@@ -122,16 +114,16 @@ The desired result is one page route at `/` and, when the stock JSON route exist
|
|
|
122
114
|
|
|
123
115
|
---
|
|
124
116
|
name: create-a-page
|
|
125
|
-
description: 'Create an SSR React page under `src/web
|
|
117
|
+
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.'
|
|
126
118
|
---
|
|
127
119
|
|
|
128
120
|
# Warlock — create a page
|
|
129
121
|
|
|
130
|
-
A page is any `*.page.tsx` beneath
|
|
122
|
+
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.
|
|
131
123
|
|
|
132
124
|
## The shape
|
|
133
125
|
|
|
134
|
-
```tsx title="src/
|
|
126
|
+
```tsx title="src/web/products/product-details.page.tsx"
|
|
135
127
|
import type { PageLoader, PageMetadata, PageProps } from "@warlock.js/web";
|
|
136
128
|
|
|
137
129
|
export const route = {
|
|
@@ -184,7 +176,28 @@ export default function ContactPage() {
|
|
|
184
176
|
}
|
|
185
177
|
```
|
|
186
178
|
|
|
187
|
-
|
|
179
|
+
`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.
|
|
180
|
+
|
|
181
|
+
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.
|
|
182
|
+
|
|
183
|
+
### The default export is required
|
|
184
|
+
|
|
185
|
+
`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**:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
The page "src/web/contact.page.tsx" has no runtime default export. Every `*.page.tsx`
|
|
189
|
+
file must default-export the React component it renders. For example:
|
|
190
|
+
`export default function Page() { return <main />; }`
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
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.
|
|
194
|
+
|
|
195
|
+
Two details worth knowing:
|
|
196
|
+
|
|
197
|
+
- **`export { Page as default }` satisfies the rule** — the check is for a runtime default binding, not for the `export default` keyword form specifically.
|
|
198
|
+
- **`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.
|
|
199
|
+
|
|
200
|
+
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.
|
|
188
201
|
|
|
189
202
|
## Route declarations
|
|
190
203
|
|
|
@@ -201,16 +214,44 @@ export const route = {
|
|
|
201
214
|
} as const;
|
|
202
215
|
```
|
|
203
216
|
|
|
204
|
-
Prefer an explicit stable `name` for links. Without one, Warlock derives a name from the
|
|
217
|
+
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.
|
|
218
|
+
|
|
219
|
+
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.
|
|
205
220
|
|
|
206
221
|
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.
|
|
207
222
|
|
|
223
|
+
## Filesystem routing
|
|
224
|
+
|
|
225
|
+
Omit `route` and the URL comes from the page's own path beneath `src/web`:
|
|
226
|
+
|
|
227
|
+
- Every directory contributes a segment, in order — `src/web/products/featured.page.tsx` derives `/products/featured`.
|
|
228
|
+
- A `(group)` directory — parentheses, not braces — contributes nothing to the URL, only to organization: `src/web/(marketing)/pricing.page.tsx` derives `/pricing`.
|
|
229
|
+
- `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`.
|
|
230
|
+
- `[id]` becomes `:id`: `src/web/products/[id].page.tsx` derives `/products/:id`.
|
|
231
|
+
- 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)).
|
|
232
|
+
|
|
233
|
+
Two pages that derive (or declare) the same effective path is a build error naming both files.
|
|
234
|
+
|
|
235
|
+
### Catch-all segments are NOT supported
|
|
236
|
+
|
|
237
|
+
**`[...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.
|
|
238
|
+
|
|
239
|
+
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.
|
|
240
|
+
|
|
241
|
+
⚠ **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.
|
|
242
|
+
|
|
243
|
+
Until a catch-all exists, use the terminal wildcard with an explicit `route`:
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
export const route = { path: "/docs/*", name: "docs.catchAll" } as const;
|
|
247
|
+
```
|
|
248
|
+
|
|
208
249
|
## Page-route grammar
|
|
209
250
|
|
|
210
251
|
Page routes deliberately accept less than API routes:
|
|
211
252
|
|
|
212
253
|
- Supported: `/`, static segments, whole-segment params such as `/products/:id`, the exact wildcard `*`, and a terminal wildcard such as `/docs/*`.
|
|
213
|
-
- Not supported: regex params, optional params, multiple params in one segment, params mixed with text, doubled or trailing slashes,
|
|
254
|
+
- 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]`).
|
|
214
255
|
|
|
215
256
|
Write two pages for an optional segment. Validate a constrained parameter in the page instead of putting a regex in its path.
|
|
216
257
|
|
|
@@ -234,6 +275,39 @@ export const metadata: PageMetadata = {
|
|
|
234
275
|
|
|
235
276
|
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.
|
|
236
277
|
|
|
278
|
+
## The error boundary — `error.page.tsx`
|
|
279
|
+
|
|
280
|
+
`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.
|
|
281
|
+
|
|
282
|
+
```tsx title="src/web/error.page.tsx"
|
|
283
|
+
import type { ErrorPageProps } from "@warlock.js/web";
|
|
284
|
+
|
|
285
|
+
export default function ErrorPage({ error, status }: ErrorPageProps) {
|
|
286
|
+
return (
|
|
287
|
+
<main>
|
|
288
|
+
<h1>Something went wrong</h1>
|
|
289
|
+
<p>Status: {status}</p>
|
|
290
|
+
</main>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
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.
|
|
296
|
+
|
|
297
|
+
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.
|
|
298
|
+
|
|
299
|
+
## The `register()` hook
|
|
300
|
+
|
|
301
|
+
`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.
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
export function register() {
|
|
305
|
+
// one-time setup for this module namespace; must not return a Promise
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Returning a Promise (or anything thenable) throws — `register()` must finish before the module is usable.
|
|
310
|
+
|
|
237
311
|
## The client boundary
|
|
238
312
|
|
|
239
313
|
The browser boundary is decided by the import graph, not by the file's location. A `*.page.tsx` is universal:
|
|
@@ -245,9 +319,30 @@ The browser boundary is decided by the import graph, not by the file's location.
|
|
|
245
319
|
|
|
246
320
|
Keep server-only repository and service reads inside `loader`. Do not read them from module-scope initializers or the default component.
|
|
247
321
|
|
|
322
|
+
## Editing a page in development
|
|
323
|
+
|
|
324
|
+
`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.
|
|
325
|
+
|
|
326
|
+
- **A JSX-only edit hot-updates.** The skeleton is unchanged, so Vite's Fast Refresh applies the projected client code with no reload and no lost component state.
|
|
327
|
+
- **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.
|
|
328
|
+
- **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.
|
|
329
|
+
- **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.
|
|
330
|
+
- 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.
|
|
331
|
+
|
|
332
|
+
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.
|
|
333
|
+
|
|
334
|
+
## Route-table changes in development
|
|
335
|
+
|
|
336
|
+
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.
|
|
337
|
+
|
|
338
|
+
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.
|
|
339
|
+
|
|
248
340
|
## Gotchas
|
|
249
341
|
|
|
250
|
-
- **
|
|
342
|
+
- **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.
|
|
343
|
+
- **A page with no default export IS refused.** Named exports alone fail the build naming the file, instead of serving a blank 200.
|
|
344
|
+
- **`[...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).
|
|
345
|
+
- **`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).
|
|
251
346
|
- **Keep `route` literal.** A computed route cannot be discovered without executing app code and is refused.
|
|
252
347
|
- **Do not annotate the loader with `: PageLoader`.** That erases the return type `PageProps` needs.
|
|
253
348
|
- **Components receive data, not HTTP objects.** `request` and `response` belong to loaders; the component also renders in the browser.
|
|
@@ -266,16 +361,86 @@ Keep server-only repository and service reads inside `loader`. Do not read them
|
|
|
266
361
|
|
|
267
362
|
---
|
|
268
363
|
name: load-page-data
|
|
269
|
-
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.'
|
|
364
|
+
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.'
|
|
270
365
|
---
|
|
271
366
|
|
|
272
367
|
# Warlock — load page data
|
|
273
368
|
|
|
274
369
|
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.
|
|
275
370
|
|
|
371
|
+
## Pass environment values through loader data
|
|
372
|
+
|
|
373
|
+
**`process.env` is refused outright in the client/universal graph, and there is
|
|
374
|
+
no `PUBLIC_` exception to it — static or computed.** `env("PUBLIC_X")` does not
|
|
375
|
+
work client-side either. **The supported pattern is: read the value in a loader,
|
|
376
|
+
which is server code, and return it as page data.**
|
|
377
|
+
|
|
378
|
+
```tsx
|
|
379
|
+
import { env } from "@warlock.js/core";
|
|
380
|
+
import type { PageLoader, PageProps } from "@warlock.js/web";
|
|
381
|
+
|
|
382
|
+
export const loader = (async () => ({
|
|
383
|
+
siteName: env("PUBLIC_SITE_NAME"),
|
|
384
|
+
})) satisfies PageLoader;
|
|
385
|
+
|
|
386
|
+
export default function HomePage({ data }: PageProps<typeof loader>) {
|
|
387
|
+
return <h1>{data.siteName}</h1>;
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Loaders are server-only and may read configuration normally. Their return is
|
|
392
|
+
serialized to the browser, so include only values that are safe to expose.
|
|
393
|
+
|
|
394
|
+
### What is refused, and where
|
|
395
|
+
|
|
396
|
+
Every one of these fails the build when it is reachable from a default page or
|
|
397
|
+
layout component, `register()`, or any helper they import:
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
process.env.API_URL; // static key — refused
|
|
401
|
+
process.env.PUBLIC_API_URL; // a PUBLIC_ prefix changes nothing — refused
|
|
402
|
+
process.env[key]; // computed key — refused
|
|
403
|
+
const { API_URL } = process.env; // bare value-read — refused
|
|
404
|
+
const all = { ...process.env }; // bare value-read — refused
|
|
405
|
+
Object.keys(process.env); // bare value-read — refused
|
|
406
|
+
JSON.stringify(process.env); // bare value-read — refused
|
|
407
|
+
env("PUBLIC_API_URL"); // pulls in @warlock.js/core, a server package — refused
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Bare value-reads matter as much as keyed ones: `process` does not exist in a
|
|
411
|
+
browser, so referencing the object at all — assigned, destructured, spread, or
|
|
412
|
+
passed as an argument — is already broken, and passing the whole object to a
|
|
413
|
+
component is how a server secret reaches a page in one line.
|
|
414
|
+
|
|
415
|
+
**Enforcement covers dev SSR as well as the client bundle, and a violation
|
|
416
|
+
fails the build.** It is not a production-only check you can discover late: the
|
|
417
|
+
same refusal fires under `warlock dev`.
|
|
418
|
+
|
|
419
|
+
Reads inside `loader`, `route`, `middleware`, `validation`, and `metadata` are
|
|
420
|
+
never affected — those exports are stripped before the client graph is formed.
|
|
421
|
+
Server-side code is unrestricted.
|
|
422
|
+
|
|
423
|
+
### The one client-side escape hatch
|
|
424
|
+
|
|
425
|
+
If a value genuinely has to be inlined into browser code rather than passed
|
|
426
|
+
through loader data, the supported spelling is `import.meta.env.PUBLIC_*` with a
|
|
427
|
+
**static** key:
|
|
428
|
+
|
|
429
|
+
```tsx
|
|
430
|
+
export default function HomePage() {
|
|
431
|
+
return <h1>{import.meta.env.PUBLIC_SITE_NAME}</h1>;
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
`import.meta.env` is the Vite surface, not Node's, and only the `PUBLIC_` prefix
|
|
436
|
+
(plus Vite's own `MODE`, `DEV`, `PROD`, `BASE_URL`, `SSR`) is allowed through. It
|
|
437
|
+
is inlined at build time, so it cannot vary per request or per deployment of the
|
|
438
|
+
same bundle — which is why loader data remains the default answer, and the only
|
|
439
|
+
one for anything request-scoped.
|
|
440
|
+
|
|
276
441
|
## The shape
|
|
277
442
|
|
|
278
|
-
```tsx title="src/
|
|
443
|
+
```tsx title="src/web/products/product-details.page.tsx"
|
|
279
444
|
import { v } from "@warlock.js/seal";
|
|
280
445
|
import type { PageLoader, PageProps } from "@warlock.js/web";
|
|
281
446
|
|
|
@@ -322,11 +487,11 @@ export default function ProductDetailsPage({ data }: PageProps<typeof loader>) {
|
|
|
322
487
|
|
|
323
488
|
## Three loader levels
|
|
324
489
|
|
|
325
|
-
| Module
|
|
326
|
-
|
|
|
327
|
-
| `src/web/root.tsx`
|
|
490
|
+
| Module | Contract | Component props |
|
|
491
|
+
| ----------------------- | -------------- | ---------------------------- |
|
|
492
|
+
| `src/web/root.tsx` | `AppLoader` | `AppProps<typeof loader>` |
|
|
328
493
|
| positional `layout.tsx` | `LayoutLoader` | `LayoutProps<typeof loader>` |
|
|
329
|
-
| `*.page.tsx`
|
|
494
|
+
| `*.page.tsx` | `PageLoader` | `PageProps<typeof loader>` |
|
|
330
495
|
|
|
331
496
|
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()`.
|
|
332
497
|
|
|
@@ -341,9 +506,17 @@ When `validating` is absent or empty, pages validate query + params, with params
|
|
|
341
506
|
|
|
342
507
|
`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`.
|
|
343
508
|
|
|
509
|
+
## Execution order
|
|
510
|
+
|
|
511
|
+
Loaders run **sequentially, root to leaf, each one awaited before the next starts**: `root.tsx`'s `AppLoader`, then the layout's `LayoutLoader`, then the page's `PageLoader`. There are exactly three levels — a page has at most one _rendering_ layout (nested rendering layouts are a build error), so there is no outer-layout/inner-layout loader chain to reason about. Layout _middleware_ still composes across the whole ancestry; only the loader levels are capped at three.
|
|
512
|
+
|
|
513
|
+
**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."
|
|
514
|
+
|
|
515
|
+
A loader that **throws** is also terminal: it stops lower loaders, discards its own level's buffer, and commits only the levels above it.
|
|
516
|
+
|
|
344
517
|
## Loader response surface
|
|
345
518
|
|
|
346
|
-
Each loader gets its own buffered response
|
|
519
|
+
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:
|
|
347
520
|
|
|
348
521
|
```ts
|
|
349
522
|
response.header("cache-control", "private, max-age=60");
|
|
@@ -408,7 +581,7 @@ The request pipeline is:
|
|
|
408
581
|
|
|
409
582
|
1. App, layout, and page middleware run outermost first and write `shared`.
|
|
410
583
|
2. `shared` is normalized, checked, and sealed.
|
|
411
|
-
3. App, layout, and page loaders run in
|
|
584
|
+
3. App, layout, and page loaders run in order, root to leaf, and may only read it.
|
|
412
585
|
4. Components receive a readonly snapshot through props or `useShared()`.
|
|
413
586
|
|
|
414
587
|
Use `useShared()` in a deep component that is not already receiving level props:
|
|
@@ -425,12 +598,12 @@ export function LocaleLabel() {
|
|
|
425
598
|
|
|
426
599
|
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.
|
|
427
600
|
|
|
428
|
-
##
|
|
601
|
+
## Level isolation rules
|
|
429
602
|
|
|
430
|
-
- App, Layout, and Page loaders cannot read each other's return values.
|
|
603
|
+
- 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.
|
|
431
604
|
- A loader may read `shared` because middleware completed and it was sealed first.
|
|
432
605
|
- A loader must not write `shared`; a post-seal write throws.
|
|
433
|
-
- Loader response mutations are buffered per level and settled deterministically.
|
|
606
|
+
- Loader response mutations are buffered per level and settled deterministically, root to leaf.
|
|
434
607
|
|
|
435
608
|
## Gotchas
|
|
436
609
|
|
|
@@ -438,7 +611,9 @@ Only put browser-safe data in `shared`: scalars, arrays, plain objects, or value
|
|
|
438
611
|
- **Return client-safe data.** Components render again in the browser; models and server handles do not survive the wire.
|
|
439
612
|
- **Write `shared` in middleware only.** Loaders run after the seal.
|
|
440
613
|
- **Required shared keys need unconditional writers.** The type is a promise for every request.
|
|
441
|
-
- **Do not use loader return values as cross-level communication.**
|
|
614
|
+
- **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.
|
|
615
|
+
- **`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.
|
|
616
|
+
- **`process.env` is refused in the client/universal graph, with no `PUBLIC_` exception.** Read it in a loader and return it as page data.
|
|
442
617
|
- **Server actions are not supported.** POST to an ordinary Warlock API route and call `refresh()` after success.
|
|
443
618
|
|
|
444
619
|
## See also
|
|
@@ -459,6 +634,8 @@ description: 'Navigate hydrated pages with `<Link>`, resolve named URLs with `hr
|
|
|
459
634
|
|
|
460
635
|
`<Link>` renders a real anchor for progressive enhancement and intercepts a plain in-app click after hydration. The server remains the only route matcher; client navigation fetches the page-data representation of the URL and swaps the Layout + Page tree.
|
|
461
636
|
|
|
637
|
+
Every behaviour on this page requires 5.1 in an installed app. In 5.0.0–5.0.2 no client JavaScript executed at all — `react-dom/client` was served as raw CommonJS and hydration never mounted — so `<Link>` degraded to its underlying anchor and every click was a full page load. See [write-the-root](../write-the-root/SKILL.md#root-is-the-hydration-boundary).
|
|
638
|
+
|
|
462
639
|
## The shape
|
|
463
640
|
|
|
464
641
|
```tsx title="src/web/components/product-link.tsx"
|
|
@@ -556,7 +733,7 @@ export function CheckoutButtons() {
|
|
|
556
733
|
|
|
557
734
|
The public primitive is `refresh()`. There is no `revalidate()` export.
|
|
558
735
|
|
|
559
|
-
```tsx title="src/
|
|
736
|
+
```tsx title="src/web/products/delete-product-button.tsx"
|
|
560
737
|
import { refresh } from "@warlock.js/web";
|
|
561
738
|
|
|
562
739
|
export function DeleteProductButton({ id }: { id: string }) {
|
|
@@ -701,7 +878,7 @@ Root imports ending in `.css`, `.scss`, `.sass`, `.less`, or `.styl` are recogni
|
|
|
701
878
|
|
|
702
879
|
A page may import CSS directly:
|
|
703
880
|
|
|
704
|
-
```tsx title="src/
|
|
881
|
+
```tsx title="src/web/products/products.page.tsx"
|
|
705
882
|
import "./products.css";
|
|
706
883
|
|
|
707
884
|
export const route = {
|
|
@@ -718,7 +895,7 @@ export default function ProductsPage() {
|
|
|
718
895
|
}
|
|
719
896
|
```
|
|
720
897
|
|
|
721
|
-
```css title="src/
|
|
898
|
+
```css title="src/web/products/products.css"
|
|
722
899
|
.products-page {
|
|
723
900
|
display: grid;
|
|
724
901
|
gap: 1rem;
|
|
@@ -727,7 +904,7 @@ export default function ProductsPage() {
|
|
|
727
904
|
|
|
728
905
|
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/`.
|
|
729
906
|
|
|
730
|
-
|
|
907
|
+
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.
|
|
731
908
|
|
|
732
909
|
## Production delivery
|
|
733
910
|
|
|
@@ -737,9 +914,9 @@ The production client build enables Vite's manifest. Vite records emitted CSS ag
|
|
|
737
914
|
<build.outdir>/client/.vite/manifest.json
|
|
738
915
|
```
|
|
739
916
|
|
|
740
|
-
|
|
917
|
+
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.
|
|
741
918
|
|
|
742
|
-
That means
|
|
919
|
+
That means root-, matched-layout-, and page-imported CSS are linked in the initial production document without shipping another page's stylesheet set.
|
|
743
920
|
|
|
744
921
|
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.
|
|
745
922
|
|
|
@@ -760,17 +937,16 @@ The hydration entry is built from the projected client graph. CSS imports are kn
|
|
|
760
937
|
|
|
761
938
|
## Diagnose missing or late CSS
|
|
762
939
|
|
|
763
|
-
1.
|
|
940
|
+
1. In development, confirm the root, matched layout, or page imports the stylesheet directly with a bare import such as `import "./app.css";`.
|
|
764
941
|
2. Confirm the rendered document contains a closing `</head>` and a stylesheet URL ending in `?direct`.
|
|
765
|
-
3.
|
|
766
|
-
4. In production, confirm `.vite/manifest.json` has `css` arrays
|
|
942
|
+
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.
|
|
943
|
+
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.
|
|
767
944
|
5. Do not hand-author a link to a hashed production asset; its name belongs to the Vite manifest.
|
|
768
945
|
|
|
769
946
|
## Gotchas
|
|
770
947
|
|
|
771
|
-
- **
|
|
772
|
-
- **
|
|
773
|
-
- **Use a bare root import.** `devStylesheetUrls` scans `import "./app.css"`, not a bound CSS-module import.
|
|
948
|
+
- **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.
|
|
949
|
+
- **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.
|
|
774
950
|
- **Keep a closing `</head>`.** Automatic link installation has nowhere safe to write without it.
|
|
775
951
|
- **Do not remove `?direct` from a dev stylesheet link.** Vite otherwise responds with JavaScript.
|
|
776
952
|
- **Do not import the hydration entry yourself.** The connector owns dev serving and production asset URLs.
|
|
@@ -794,7 +970,7 @@ A positional `layout.tsx` applies to pages in its directory and descendant direc
|
|
|
794
970
|
|
|
795
971
|
## The shape
|
|
796
972
|
|
|
797
|
-
```tsx title="src/
|
|
973
|
+
```tsx title="src/web/products/layout.tsx"
|
|
798
974
|
import type { LayoutLoader, LayoutProps } from "@warlock.js/web";
|
|
799
975
|
|
|
800
976
|
export const prefix = "/products";
|
|
@@ -829,7 +1005,7 @@ export default function ProductsLayout({
|
|
|
829
1005
|
|
|
830
1006
|
A page beside it can declare its path relative to the prefix:
|
|
831
1007
|
|
|
832
|
-
```tsx title="src/
|
|
1008
|
+
```tsx title="src/web/products/index.page.tsx"
|
|
833
1009
|
export const route = {
|
|
834
1010
|
path: "/",
|
|
835
1011
|
name: "products.index",
|
|
@@ -844,17 +1020,17 @@ The effective URL is `/products`. A page with `route.path = "/:id"` under the sa
|
|
|
844
1020
|
|
|
845
1021
|
## Prefix composition
|
|
846
1022
|
|
|
847
|
-
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.
|
|
1023
|
+
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.
|
|
848
1024
|
|
|
849
|
-
```tsx title="src/
|
|
1025
|
+
```tsx title="src/web/users/layout.tsx"
|
|
850
1026
|
export const prefix = "/users";
|
|
851
1027
|
```
|
|
852
1028
|
|
|
853
|
-
```tsx title="src/
|
|
1029
|
+
```tsx title="src/web/users/account/layout.tsx"
|
|
854
1030
|
export const prefix = "/account";
|
|
855
1031
|
```
|
|
856
1032
|
|
|
857
|
-
```tsx title="src/
|
|
1033
|
+
```tsx title="src/web/users/account/settings.page.tsx"
|
|
858
1034
|
export const route = {
|
|
859
1035
|
path: "/settings",
|
|
860
1036
|
name: "users.account.settings",
|
|
@@ -877,6 +1053,14 @@ The two prefix-only layouts above are legal because neither renders. Add a defau
|
|
|
877
1053
|
|
|
878
1054
|
Non-rendering layouts may carry prefixes and middleware and may nest freely. Do not delete a middleware-only authorization boundary to satisfy the rendering limit; consolidate only the default-export wrappers.
|
|
879
1055
|
|
|
1056
|
+
## `404.page.tsx` never gets a layout
|
|
1057
|
+
|
|
1058
|
+
A `404.page.tsx` renders with no layouts, even when it sits in a directory with a rendering `layout.tsx` above it. Discovery reports an empty layout chain for the not-found page only, and both the client hydration registry and the production route table read from that same chain — the server has always rendered it with no layout wrapper, so the client no longer hydrates one either. A page that exists to handle failure must not depend on app chrome that can itself throw or need data.
|
|
1059
|
+
|
|
1060
|
+
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.
|
|
1061
|
+
|
|
1062
|
+
`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.
|
|
1063
|
+
|
|
880
1064
|
## Why layout state persists
|
|
881
1065
|
|
|
882
1066
|
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.
|
|
@@ -889,11 +1073,15 @@ Client navigation rebuilds the Layout + Page element tree at the same `#root` po
|
|
|
889
1073
|
|
|
890
1074
|
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).
|
|
891
1075
|
|
|
1076
|
+
## The client boundary
|
|
1077
|
+
|
|
1078
|
+
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).
|
|
1079
|
+
|
|
892
1080
|
## Gotchas
|
|
893
1081
|
|
|
894
1082
|
- **`layout.tsx` is positional.** It applies by directory ancestry; moving a page can change its layout and URL prefix together.
|
|
895
1083
|
- **Only one layout on a path may render.** Multiple prefix/middleware-only layouts are fine; multiple default exports are not.
|
|
896
|
-
- **A layout prefix changes the registered URL.** Check the composed path, not only the page's `route.path`.
|
|
1084
|
+
- **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`.
|
|
897
1085
|
- **Keep `prefix` literal.** The build parses it without executing the module.
|
|
898
1086
|
- **Use `LayoutProps<typeof loader>`.** Bare `LayoutProps` is for a layout with no loader and gives `data` as `undefined`.
|
|
899
1087
|
- **Layout persistence is type-and-position based.** Changing to another layout component remounts it, as normal React reconciliation requires.
|
|
@@ -955,6 +1143,10 @@ The browser hydrates `#root`, not the whole document. The client tree deliberate
|
|
|
955
1143
|
|
|
956
1144
|
Because App is outside the hydrated subtree, put client state that must survive navigation in a layout or component beneath `#root`, not in the document root.
|
|
957
1145
|
|
|
1146
|
+
**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.
|
|
1147
|
+
|
|
1148
|
+
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.
|
|
1149
|
+
|
|
958
1150
|
## `<Head />`
|
|
959
1151
|
|
|
960
1152
|
`<Head />` renders the resolved page metadata at that position. It takes no props and emits:
|
package/llms.txt
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
> Package: `@warlock.js/web`
|
|
4
4
|
|
|
5
|
+
> Warlock.js web layer for the SSR page pipeline and client runtime.
|
|
6
|
+
|
|
5
7
|
> SSR React pages served by the same Warlock HTTP server that serves the API — a backend framework that renders React, not a React framework.
|
|
6
8
|
|
|
7
9
|
> First published release: 5.0.0. `@warlock.js/web` had never been published to npm before this release.
|
|
8
10
|
|
|
11
|
+
> Use 5.1 or newer. In an installed 5.0.0–5.0.2 app no client JavaScript ran at all: the dev server served `react-dom/client` as raw CommonJS, so `hydrateRoot` was missing and hydration failed on import — dead `useState`, no HMR, `<Link>` falling back to full page loads. Pages still server-rendered, which is why it looked like a React problem.
|
|
12
|
+
|
|
9
13
|
> Consumer entry points: `@warlock.js/web`, `@warlock.js/web/client/runtime`, `@warlock.js/web/connector`, and `@warlock.js/web/vite`. The published `@warlock.js/web/hydration` subpath is a framework build input; application code never imports it.
|
|
10
14
|
|
|
11
15
|
> Deliberate non-goals: server actions are a v2 design decision rather than a missing v1 feature; page routes reject regex parameters, optional parameters, and multiple parameters in one segment.
|
|
@@ -13,8 +17,8 @@
|
|
|
13
17
|
## Skills
|
|
14
18
|
|
|
15
19
|
- [add-web-to-an-app](@warlock.js/web/add-web-to-an-app/SKILL.md): Install the SSR page layer with `warlock add web`: add React/Vite peers, scaffold `src/web/root.tsx` and `src/web/home.page.tsx`, register `webConnector()`, and safely relocate the stock top-level `GET "/"` JSON route to `/welcome`. Triggers: `warlock add web`, `webConnector`, `src/web/root.tsx`, `src/web/home.page.tsx`, `GET "/welcome"`; "add web to an app", "install Warlock web", "scaffold SSR", "homepage route collision". Skip: author a page — `@warlock.js/web/create-a-page/SKILL.md`; customize the document — `@warlock.js/web/write-the-root/SKILL.md`; dev/build/start commands — `@warlock.js/core/run-app/SKILL.md`; competing installers `create-next-app`, `vite create`, `remix init`.
|
|
16
|
-
- [create-a-page](@warlock.js/web/create-a-page/SKILL.md): Create an SSR React page under `src/web
|
|
17
|
-
- [load-page-data](@warlock.js/web/load-page-data/SKILL.md): 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.
|
|
20
|
+
- [create-a-page](@warlock.js/web/create-a-page/SKILL.md): 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.
|
|
21
|
+
- [load-page-data](@warlock.js/web/load-page-data/SKILL.md): 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.
|
|
18
22
|
- [navigate-on-the-client](@warlock.js/web/navigate-on-the-client/SKILL.md): Navigate hydrated pages with `<Link>`, resolve named URLs with `href()`, use `navigateTo` / `navigateBack`, prefetch on interaction, inspect the server match with `currentRoute()`, and re-fetch loaders after a mutation with `refresh()`. Triggers: `Link`, `href`, `navigateTo`, `navigateBack`, `refresh`, `currentRoute`, `previousRoute`; "navigate without a reload", "link to a named route", "refresh page data", "revalidate loaders", "client-side back"; typical import `import { Link, refresh } from "@warlock.js/web"`. Skip: define a page route — `@warlock.js/web/create-a-page/SKILL.md`; loader mechanics — `@warlock.js/web/load-page-data/SKILL.md`; root hydration boundary — `@warlock.js/web/write-the-root/SKILL.md`; competing routers `@mongez/react-router`, `react-router-dom`, Next navigation.
|
|
19
23
|
- [serve-styles](@warlock.js/web/serve-styles/SKILL.md): Serve CSS imported by `root.tsx` or `*.page.tsx`, with render-blocking `<link rel="stylesheet">` delivery from Vite source URLs in development and Vite manifest assets in production. Triggers: `import "./app.css"`, page CSS, `?direct`, `manifest.json`, stylesheet flash, FOUC, `<head>`; "add global styles", "style a page", "CSS missing in SSR", "page flashes unstyled", "serve CSS in production". Skip: root document markup — `@warlock.js/web/write-the-root/SKILL.md`; page authoring — `@warlock.js/web/create-a-page/SKILL.md`; client navigation — `@warlock.js/web/navigate-on-the-client/SKILL.md`; competing styling systems CSS-in-JS, Next CSS, styled-components.
|
|
20
24
|
- [use-layouts](@warlock.js/web/use-layouts/SKILL.md): Wrap pages with positional `layout.tsx` modules, compose literal `prefix` exports, load typed layout data with `LayoutLoader` / `LayoutProps`, and preserve layout state during client navigation. Triggers: `layout.tsx`, `prefix`, `LayoutLoader`, `LayoutProps`, `children`; "add a page layout", "share navigation between pages", "prefix page routes", "keep a layout mounted". Skip: full-document root — `@warlock.js/web/write-the-root/SKILL.md`; page route export — `@warlock.js/web/create-a-page/SKILL.md`; loader and shared lifecycle — `@warlock.js/web/load-page-data/SKILL.md`; competing layout systems `next/layout`, React Router outlets, Remix nested routes.
|
package/package.json
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
},
|
|
12
12
|
"peerDependencies": {
|
|
13
13
|
"@vitejs/plugin-react": "^5.2.0",
|
|
14
|
-
"@warlock.js/core": "5.0
|
|
15
|
-
"@warlock.js/seal": "5.0
|
|
14
|
+
"@warlock.js/core": "5.2.0",
|
|
15
|
+
"@warlock.js/seal": "5.2.0",
|
|
16
16
|
"react": "*",
|
|
17
17
|
"react-dom": "*",
|
|
18
18
|
"vite": ">=7.3.5 <8"
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
],
|
|
38
38
|
"author": "hassanzohdy",
|
|
39
39
|
"license": "MIT",
|
|
40
|
-
"version": "5.0
|
|
40
|
+
"version": "5.2.0",
|
|
41
41
|
"type": "module",
|
|
42
42
|
"main": "./esm/index.mjs",
|
|
43
43
|
"module": "./esm/index.mjs",
|
|
@@ -10,7 +10,7 @@ description: 'Install the SSR page layer with `warlock add web`: add React/Vite
|
|
|
10
10
|
## The shape
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
|
|
13
|
+
pnpm warlock add web
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
On the ordinary path it leaves this application-owned shape:
|
|
@@ -81,8 +81,8 @@ On the refusal path, free `/` yourself and create a page with either `route = "/
|
|
|
81
81
|
Read the files rather than assuming the command could patch every application shape:
|
|
82
82
|
|
|
83
83
|
```bash
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
pnpm warlock routes --method GET --path /
|
|
85
|
+
pnpm warlock routes --method GET --path /welcome
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
The desired result is one page route at `/` and, when the stock JSON route existed, one API route at `/welcome`.
|