@warlock.js/web 5.2.3 → 5.3.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/esm/build/contribution.mjs.map +1 -1
- package/esm/build/discover-pages.mjs +5 -7
- package/esm/build/discover-pages.mjs.map +1 -1
- package/esm/build/generate-client-registry.mjs.map +1 -1
- package/esm/build/generate-pages-barrel.mjs.map +1 -1
- package/esm/build/page-default-export.mjs.map +1 -1
- package/esm/build/page-routes-manifest.mjs.map +1 -1
- package/esm/build/public-files.mjs.map +1 -1
- package/esm/build/read-route-exports.mjs.map +1 -1
- package/esm/client/build-hydrated-tree.mjs.map +1 -1
- package/esm/client/hydrate-page.mjs +4 -3
- package/esm/client/hydrate-page.mjs.map +1 -1
- package/esm/client/navigation/fetch-page-data.mjs +2 -2
- package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
- package/esm/client/navigation/navigation-root.mjs +5 -1
- package/esm/client/navigation/navigation-root.mjs.map +1 -1
- package/esm/client/navigation/prefetch.mjs.map +1 -1
- package/esm/client/runtime/manifest.mjs.map +1 -1
- package/esm/client/runtime/matcher.mjs.map +1 -1
- package/esm/components/document-context.mjs.map +1 -1
- package/esm/components/link.mjs.map +1 -1
- package/esm/hydration-payload.mjs +6 -2
- package/esm/hydration-payload.mjs.map +1 -1
- package/esm/index.d.mts +4 -2
- package/esm/index.mjs +2 -1
- package/esm/localization.d.mts +21 -0
- package/esm/localization.mjs +28 -0
- package/esm/localization.mjs.map +1 -0
- package/esm/routing/data-request.mjs +5 -3
- package/esm/routing/data-request.mjs.map +1 -1
- package/esm/routing/filesystem-route.mjs +36 -7
- package/esm/routing/filesystem-route.mjs.map +1 -1
- package/esm/routing/layout-policy.mjs.map +1 -1
- package/esm/routing/page-file-segment.mjs +66 -0
- package/esm/routing/page-file-segment.mjs.map +1 -0
- package/esm/routing/page-route-grammar.mjs +79 -0
- package/esm/routing/page-route-grammar.mjs.map +1 -0
- package/esm/routing/query-string.mjs.map +1 -1
- package/esm/routing/route-identity.d.mts +69 -0
- package/esm/routing/route-identity.mjs +100 -44
- package/esm/routing/route-identity.mjs.map +1 -1
- package/esm/routing/route-table.mjs.map +1 -1
- package/esm/server/build-hydration-payload.mjs +3 -2
- package/esm/server/build-hydration-payload.mjs.map +1 -1
- package/esm/server/create-page-route-handler.d.mts +34 -1
- package/esm/server/create-page-route-handler.mjs +35 -4
- package/esm/server/create-page-route-handler.mjs.map +1 -1
- package/esm/server/execute-page-request.mjs.map +1 -1
- package/esm/server/framework-default-not-found-stylesheet.mjs +102 -0
- package/esm/server/framework-default-not-found-stylesheet.mjs.map +1 -0
- package/esm/server/install-page-routes-from-manifest.mjs +13 -16
- package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
- package/esm/server/install-page-routes.d.mts +3 -1
- package/esm/server/install-page-routes.mjs +11 -11
- package/esm/server/install-page-routes.mjs.map +1 -1
- package/esm/server/match-page-route.mjs.map +1 -1
- package/esm/server/not-found-page.d.mts +1 -13
- package/esm/server/not-found-page.mjs +50 -5
- package/esm/server/not-found-page.mjs.map +1 -1
- package/esm/server/page-file-change.mjs.map +1 -1
- package/esm/server/page-route-reload.mjs.map +1 -1
- package/esm/server/register-production-public-files.mjs +16 -1
- package/esm/server/register-production-public-files.mjs.map +1 -1
- package/esm/server/render-page.d.mts +1 -8
- package/esm/server/render-page.mjs +24 -21
- package/esm/server/render-page.mjs.map +1 -1
- package/esm/server/response-cache-floor.mjs +79 -0
- package/esm/server/response-cache-floor.mjs.map +1 -0
- package/esm/server/set-cookie-cache-floor-hook.mjs +41 -0
- package/esm/server/set-cookie-cache-floor-hook.mjs.map +1 -0
- package/esm/server/settle-page-response.mjs.map +1 -1
- package/esm/server/stylesheet-urls.mjs.map +1 -1
- package/esm/server/unregistered-pages.mjs.map +1 -1
- 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 +23 -4
- package/esm/server/web-connector.mjs.map +1 -1
- package/esm/shared.mjs.map +1 -1
- package/esm/vite/build-client.mjs.map +1 -1
- package/esm/vite/gate-a-resolve.mjs.map +1 -1
- package/esm/vite/gate-b-secrets.mjs.map +1 -1
- package/esm/vite/gate-c-verify.mjs.map +1 -1
- package/esm/vite/hydration-entries.mjs.map +1 -1
- package/esm/vite/index.mjs.map +1 -1
- package/esm/vite/page-registry-plugin.mjs.map +1 -1
- package/esm/vite/projection.mjs.map +1 -1
- package/llms-full.txt +62 -33
- package/llms.txt +1 -1
- package/package.json +4 -3
- package/skills/create-a-page/SKILL.md +59 -28
- package/skills/navigate-on-the-client/SKILL.md +16 -11
- package/skills/serve-styles/SKILL.md +2 -1
- package/skills/use-layouts/SKILL.md +3 -6
- package/skills/write-the-root/SKILL.md +0 -2
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: create-a-page
|
|
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.'
|
|
3
|
+
description: 'Create an SSR React page under `src/web/**`, with either a literal `route` or a filesystem-derived one, an explicit public-cache opt-in, a default component, an optional typed `loader`, page `metadata`, the `error.page.tsx` boundary, and the universal `register()` hook. Triggers: `*.page.tsx`, `route`, `route.cache`, `maxAge`, `PageLoader`, `PageProps`, `PageMetadata`, `error.page.tsx`, `register`, `[...slug]`; "create a page", "cache a public 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 `src/web/` — the
|
|
8
|
+
A page is any `*.page.tsx` beneath `src/web/` — the page root. 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
|
|
|
@@ -15,13 +15,12 @@ import type { PageLoader, PageMetadata, PageProps } from "@warlock.js/web";
|
|
|
15
15
|
export const route = {
|
|
16
16
|
path: "/products/:id",
|
|
17
17
|
name: "products.details",
|
|
18
|
+
cache: { public: true, maxAge: 60 },
|
|
18
19
|
} as const;
|
|
19
20
|
|
|
20
|
-
export const loader = (async ({ request
|
|
21
|
+
export const loader = (async ({ request }) => {
|
|
21
22
|
const id = request.input("id");
|
|
22
23
|
|
|
23
|
-
response.header("cache-control", "private, max-age=60");
|
|
24
|
-
|
|
25
24
|
return {
|
|
26
25
|
product: {
|
|
27
26
|
id,
|
|
@@ -64,8 +63,6 @@ export default function ContactPage() {
|
|
|
64
63
|
|
|
65
64
|
`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
65
|
|
|
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
66
|
### The default export is required
|
|
70
67
|
|
|
71
68
|
`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**:
|
|
@@ -76,8 +73,6 @@ file must default-export the React component it renders. For example:
|
|
|
76
73
|
`export default function Page() { return <main />; }`
|
|
77
74
|
```
|
|
78
75
|
|
|
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
76
|
Two details worth knowing:
|
|
82
77
|
|
|
83
78
|
- **`export { Page as default }` satisfies the rule** — the check is for a runtime default binding, not for the `export default` keyword form specifically.
|
|
@@ -97,6 +92,7 @@ export const route = "/products";
|
|
|
97
92
|
export const route = {
|
|
98
93
|
path: "/products/:id",
|
|
99
94
|
name: "products.details",
|
|
95
|
+
cache: { public: true, maxAge: 60 },
|
|
100
96
|
} as const;
|
|
101
97
|
```
|
|
102
98
|
|
|
@@ -106,25 +102,53 @@ Every segment of a page's URL is written down somewhere: `route.path` (or the de
|
|
|
106
102
|
|
|
107
103
|
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.
|
|
108
104
|
|
|
105
|
+
## Page caching
|
|
106
|
+
|
|
107
|
+
Page documents and their `x-warlock-data` representations are `no-store` by
|
|
108
|
+
default. Opt a public page into shared caching on its route:
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
export const route = {
|
|
112
|
+
path: "/products",
|
|
113
|
+
name: "products.index",
|
|
114
|
+
cache: { public: true, maxAge: 60 },
|
|
115
|
+
} as const;
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`maxAge` is seconds. Both keys are required: `cache: { public: true }` is a
|
|
119
|
+
boot-time `InvalidPageCacheOptInError`, because the framework will not invent
|
|
120
|
+
a freshness window. Remove `cache` entirely to keep the safe `no-store`
|
|
121
|
+
default.
|
|
122
|
+
|
|
123
|
+
The declaration is an opt-in, not an override of request safety. A response
|
|
124
|
+
that sets or clears a cookie, or a request that used authenticated state,
|
|
125
|
+
emits `Cache-Control: private, no-store`. If Warlock cannot determine whether
|
|
126
|
+
the request used authenticated state, it revokes the opt-in and emits
|
|
127
|
+
`Cache-Control: no-store`. Only a provably unauthenticated request with no
|
|
128
|
+
`Set-Cookie` can emit `public, max-age=<maxAge>`.
|
|
129
|
+
|
|
130
|
+
This decision is applied once after loaders finish, to both representations.
|
|
131
|
+
Setting `Cache-Control` manually in a loader cannot bypass the floor.
|
|
132
|
+
|
|
109
133
|
## Filesystem routing
|
|
110
134
|
|
|
111
135
|
Omit `route` and the URL comes from the page's own path beneath `src/web`:
|
|
112
136
|
|
|
113
137
|
- 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`.
|
|
138
|
+
- A `(group)` directory — parentheses, not braces — contributes nothing to the URL, only to organization: `src/web/(marketing)/pricing.page.tsx` derives `/pricing`. Bracket syntax inside a group name is refused at boot because it can never contribute a dynamic segment; use `(marketing)/[id]/page.page.tsx`, not `(marketing[id])/page.page.tsx`.
|
|
115
139
|
- `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
140
|
- `[id]` becomes `:id`: `src/web/products/[id].page.tsx` derives `/products/:id`.
|
|
117
141
|
- 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
142
|
|
|
119
143
|
Two pages that derive (or declare) the same effective path is a build error naming both files.
|
|
120
144
|
|
|
121
|
-
### Catch-all segments are
|
|
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.
|
|
145
|
+
### Catch-all segments are refused
|
|
126
146
|
|
|
127
|
-
|
|
147
|
+
There is no catch-all / rest-parameter form in filesystem routing. Only
|
|
148
|
+
`[name]` — a plain identifier in square brackets — is recognized as a
|
|
149
|
+
dynamic segment. `src/web/docs/[...slug].page.tsx` raises
|
|
150
|
+
`PageFileSegmentNotSupportedError` at boot and names both the page file and
|
|
151
|
+
the rejected segment.
|
|
128
152
|
|
|
129
153
|
Until a catch-all exists, use the terminal wildcard with an explicit `route`:
|
|
130
154
|
|
|
@@ -141,6 +165,11 @@ Page routes deliberately accept less than API routes:
|
|
|
141
165
|
|
|
142
166
|
Write two pages for an optional segment. Validate a constrained parameter in the page instead of putting a regex in its path.
|
|
143
167
|
|
|
168
|
+
An unsupported declared path is a boot-time
|
|
169
|
+
`PageRoutePathNotSupportedError`, never a literal or normalized route.
|
|
170
|
+
Examples that fail include `/users/:id?`, `/users/:id(\\d+)`,
|
|
171
|
+
`/near/:lat-:lng`, `/a//b`, and `/users/`.
|
|
172
|
+
|
|
144
173
|
## Metadata
|
|
145
174
|
|
|
146
175
|
`metadata` may be a static object or a function of the resolved loader data and readonly `shared` payload:
|
|
@@ -180,6 +209,10 @@ export default function ErrorPage({ error, status }: ErrorPageProps) {
|
|
|
180
209
|
|
|
181
210
|
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
211
|
|
|
212
|
+
Every unhandled error response is also forced to
|
|
213
|
+
`Cache-Control: private, no-store` at the framework's shared error funnel.
|
|
214
|
+
This includes page and API-route failures.
|
|
215
|
+
|
|
183
216
|
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
217
|
|
|
185
218
|
## The `register()` hook
|
|
@@ -205,16 +238,16 @@ The browser boundary is decided by the import graph, not by the file's location.
|
|
|
205
238
|
|
|
206
239
|
Keep server-only repository and service reads inside `loader`. Do not read them from module-scope initializers or the default component.
|
|
207
240
|
|
|
208
|
-
### `.client` does not isolate SSR
|
|
241
|
+
### `.client` does not isolate SSR
|
|
209
242
|
|
|
210
243
|
A `.client.tsx` suffix is a naming convention, not an SSR-isolation boundary. A
|
|
211
244
|
module statically imported by a page, layout, root, or any of their imports is
|
|
212
245
|
still evaluated by the server. Top-level browser globals such as `window`
|
|
213
|
-
therefore crash SSR boot. Warlock
|
|
246
|
+
therefore crash SSR boot. Warlock does not ship a client-only component
|
|
214
247
|
primitive; code that requires browser globals at module scope cannot be part of
|
|
215
248
|
the SSR page graph.
|
|
216
249
|
|
|
217
|
-
### Static assets use `public/`
|
|
250
|
+
### Static assets use `public/`
|
|
218
251
|
|
|
219
252
|
The production server build does not support imported non-stylesheet assets. An
|
|
220
253
|
import such as `import logo from "./logo.svg"` works under Vite in development
|
|
@@ -225,7 +258,7 @@ are the exception and remain supported.
|
|
|
225
258
|
|
|
226
259
|
## Editing a page in development
|
|
227
260
|
|
|
228
|
-
`warlock dev` decides Fast Refresh vs. a full reload by comparing the module's
|
|
261
|
+
`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.
|
|
229
262
|
|
|
230
263
|
- **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.
|
|
231
264
|
- **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.
|
|
@@ -233,21 +266,19 @@ are the exception and remain supported.
|
|
|
233
266
|
- **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.
|
|
234
267
|
- 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.
|
|
235
268
|
|
|
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.
|
|
237
|
-
|
|
238
269
|
## Route-table changes in development
|
|
239
270
|
|
|
240
271
|
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
272
|
|
|
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
|
-
|
|
244
273
|
## Gotchas
|
|
245
274
|
|
|
246
|
-
- **A page with no `route` is not unreachable.** It derives a real URL from its file location — see [Filesystem routing](#filesystem-routing).
|
|
275
|
+
- **A page with no `route` is not unreachable.** It derives a real URL from its file location — see [Filesystem routing](#filesystem-routing).
|
|
247
276
|
- **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
|
|
249
|
-
- **Imported static assets do not build
|
|
250
|
-
- **`[...slug]` is not a catch-all.** It
|
|
277
|
+
- **`.client.tsx` does not prevent SSR evaluation.** It is a naming convention, not a client-only component primitive.
|
|
278
|
+
- **Imported static assets do not build.** Put them in `public/` and reference their root URL; CSS imports remain supported.
|
|
279
|
+
- **`[...slug]` is not a catch-all.** It fails boot with
|
|
280
|
+
`PageFileSegmentNotSupportedError`; use an explicit terminal `*` route
|
|
281
|
+
instead — see [Catch-all segments are refused](#catch-all-segments-are-refused).
|
|
251
282
|
- **`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).
|
|
252
283
|
- **Keep `route` literal.** A computed route cannot be discovered without executing app code and is refused.
|
|
253
284
|
- **Do not annotate the loader with `: PageLoader`.** That erases the return type `PageProps` needs.
|
|
@@ -7,7 +7,7 @@ description: 'Navigate hydrated pages with `<Link>`, resolve named URLs with `hr
|
|
|
7
7
|
|
|
8
8
|
`<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.
|
|
9
9
|
|
|
10
|
-
Every behaviour on this page
|
|
10
|
+
Every behaviour on this page depends on hydration having mounted. See [write-the-root](../write-the-root/SKILL.md#root-is-the-hydration-boundary).
|
|
11
11
|
|
|
12
12
|
## The shape
|
|
13
13
|
|
|
@@ -16,12 +16,7 @@ import { Link } from "@warlock.js/web";
|
|
|
16
16
|
|
|
17
17
|
export function ProductLink({ id }: { id: string }) {
|
|
18
18
|
return (
|
|
19
|
-
<Link
|
|
20
|
-
to="products.details"
|
|
21
|
-
params={{ id }}
|
|
22
|
-
query={{ tab: "specifications" }}
|
|
23
|
-
prefetch
|
|
24
|
-
>
|
|
19
|
+
<Link to="products.details" params={{ id }} query={{ tab: "specifications" }} prefetch>
|
|
25
20
|
View product
|
|
26
21
|
</Link>
|
|
27
22
|
);
|
|
@@ -42,7 +37,9 @@ export function NavigationLinks() {
|
|
|
42
37
|
<nav>
|
|
43
38
|
<Link to="products.index">Products</Link>
|
|
44
39
|
<Link href="/pricing">Pricing</Link>
|
|
45
|
-
<Link href="https://example.com/docs" newTab>
|
|
40
|
+
<Link href="https://example.com/docs" newTab>
|
|
41
|
+
External docs
|
|
42
|
+
</Link>
|
|
46
43
|
<Link email="sales@example.com">Email sales</Link>
|
|
47
44
|
<Link tel="+201000000000">Call sales</Link>
|
|
48
45
|
</nav>
|
|
@@ -93,8 +90,12 @@ export function CheckoutButtons() {
|
|
|
93
90
|
|
|
94
91
|
return (
|
|
95
92
|
<div>
|
|
96
|
-
<button type="button" onClick={navigateBack}>
|
|
97
|
-
|
|
93
|
+
<button type="button" onClick={navigateBack}>
|
|
94
|
+
Back
|
|
95
|
+
</button>
|
|
96
|
+
<button type="button" onClick={openCheckout}>
|
|
97
|
+
Checkout
|
|
98
|
+
</button>
|
|
98
99
|
</div>
|
|
99
100
|
);
|
|
100
101
|
}
|
|
@@ -126,7 +127,11 @@ export function DeleteProductButton({ id }: { id: string }) {
|
|
|
126
127
|
}
|
|
127
128
|
};
|
|
128
129
|
|
|
129
|
-
return
|
|
130
|
+
return (
|
|
131
|
+
<button type="button" onClick={deleteProduct}>
|
|
132
|
+
Delete product
|
|
133
|
+
</button>
|
|
134
|
+
);
|
|
130
135
|
}
|
|
131
136
|
```
|
|
132
137
|
|
|
@@ -52,6 +52,7 @@ Use a bare side-effect import in `root.tsx`. In development the server parses th
|
|
|
52
52
|
|
|
53
53
|
Development has no client manifest, so Warlock reads `root.tsx`, finds bare stylesheet imports, resolves them relative to the root file, and emits a link before `</head>`:
|
|
54
54
|
|
|
55
|
+
<!-- prettier-ignore -->
|
|
55
56
|
```html
|
|
56
57
|
<link rel="stylesheet" href="/src/web/app.css?direct">
|
|
57
58
|
```
|
|
@@ -90,7 +91,7 @@ export default function ProductsPage() {
|
|
|
90
91
|
|
|
91
92
|
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
93
|
|
|
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
|
|
94
|
+
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`. Put it in the application's `public/` directory and reference its root URL instead: `public/logo.svg` is `/logo.svg`.
|
|
94
95
|
|
|
95
96
|
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.
|
|
96
97
|
|
|
@@ -23,14 +23,11 @@ export const loader = (async () => {
|
|
|
23
23
|
};
|
|
24
24
|
}) satisfies LayoutLoader;
|
|
25
25
|
|
|
26
|
-
export default function ProductsLayout({
|
|
27
|
-
data,
|
|
28
|
-
children,
|
|
29
|
-
}: LayoutProps<typeof loader>) {
|
|
26
|
+
export default function ProductsLayout({ data, children }: LayoutProps<typeof loader>) {
|
|
30
27
|
return (
|
|
31
28
|
<section>
|
|
32
29
|
<nav aria-label="Products">
|
|
33
|
-
{data.navigation.map(item => (
|
|
30
|
+
{data.navigation.map((item) => (
|
|
34
31
|
<a key={item.href} href={item.href}>
|
|
35
32
|
{item.label}
|
|
36
33
|
</a>
|
|
@@ -94,7 +91,7 @@ Non-rendering layouts may carry prefixes and middleware and may nest freely. Do
|
|
|
94
91
|
|
|
95
92
|
## `404.page.tsx` never gets a layout
|
|
96
93
|
|
|
97
|
-
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
|
|
94
|
+
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 renders it with no layout wrapper, and the client hydrates it the same way, with no layout of its own. A page that exists to handle failure must not depend on app chrome that can itself throw or need data.
|
|
98
95
|
|
|
99
96
|
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
97
|
|
|
@@ -46,8 +46,6 @@ The browser hydrates `#root`, not the whole document. The client tree deliberate
|
|
|
46
46
|
|
|
47
47
|
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.
|
|
48
48
|
|
|
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
|
-
|
|
51
49
|
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
50
|
|
|
53
51
|
## `<Head />`
|