@warlock.js/web 5.0.2 → 5.1.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 +76 -0
- package/esm/build/contribution.d.mts +10 -0
- package/esm/build/contribution.mjs +36 -0
- package/esm/build/contribution.mjs.map +1 -1
- package/esm/build/discover-pages.mjs +226 -12
- package/esm/build/discover-pages.mjs.map +1 -1
- package/esm/build/generate-client-registry.mjs.map +1 -1
- package/esm/client/navigation/navigation-root.mjs +43 -6
- package/esm/client/navigation/navigation-root.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/metadata.d.mts +14 -0
- package/esm/metadata.mjs +45 -0
- package/esm/metadata.mjs.map +1 -0
- package/esm/routing/url-fragment.mjs +120 -0
- package/esm/routing/url-fragment.mjs.map +1 -0
- package/esm/server/create-page-route-handler.d.mts +27 -0
- package/esm/server/create-page-route-handler.mjs +12 -10
- package/esm/server/create-page-route-handler.mjs.map +1 -1
- package/esm/server/index.d.mts +2 -1
- package/esm/server/index.mjs +2 -1
- package/esm/server/install-page-routes-from-manifest.mjs +23 -1
- package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
- package/esm/server/install-page-routes.d.mts +4 -2
- package/esm/server/install-page-routes.mjs +33 -5
- package/esm/server/install-page-routes.mjs.map +1 -1
- package/esm/server/install-production-page-routes.mjs +6 -1
- package/esm/server/install-production-page-routes.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/web-connector-factory.mjs +2 -1
- package/esm/server/web-connector-factory.mjs.map +1 -1
- package/esm/server/web-connector.mjs +95 -0
- package/esm/server/web-connector.mjs.map +1 -1
- package/esm/vite/hydration-entries.mjs +8 -4
- package/esm/vite/hydration-entries.mjs.map +1 -1
- package/esm/vite/page-registry-plugin.mjs +211 -0
- package/esm/vite/page-registry-plugin.mjs.map +1 -1
- package/llms-full.txt +33 -5
- package/llms.txt +4 -2
- package/package.json +3 -3
- package/skills/create-a-page/SKILL.md +18 -2
- package/skills/navigate-on-the-client/SKILL.md +2 -0
- package/skills/use-layouts/SKILL.md +6 -0
- package/skills/write-the-root/SKILL.md +2 -0
package/llms-full.txt
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
> First published release: 5.0.0. `@warlock.js/web` had never been published to npm before this release.
|
|
8
8
|
|
|
9
|
+
> 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.
|
|
10
|
+
|
|
9
11
|
> 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
12
|
|
|
11
13
|
> 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
14
|
|
|
13
|
-
>
|
|
15
|
+
> Hand-maintained. Concatenates every SKILL.md and reference file under `@warlock.js/web/skills/`. Update this file by hand when the package's public API changes.
|
|
14
16
|
|
|
15
17
|
## add-web-to-an-app `@warlock.js/web/add-web-to-an-app/SKILL.md`
|
|
16
18
|
|
|
@@ -122,7 +124,7 @@ The desired result is one page route at `/` and, when the stock JSON route exist
|
|
|
122
124
|
|
|
123
125
|
---
|
|
124
126
|
name: create-a-page
|
|
125
|
-
description: 'Create an SSR React page under `src/web/**` or `src/app/<module>/web/**` with a literal `route`, a default component, an optional typed `loader`, and page `metadata
|
|
127
|
+
description: 'Create an SSR React page under `src/web/**` or `src/app/<module>/web/**` with a literal `route`, a default component, an optional typed `loader`, and page `metadata`; know when a dev edit gets Fast Refresh vs. a full reload. Triggers: `*.page.tsx`, `route`, `PageLoader`, `PageProps`, `PageMetadata`, Fast Refresh, HMR; "create a page", "add an SSR route", "make a React page", "type page loader data", "why did my edit reload the page"; 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
128
|
---
|
|
127
129
|
|
|
128
130
|
# Warlock — create a page
|
|
@@ -184,7 +186,9 @@ export default function ContactPage() {
|
|
|
184
186
|
}
|
|
185
187
|
```
|
|
186
188
|
|
|
187
|
-
The `route` export is required
|
|
189
|
+
The `route` export is required, and a page without one is **refused, not skipped**. `warlock dev` and the production discovery pass throw the same `MissingRouteExportError`, naming the file, because a page with no route is a page nothing can ever reach.
|
|
190
|
+
|
|
191
|
+
Changed in 5.1: through 5.0.2 the dev server silently skipped a route-less page, so the file you had just written 404'd with nothing said. Dev and build now reach the same verdict from the same condition.
|
|
188
192
|
|
|
189
193
|
## Route declarations
|
|
190
194
|
|
|
@@ -203,6 +207,8 @@ export const route = {
|
|
|
203
207
|
|
|
204
208
|
Prefer an explicit stable `name` for links. Without one, Warlock derives a name from the module and declared path: a module page gets `<module>.<path-as-dots>`, a global root page gets `index`, and another global page gets its dotted path.
|
|
205
209
|
|
|
210
|
+
**Only the route NAME is ever derived — never the route PATH.** There is no filename-to-URL convention in Warlock and there never has been. Omitting `route` does not fall back to the file's location; it fails (see above). Every segment of a page's URL is written down somewhere: the page's own `route.path`, prefixed by the literal `prefix` exports of the positional layouts above it ([use-layouts](../use-layouts/SKILL.md)). Where the file sits decides which layouts are above it — never what the path spells.
|
|
211
|
+
|
|
206
212
|
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
213
|
|
|
208
214
|
## Page-route grammar
|
|
@@ -245,9 +251,21 @@ The browser boundary is decided by the import graph, not by the file's location.
|
|
|
245
251
|
|
|
246
252
|
Keep server-only repository and service reads inside `loader`. Do not read them from module-scope initializers or the default component.
|
|
247
253
|
|
|
254
|
+
## Editing a page in development
|
|
255
|
+
|
|
256
|
+
`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.
|
|
257
|
+
|
|
258
|
+
- **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.
|
|
259
|
+
- **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.
|
|
260
|
+
- **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.
|
|
261
|
+
- **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.
|
|
262
|
+
- Creating or deleting a page file is page-graph churn, not an in-place edit; Vite handles it on its own.
|
|
263
|
+
|
|
264
|
+
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.
|
|
265
|
+
|
|
248
266
|
## Gotchas
|
|
249
267
|
|
|
250
|
-
- **Do not
|
|
268
|
+
- **Do not expect URLs to be derived from filenames.** No such convention exists. The file's location chooses discovery and which layout prefixes apply; `route` chooses the rest of the public URL. A page with no `route` is refused, not mounted at its path.
|
|
251
269
|
- **Keep `route` literal.** A computed route cannot be discovered without executing app code and is refused.
|
|
252
270
|
- **Do not annotate the loader with `: PageLoader`.** That erases the return type `PageProps` needs.
|
|
253
271
|
- **Components receive data, not HTTP objects.** `request` and `response` belong to loaders; the component also renders in the browser.
|
|
@@ -459,6 +477,8 @@ description: 'Navigate hydrated pages with `<Link>`, resolve named URLs with `hr
|
|
|
459
477
|
|
|
460
478
|
`<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
479
|
|
|
480
|
+
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).
|
|
481
|
+
|
|
462
482
|
## The shape
|
|
463
483
|
|
|
464
484
|
```tsx title="src/web/components/product-link.tsx"
|
|
@@ -785,7 +805,7 @@ The hydration entry is built from the projected client graph. CSS imports are kn
|
|
|
785
805
|
|
|
786
806
|
---
|
|
787
807
|
name: use-layouts
|
|
788
|
-
description: 'Wrap pages with positional `layout.tsx` modules, compose literal `prefix` exports, load typed layout data with `LayoutLoader` / `LayoutProps`,
|
|
808
|
+
description: 'Wrap pages with positional `layout.tsx` modules, compose literal `prefix` exports, load typed layout data with `LayoutLoader` / `LayoutProps`, preserve layout state during client navigation, and know why `404.page.tsx` never receives a layout. Triggers: `layout.tsx`, `prefix`, `LayoutLoader`, `LayoutProps`, `children`, `404.page.tsx`; "add a page layout", "share navigation between pages", "prefix page routes", "keep a layout mounted", "404 page has no layout". 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.'
|
|
789
809
|
---
|
|
790
810
|
|
|
791
811
|
# Warlock — use layouts
|
|
@@ -877,6 +897,12 @@ The two prefix-only layouts above are legal because neither renders. Add a defau
|
|
|
877
897
|
|
|
878
898
|
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
899
|
|
|
900
|
+
## `404.page.tsx` never gets a layout
|
|
901
|
+
|
|
902
|
+
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.
|
|
903
|
+
|
|
904
|
+
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.
|
|
905
|
+
|
|
880
906
|
## Why layout state persists
|
|
881
907
|
|
|
882
908
|
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.
|
|
@@ -955,6 +981,8 @@ The browser hydrates `#root`, not the whole document. The client tree deliberate
|
|
|
955
981
|
|
|
956
982
|
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
983
|
|
|
984
|
+
**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.
|
|
985
|
+
|
|
958
986
|
## `<Head />`
|
|
959
987
|
|
|
960
988
|
`<Head />` renders the resolved page metadata at that position. It takes no props and emits:
|
package/llms.txt
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
> First published release: 5.0.0. `@warlock.js/web` had never been published to npm before this release.
|
|
8
8
|
|
|
9
|
+
> 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.
|
|
10
|
+
|
|
9
11
|
> 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
12
|
|
|
11
13
|
> 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,9 +15,9 @@
|
|
|
13
15
|
## Skills
|
|
14
16
|
|
|
15
17
|
- [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/**` or `src/app/<module>/web/**` with a literal `route`, a default component, an optional typed `loader`, and page `metadata
|
|
18
|
+
- [create-a-page](@warlock.js/web/create-a-page/SKILL.md): Create an SSR React page under `src/web/**` or `src/app/<module>/web/**` with a literal `route`, a default component, an optional typed `loader`, and page `metadata`; know when a dev edit gets Fast Refresh vs. a full reload. Triggers: `*.page.tsx`, `route`, `PageLoader`, `PageProps`, `PageMetadata`, Fast Refresh, HMR; "create a page", "add an SSR route", "make a React page", "type page loader data", "why did my edit reload the page"; 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.
|
|
17
19
|
- [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.
|
|
18
20
|
- [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
21
|
- [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
|
-
- [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`,
|
|
22
|
+
- [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`, preserve layout state during client navigation, and know why `404.page.tsx` never receives a layout. Triggers: `layout.tsx`, `prefix`, `LayoutLoader`, `LayoutProps`, `children`, `404.page.tsx`; "add a page layout", "share navigation between pages", "prefix page routes", "keep a layout mounted", "404 page has no layout". 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.
|
|
21
23
|
- [write-the-root](@warlock.js/web/write-the-root/SKILL.md): Author `src/web/root.tsx`, the full-document application root that owns `<html>`, `<head>`, and `<body>`, places page metadata with `<Head />`, renders the hydrated subtree inside `#root`, and emits the payload with `<Scripts />`. Triggers: `root.tsx`, `AppProps`, `AppLoader`, `Head`, `Scripts`, `id="root"`; "customize the root document", "add html lang", "add an app provider", "where do Head and Scripts go"; typical import `import { Head, Scripts, type AppProps } from "@warlock.js/web"`. Skip: page component contract — `@warlock.js/web/create-a-page/SKILL.md`; layout wrappers — `@warlock.js/web/use-layouts/SKILL.md`; CSS delivery — `@warlock.js/web/serve-styles/SKILL.md`; competing roots `next/layout`, Remix `root`, React `createRoot`.
|
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.1.0",
|
|
15
|
+
"@warlock.js/seal": "5.1.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.1.0",
|
|
41
41
|
"type": "module",
|
|
42
42
|
"main": "./esm/index.mjs",
|
|
43
43
|
"module": "./esm/index.mjs",
|
|
@@ -62,7 +62,9 @@ export default function ContactPage() {
|
|
|
62
62
|
}
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
The `route` export is required
|
|
65
|
+
The `route` export is required, and a page without one is **refused, not skipped**. `warlock dev` and the production discovery pass throw the same `MissingRouteExportError`, naming the file, because a page with no route is a page nothing can ever reach.
|
|
66
|
+
|
|
67
|
+
Changed in 5.1: through 5.0.2 the dev server silently skipped a route-less page, so the file you had just written 404'd with nothing said. Dev and build now reach the same verdict from the same condition.
|
|
66
68
|
|
|
67
69
|
## Route declarations
|
|
68
70
|
|
|
@@ -81,6 +83,8 @@ export const route = {
|
|
|
81
83
|
|
|
82
84
|
Prefer an explicit stable `name` for links. Without one, Warlock derives a name from the module and declared path: a module page gets `<module>.<path-as-dots>`, a global root page gets `index`, and another global page gets its dotted path.
|
|
83
85
|
|
|
86
|
+
**Only the route NAME is ever derived — never the route PATH.** There is no filename-to-URL convention in Warlock and there never has been. Omitting `route` does not fall back to the file's location; it fails (see above). Every segment of a page's URL is written down somewhere: the page's own `route.path`, prefixed by the literal `prefix` exports of the positional layouts above it ([use-layouts](../use-layouts/SKILL.md)). Where the file sits decides which layouts are above it — never what the path spells.
|
|
87
|
+
|
|
84
88
|
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.
|
|
85
89
|
|
|
86
90
|
## Page-route grammar
|
|
@@ -123,9 +127,21 @@ The browser boundary is decided by the import graph, not by the file's location.
|
|
|
123
127
|
|
|
124
128
|
Keep server-only repository and service reads inside `loader`. Do not read them from module-scope initializers or the default component.
|
|
125
129
|
|
|
130
|
+
## Editing a page in development
|
|
131
|
+
|
|
132
|
+
`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.
|
|
133
|
+
|
|
134
|
+
- **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.
|
|
135
|
+
- **A `metadata`-only edit reloads the document.** `metadata` sits outside the skeleton's masked region, so the edit moves it. Warlock sends a full reload, which re-runs SSR and rebuilds `<head>`. Component state is lost — that is the price of seeing the new `<title>` without touching the browser.
|
|
136
|
+
- **Any module-level change reloads, not just `metadata`.** An edited import, a module-level declaration, or an edit confined to `route`, `middleware`, `validation`, or `loader` all move the skeleton the same way and take the same full-reload path.
|
|
137
|
+
- **A helper function used only by the JSX still reloads if it is declared at module level.** The rule does not try to prove which half of a shared declaration the edit was "really" for — it over-approximates deliberately, because a false reload only costs component state, while a missed one ships a stale `<head>` and calls it a hot update.
|
|
138
|
+
- Creating or deleting a page file is page-graph churn, not an in-place edit; Vite handles it on its own.
|
|
139
|
+
|
|
140
|
+
Changed in 5.1: a metadata-only edit previously left a stale `<head>` until you refreshed the browser by hand. The current skeleton-comparison rule replaces that earlier, narrower "metadata-only" special case.
|
|
141
|
+
|
|
126
142
|
## Gotchas
|
|
127
143
|
|
|
128
|
-
- **Do not
|
|
144
|
+
- **Do not expect URLs to be derived from filenames.** No such convention exists. The file's location chooses discovery and which layout prefixes apply; `route` chooses the rest of the public URL. A page with no `route` is refused, not mounted at its path.
|
|
129
145
|
- **Keep `route` literal.** A computed route cannot be discovered without executing app code and is refused.
|
|
130
146
|
- **Do not annotate the loader with `: PageLoader`.** That erases the return type `PageProps` needs.
|
|
131
147
|
- **Components receive data, not HTTP objects.** `request` and `response` belong to loaders; the component also renders in the browser.
|
|
@@ -7,6 +7,8 @@ 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 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).
|
|
11
|
+
|
|
10
12
|
## The shape
|
|
11
13
|
|
|
12
14
|
```tsx title="src/web/components/product-link.tsx"
|
|
@@ -92,6 +92,12 @@ The two prefix-only layouts above are legal because neither renders. Add a defau
|
|
|
92
92
|
|
|
93
93
|
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.
|
|
94
94
|
|
|
95
|
+
## `404.page.tsx` never gets a layout
|
|
96
|
+
|
|
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 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.
|
|
98
|
+
|
|
99
|
+
This is scoped to the not-found page: an ordinary page in the same directory still gets its full layout chain, and nested-layout refusal on the 404's own path is still enforced exactly as it is for any other page.
|
|
100
|
+
|
|
95
101
|
## Why layout state persists
|
|
96
102
|
|
|
97
103
|
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.
|
|
@@ -46,6 +46,8 @@ 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
|
+
|
|
49
51
|
## `<Head />`
|
|
50
52
|
|
|
51
53
|
`<Head />` renders the resolved page metadata at that position. It takes no props and emits:
|