@warlock.js/web 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +125 -0
  4. package/esm/build/contribution.d.mts +55 -0
  5. package/esm/build/contribution.mjs +132 -0
  6. package/esm/build/contribution.mjs.map +1 -0
  7. package/esm/build/discover-pages.mjs +294 -0
  8. package/esm/build/discover-pages.mjs.map +1 -0
  9. package/esm/build/generate-client-registry.mjs +123 -0
  10. package/esm/build/generate-client-registry.mjs.map +1 -0
  11. package/esm/build/generate-pages-barrel.mjs +330 -0
  12. package/esm/build/generate-pages-barrel.mjs.map +1 -0
  13. package/esm/build/read-route-exports.mjs +212 -0
  14. package/esm/build/read-route-exports.mjs.map +1 -0
  15. package/esm/client/build-hydrated-tree.mjs +113 -0
  16. package/esm/client/build-hydrated-tree.mjs.map +1 -0
  17. package/esm/client/hydrate-page.mjs +73 -0
  18. package/esm/client/hydrate-page.mjs.map +1 -0
  19. package/esm/client/navigation/current-route.d.mts +91 -0
  20. package/esm/client/navigation/current-route.mjs +66 -0
  21. package/esm/client/navigation/current-route.mjs.map +1 -0
  22. package/esm/client/navigation/fetch-page-data.mjs +97 -0
  23. package/esm/client/navigation/fetch-page-data.mjs.map +1 -0
  24. package/esm/client/navigation/navigation-root.mjs +196 -0
  25. package/esm/client/navigation/navigation-root.mjs.map +1 -0
  26. package/esm/client/navigation/prefetch.mjs +110 -0
  27. package/esm/client/navigation/prefetch.mjs.map +1 -0
  28. package/esm/client/navigation/refresh.d.mts +40 -0
  29. package/esm/client/navigation/refresh.mjs +146 -0
  30. package/esm/client/navigation/refresh.mjs.map +1 -0
  31. package/esm/client/navigation/verbs.d.mts +55 -0
  32. package/esm/client/navigation/verbs.mjs +66 -0
  33. package/esm/client/navigation/verbs.mjs.map +1 -0
  34. package/esm/client/runtime/index.d.mts +4 -0
  35. package/esm/client/runtime/index.mjs +4 -0
  36. package/esm/client/runtime/manifest.d.mts +8 -0
  37. package/esm/client/runtime/manifest.mjs +77 -0
  38. package/esm/client/runtime/manifest.mjs.map +1 -0
  39. package/esm/client/runtime/matcher.d.mts +7 -0
  40. package/esm/client/runtime/matcher.mjs +169 -0
  41. package/esm/client/runtime/matcher.mjs.map +1 -0
  42. package/esm/client/runtime/types.d.mts +22 -0
  43. package/esm/components/default-app.mjs +32 -0
  44. package/esm/components/default-app.mjs.map +1 -0
  45. package/esm/components/document-context.d.mts +1 -0
  46. package/esm/components/document-context.mjs +24 -0
  47. package/esm/components/document-context.mjs.map +1 -0
  48. package/esm/components/head.d.mts +14 -0
  49. package/esm/components/head.mjs +63 -0
  50. package/esm/components/head.mjs.map +1 -0
  51. package/esm/components/link.d.mts +157 -0
  52. package/esm/components/link.mjs +194 -0
  53. package/esm/components/link.mjs.map +1 -0
  54. package/esm/components/scripts.d.mts +14 -0
  55. package/esm/components/scripts.mjs +21 -0
  56. package/esm/components/scripts.mjs.map +1 -0
  57. package/esm/connector/index.d.mts +6 -0
  58. package/esm/connector/index.mjs +7 -0
  59. package/esm/context.d.mts +107 -0
  60. package/esm/hydration/index.d.mts +1 -0
  61. package/esm/hydration/index.mjs +33 -0
  62. package/esm/hydration/index.mjs.map +1 -0
  63. package/esm/hydration-payload.d.mts +1 -0
  64. package/esm/hydration-payload.mjs +72 -0
  65. package/esm/hydration-payload.mjs.map +1 -0
  66. package/esm/index.d.mts +35 -0
  67. package/esm/index.mjs +12 -0
  68. package/esm/loaders.d.mts +47 -0
  69. package/esm/metadata.d.mts +54 -0
  70. package/esm/props.d.mts +42 -0
  71. package/esm/route.d.mts +30 -0
  72. package/esm/routing/compose-route-path.mjs +33 -0
  73. package/esm/routing/compose-route-path.mjs.map +1 -0
  74. package/esm/routing/data-request.mjs +52 -0
  75. package/esm/routing/data-request.mjs.map +1 -0
  76. package/esm/routing/layout-policy.mjs +78 -0
  77. package/esm/routing/layout-policy.mjs.map +1 -0
  78. package/esm/routing/navigator.mjs +21 -0
  79. package/esm/routing/navigator.mjs.map +1 -0
  80. package/esm/routing/query-string.d.mts +250 -0
  81. package/esm/routing/query-string.mjs +340 -0
  82. package/esm/routing/query-string.mjs.map +1 -0
  83. package/esm/routing/route-identity.mjs +68 -0
  84. package/esm/routing/route-identity.mjs.map +1 -0
  85. package/esm/routing/route-table.d.mts +34 -0
  86. package/esm/routing/route-table.mjs +182 -0
  87. package/esm/routing/route-table.mjs.map +1 -0
  88. package/esm/routing/router-events.d.mts +159 -0
  89. package/esm/routing/router-events.mjs +83 -0
  90. package/esm/routing/router-events.mjs.map +1 -0
  91. package/esm/server/buffered-response.mjs +115 -0
  92. package/esm/server/buffered-response.mjs.map +1 -0
  93. package/esm/server/build-hydration-payload.mjs +27 -0
  94. package/esm/server/build-hydration-payload.mjs.map +1 -0
  95. package/esm/server/client-asset-url-prefix.d.mts +22 -0
  96. package/esm/server/client-asset-url-prefix.mjs +23 -0
  97. package/esm/server/client-asset-url-prefix.mjs.map +1 -0
  98. package/esm/server/create-page-module-loader.mjs +49 -0
  99. package/esm/server/create-page-module-loader.mjs.map +1 -0
  100. package/esm/server/create-page-route-handler.mjs +107 -0
  101. package/esm/server/create-page-route-handler.mjs.map +1 -0
  102. package/esm/server/dev-server.mjs +167 -0
  103. package/esm/server/dev-server.mjs.map +1 -0
  104. package/esm/server/execute-page-request.d.mts +1 -0
  105. package/esm/server/execute-page-request.mjs +166 -0
  106. package/esm/server/execute-page-request.mjs.map +1 -0
  107. package/esm/server/execute-page-request.types.d.mts +1 -0
  108. package/esm/server/hydration-client-url.d.mts +37 -0
  109. package/esm/server/hydration-client-url.mjs +101 -0
  110. package/esm/server/hydration-client-url.mjs.map +1 -0
  111. package/esm/server/index.mjs +13 -0
  112. package/esm/server/install-page-routes-from-manifest.mjs +133 -0
  113. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -0
  114. package/esm/server/install-page-routes.d.mts +1 -0
  115. package/esm/server/install-page-routes.mjs +6 -0
  116. package/esm/server/install-production-page-routes.mjs +55 -0
  117. package/esm/server/install-production-page-routes.mjs.map +1 -0
  118. package/esm/server/match-page-route.mjs +48 -0
  119. package/esm/server/match-page-route.mjs.map +1 -0
  120. package/esm/server/page-context.d.mts +1 -0
  121. package/esm/server/page-context.mjs +33 -0
  122. package/esm/server/page-context.mjs.map +1 -0
  123. package/esm/server/page-manifest.d.mts +102 -0
  124. package/esm/server/page-manifest.mjs +31 -0
  125. package/esm/server/page-manifest.mjs.map +1 -0
  126. package/esm/server/render-page.mjs +214 -0
  127. package/esm/server/render-page.mjs.map +1 -0
  128. package/esm/server/resolve-page-metadata.mjs +33 -0
  129. package/esm/server/resolve-page-metadata.mjs.map +1 -0
  130. package/esm/server/resolve-validation-data.mjs +27 -0
  131. package/esm/server/resolve-validation-data.mjs.map +1 -0
  132. package/esm/server/settle-page-response.d.mts +1 -0
  133. package/esm/server/settle-page-response.mjs +90 -0
  134. package/esm/server/settle-page-response.mjs.map +1 -0
  135. package/esm/server/stylesheet-urls.mjs +65 -0
  136. package/esm/server/stylesheet-urls.mjs.map +1 -0
  137. package/esm/server/web-connector-factory.d.mts +25 -0
  138. package/esm/server/web-connector-factory.mjs +121 -0
  139. package/esm/server/web-connector-factory.mjs.map +1 -0
  140. package/esm/server/web-connector.d.mts +34 -0
  141. package/esm/server/web-connector.mjs +516 -0
  142. package/esm/server/web-connector.mjs.map +1 -0
  143. package/esm/shared.d.mts +22 -0
  144. package/esm/shared.mjs +223 -0
  145. package/esm/shared.mjs.map +1 -0
  146. package/esm/validation.d.mts +31 -0
  147. package/esm/vite/app-convention-aliases.mjs +43 -0
  148. package/esm/vite/app-convention-aliases.mjs.map +1 -0
  149. package/esm/vite/build-client.d.mts +38 -0
  150. package/esm/vite/build-client.mjs +72 -0
  151. package/esm/vite/build-client.mjs.map +1 -0
  152. package/esm/vite/gate-a-resolve.d.mts +57 -0
  153. package/esm/vite/gate-a-resolve.mjs +834 -0
  154. package/esm/vite/gate-a-resolve.mjs.map +1 -0
  155. package/esm/vite/gate-b-secrets.d.mts +57 -0
  156. package/esm/vite/gate-b-secrets.mjs +299 -0
  157. package/esm/vite/gate-b-secrets.mjs.map +1 -0
  158. package/esm/vite/gate-c-verify.d.mts +118 -0
  159. package/esm/vite/gate-c-verify.mjs +263 -0
  160. package/esm/vite/gate-c-verify.mjs.map +1 -0
  161. package/esm/vite/hydration-entries.d.mts +17 -0
  162. package/esm/vite/hydration-entries.mjs +45 -0
  163. package/esm/vite/hydration-entries.mjs.map +1 -0
  164. package/esm/vite/index.d.mts +126 -0
  165. package/esm/vite/index.mjs +146 -0
  166. package/esm/vite/index.mjs.map +1 -0
  167. package/esm/vite/page-registry-plugin.d.mts +43 -0
  168. package/esm/vite/page-registry-plugin.mjs +138 -0
  169. package/esm/vite/page-registry-plugin.mjs.map +1 -0
  170. package/esm/vite/projection.d.mts +31 -0
  171. package/esm/vite/projection.mjs +384 -0
  172. package/esm/vite/projection.mjs.map +1 -0
  173. package/llms-full.txt +1041 -0
  174. package/llms.txt +21 -0
  175. package/package.json +77 -0
  176. package/skills/add-web-to-an-app/SKILL.md +103 -0
  177. package/skills/create-a-page/SKILL.md +141 -0
  178. package/skills/load-page-data/SKILL.md +183 -0
  179. package/skills/navigate-on-the-client/SKILL.md +182 -0
  180. package/skills/serve-styles/SKILL.md +145 -0
  181. package/skills/use-layouts/SKILL.md +121 -0
  182. package/skills/write-the-root/SKILL.md +131 -0
@@ -0,0 +1,182 @@
1
+ ---
2
+ name: navigate-on-the-client
3
+ description: '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.'
4
+ ---
5
+
6
+ # Warlock — navigate on the client
7
+
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
+
10
+ ## The shape
11
+
12
+ ```tsx title="src/web/components/product-link.tsx"
13
+ import { Link } from "@warlock.js/web";
14
+
15
+ export function ProductLink({ id }: { id: string }) {
16
+ return (
17
+ <Link
18
+ to="products.details"
19
+ params={{ id }}
20
+ query={{ tab: "specifications" }}
21
+ prefetch
22
+ >
23
+ View product
24
+ </Link>
25
+ );
26
+ }
27
+ ```
28
+
29
+ Route-name destinations resolve through the table published from the same page graph the server installs. Missing or extra params throw rather than producing a URL that silently 404s.
30
+
31
+ ## Link destinations
32
+
33
+ Pass exactly one destination prop:
34
+
35
+ ```tsx
36
+ import { Link } from "@warlock.js/web";
37
+
38
+ export function NavigationLinks() {
39
+ return (
40
+ <nav>
41
+ <Link to="products.index">Products</Link>
42
+ <Link href="/pricing">Pricing</Link>
43
+ <Link href="https://example.com/docs" newTab>External docs</Link>
44
+ <Link email="sales@example.com">Email sales</Link>
45
+ <Link tel="+201000000000">Call sales</Link>
46
+ </nav>
47
+ );
48
+ }
49
+ ```
50
+
51
+ `to` and `href` both accept a route name or literal URL:
52
+
53
+ - A value beginning with `/` or with a URI scheme is literal and is passed through.
54
+ - Any other value is a route name and is resolved through `href()`.
55
+ - `params` and `query` apply only to route names.
56
+ - `newTab` supplies `_blank` and `noopener noreferrer` unless you provide your own `target` or `rel`.
57
+
58
+ `prefetch` fetches in-app page data on hover or keyboard focus. It is ignored for external, email, telephone, new-tab, and explicitly targeted links. Prefetch is best-effort and never delays the interaction.
59
+
60
+ Modified clicks, middle clicks, downloads, another browsing context, or an earlier `preventDefault()` remain browser-owned.
61
+
62
+ ## Build a URL without React
63
+
64
+ `href(name, params?, query?)` is the durable primitive for redirects, headers, email bodies, and other non-component callers:
65
+
66
+ ```ts
67
+ import { href } from "@warlock.js/web";
68
+
69
+ const productUrl = href(
70
+ "products.details",
71
+ { id: "42" },
72
+ { tab: "reviews", tags: ["featured", "sale"] },
73
+ );
74
+ ```
75
+
76
+ Unknown route names throw at runtime with the known names. Call `href()` after the route table has been published—during a page render, event, or request—not from an eager module initializer before boot.
77
+
78
+ ## Programmatic navigation
79
+
80
+ ```tsx title="src/web/components/checkout-button.tsx"
81
+ import { href, navigateBack, navigateTo } from "@warlock.js/web";
82
+
83
+ export function CheckoutButtons() {
84
+ const openCheckout = () => {
85
+ const url = href("checkout.index");
86
+
87
+ if (!navigateTo(url)) {
88
+ window.location.assign(url);
89
+ }
90
+ };
91
+
92
+ return (
93
+ <div>
94
+ <button type="button" onClick={navigateBack}>Back</button>
95
+ <button type="button" onClick={openCheckout}>Checkout</button>
96
+ </div>
97
+ );
98
+ }
99
+ ```
100
+
101
+ `navigateTo(path, { replace?: boolean })` accepts a path, not a route name. Resolve a name with `href()` first. It returns `false` when no client runtime accepted the navigation; use a real browser navigation when arrival is mandatory. `navigateBack()` is a no-op without a browser.
102
+
103
+ ## Re-fetch after a mutation
104
+
105
+ The public primitive is `refresh()`. There is no `revalidate()` export.
106
+
107
+ ```tsx title="src/app/products/web/delete-product-button.tsx"
108
+ import { refresh } from "@warlock.js/web";
109
+
110
+ export function DeleteProductButton({ id }: { id: string }) {
111
+ const deleteProduct = async () => {
112
+ const response = await fetch(`/api/products/${encodeURIComponent(id)}`, {
113
+ method: "DELETE",
114
+ });
115
+
116
+ if (!response.ok) {
117
+ throw new Error(`Delete failed with status ${response.status}`);
118
+ }
119
+
120
+ const refreshed = await refresh();
121
+
122
+ if (!refreshed) {
123
+ window.location.reload();
124
+ }
125
+ };
126
+
127
+ return <button type="button" onClick={deleteProduct}>Delete product</button>;
128
+ }
129
+ ```
130
+
131
+ `refresh()` re-fetches the current URL's App, Layout, and Page loaders and swaps fresh data without pushing history. It returns `true` only when fresh data reached the screen. On network/build failure it returns `false` and leaves the current page intact.
132
+
133
+ This is an ordinary API mutation followed by a re-fetch. Server actions are not supported.
134
+
135
+ ## Current and previous routes
136
+
137
+ `currentRoute()` and `previousRoute()` report route names and params sent by the server; they never re-match the browser URL.
138
+
139
+ ```tsx title="src/web/components/current-product-id.tsx"
140
+ import { currentRoute, previousRoute } from "@warlock.js/web";
141
+
142
+ export function CurrentProductId() {
143
+ const current = currentRoute();
144
+ const previous = previousRoute();
145
+
146
+ return (
147
+ <dl>
148
+ <dt>Current product</dt>
149
+ <dd>{current?.params?.id ?? "none"}</dd>
150
+ <dt>Previous page</dt>
151
+ <dd>{previous?.name ?? "first visit"}</dd>
152
+ </dl>
153
+ );
154
+ }
155
+ ```
156
+
157
+ `previousRoute()` means the previously swapped page, not the previous browser-history entry. On the first page it is `undefined`.
158
+
159
+ `routerEvents` is exported and `refresh()` emits its start/end/error lifecycle today. Ordinary Link and `navigateTo` swaps are not yet wired to that emitter, so do not use it as a complete global navigation progress signal yet.
160
+
161
+ ## Failure behavior
162
+
163
+ - A failed navigation falls back to a full browser load so the user still arrives.
164
+ - A failed `refresh()` keeps the current page and reports `false`; it never spends the page the user already has.
165
+ - Back/Forward uses the same data fetch and tree swap, replacing the existing history entry rather than pushing another.
166
+ - Navigating within one layout preserves layout state through React reconciliation.
167
+
168
+ ## Gotchas
169
+
170
+ - **Use `refresh()`, not `revalidate()`.** `refresh` is the exported loader re-fetch primitive.
171
+ - **`navigateTo` accepts a URL/path.** Resolve named routes with `href()` first.
172
+ - **Pass exactly one Link destination.** Combining `to`, `href`, `email`, or `tel` throws.
173
+ - **Do not add `params` or `query` to a literal URL.** Put them in the URL itself or use a route name.
174
+ - **Do not build a client matcher.** The server's matched name and params travel in the payload.
175
+ - **Do not import `esm/hydration/index.mjs`.** Normal consumers import navigation from `@warlock.js/web`; low-level runtime contracts live at `@warlock.js/web/client/runtime`.
176
+
177
+ ## See also
178
+
179
+ - [`create-a-page/SKILL.md`](../create-a-page/SKILL.md) — declare route names and params.
180
+ - [`load-page-data/SKILL.md`](../load-page-data/SKILL.md) — what `refresh()` re-runs.
181
+ - [`use-layouts/SKILL.md`](../use-layouts/SKILL.md) — why layout state persists.
182
+ - [`write-the-root/SKILL.md`](../write-the-root/SKILL.md) — the `#root` swap boundary.
@@ -0,0 +1,145 @@
1
+ ---
2
+ name: serve-styles
3
+ description: '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.'
4
+ ---
5
+
6
+ # Warlock — serve styles
7
+
8
+ Import styles from `root.tsx` for global, render-blocking CSS. A page may also import its own stylesheet, but development and production deliver page-local CSS differently.
9
+
10
+ ## The shape
11
+
12
+ ```tsx title="src/web/root.tsx"
13
+ import { Head, Scripts } from "@warlock.js/web";
14
+ import type { AppProps } from "@warlock.js/web";
15
+ import "./app.css";
16
+
17
+ export default function App({ children }: AppProps) {
18
+ return (
19
+ <html lang="en">
20
+ <head>
21
+ <Head />
22
+ </head>
23
+ <body>
24
+ <div id="root">{children}</div>
25
+ <Scripts />
26
+ </body>
27
+ </html>
28
+ );
29
+ }
30
+ ```
31
+
32
+ ```css title="src/web/app.css"
33
+ :root {
34
+ color-scheme: light dark;
35
+ font-family: system-ui, sans-serif;
36
+ }
37
+
38
+ body {
39
+ margin: 0;
40
+ }
41
+
42
+ main {
43
+ max-width: 72rem;
44
+ margin-inline: auto;
45
+ padding: 2rem;
46
+ }
47
+ ```
48
+
49
+ Use a bare side-effect import in `root.tsx`. In development the server parses that exact form to discover global styles before rendering the document.
50
+
51
+ ## Development delivery
52
+
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
+
55
+ ```html
56
+ <link rel="stylesheet" href="/src/web/app.css?direct">
57
+ ```
58
+
59
+ The `?direct` query is required: without it Vite serves an imported CSS URL as a JavaScript module, which a stylesheet link cannot apply. With it Vite returns real `text/css`.
60
+
61
+ Root imports ending in `.css`, `.scss`, `.sass`, `.less`, or `.styl` are recognized. The source file must resolve inside the app root; an outside path is left to the client graph rather than converted to an unsafe `/@fs/` guess.
62
+
63
+ ## Page-local styles
64
+
65
+ A page may import CSS directly:
66
+
67
+ ```tsx title="src/app/products/web/products.page.tsx"
68
+ import "./products.css";
69
+
70
+ export const route = {
71
+ path: "/products",
72
+ name: "products.index",
73
+ } as const;
74
+
75
+ export default function ProductsPage() {
76
+ return (
77
+ <main className="products-page">
78
+ <h1>Products</h1>
79
+ </main>
80
+ );
81
+ }
82
+ ```
83
+
84
+ ```css title="src/app/products/web/products.css"
85
+ .products-page {
86
+ display: grid;
87
+ gap: 1rem;
88
+ }
89
+ ```
90
+
91
+ Recognized asset imports survive the page's client projection. The client boundary is determined by the import graph, not by the file living under `web/`.
92
+
93
+ In development, only root stylesheet imports are converted into render-blocking document links. A page-local import is loaded by Vite's client module graph around hydration, so it still applies but is not guaranteed before first paint. Move above-the-fold or site-wide rules into a stylesheet imported by `root.tsx` when avoiding a flash matters.
94
+
95
+ ## Production delivery
96
+
97
+ The production client build enables Vite's manifest. Vite records emitted CSS against the chunks that imported it. At boot, Warlock reads:
98
+
99
+ ```text
100
+ <build.outdir>/client/.vite/manifest.json
101
+ ```
102
+
103
+ It collects every valid `css` asset across all manifest entries, deduplicates them in manifest order, and emits each as a render-blocking link before `</head>`.
104
+
105
+ That means both root-imported and page-imported CSS are linked in the initial production document. The current implementation collects across the whole client manifest rather than selecting only the matched route's CSS, so every page receives the emitted stylesheet set.
106
+
107
+ Only manifest URLs under the one client asset prefix are emitted; a stylesheet outside the mounted asset directory is dropped instead of producing a dead link.
108
+
109
+ ## Where links land
110
+
111
+ Stylesheet links are inserted into the rendered HTML immediately before the final `</head>`. They are not rendered by the `<Head />` component itself.
112
+
113
+ Consequences:
114
+
115
+ - A custom root must render a real `<head>...</head>` for automatic stylesheet links.
116
+ - Root-authored `<link>` and `<style>` elements appear before Warlock's injected stylesheet links, so later injected rules can win normal cascade ties.
117
+ - `<Head />` remains responsible for page metadata; the closing `</head>` is what stylesheet installation needs.
118
+ - A root with no closing `</head>` is returned unchanged and receives no automatic stylesheet links.
119
+
120
+ ## What ships to the browser
121
+
122
+ The hydration entry is built from the projected client graph. CSS imports are known-safe asset edges and survive projection even when server exports in the same page module are removed. Application code never imports the published hydration file directly; the web build uses `esm/hydration/index.mjs` as an input and the route handler installs its emitted module URL.
123
+
124
+ ## Diagnose missing or late CSS
125
+
126
+ 1. For global CSS in development, confirm `root.tsx` uses a bare import such as `import "./app.css";`.
127
+ 2. Confirm the rendered document contains a closing `</head>` and a stylesheet URL ending in `?direct`.
128
+ 3. For page-local CSS in development, expect Vite's client graph to apply it after hydration; move critical rules to the root import if first paint matters.
129
+ 4. In production, confirm `.vite/manifest.json` has `css` arrays and the referenced files live under the client asset prefix.
130
+ 5. Do not hand-author a link to a hashed production asset; its name belongs to the Vite manifest.
131
+
132
+ ## Gotchas
133
+
134
+ - **Root CSS and page CSS differ in dev.** Root imports are render-blocking; page imports are client-injected.
135
+ - **Production currently links all emitted CSS on every page.** It is safe for first paint but not route-minimal.
136
+ - **Use a bare root import.** `devStylesheetUrls` scans `import "./app.css"`, not a bound CSS-module import.
137
+ - **Keep a closing `</head>`.** Automatic link installation has nowhere safe to write without it.
138
+ - **Do not remove `?direct` from a dev stylesheet link.** Vite otherwise responds with JavaScript.
139
+ - **Do not import the hydration entry yourself.** The connector owns dev serving and production asset URLs.
140
+
141
+ ## See also
142
+
143
+ - [`write-the-root/SKILL.md`](../write-the-root/SKILL.md) — the document `<head>` these links enter.
144
+ - [`create-a-page/SKILL.md`](../create-a-page/SKILL.md) — page-local asset imports and projection.
145
+ - [`navigate-on-the-client/SKILL.md`](../navigate-on-the-client/SKILL.md) — client swaps after the initial styled document.
@@ -0,0 +1,121 @@
1
+ ---
2
+ name: use-layouts
3
+ description: '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.'
4
+ ---
5
+
6
+ # Warlock — use layouts
7
+
8
+ A positional `layout.tsx` applies to pages in its directory and descendant directories. Its default export wraps the page; its optional `prefix` contributes to every descendant page's effective URL.
9
+
10
+ ## The shape
11
+
12
+ ```tsx title="src/app/products/web/layout.tsx"
13
+ import type { LayoutLoader, LayoutProps } from "@warlock.js/web";
14
+
15
+ export const prefix = "/products";
16
+
17
+ export const loader = (async () => {
18
+ return {
19
+ navigation: [
20
+ { href: "/products", label: "Products" },
21
+ { href: "/products/new", label: "New product" },
22
+ ],
23
+ };
24
+ }) satisfies LayoutLoader;
25
+
26
+ export default function ProductsLayout({
27
+ data,
28
+ children,
29
+ }: LayoutProps<typeof loader>) {
30
+ return (
31
+ <section>
32
+ <nav aria-label="Products">
33
+ {data.navigation.map(item => (
34
+ <a key={item.href} href={item.href}>
35
+ {item.label}
36
+ </a>
37
+ ))}
38
+ </nav>
39
+ {children}
40
+ </section>
41
+ );
42
+ }
43
+ ```
44
+
45
+ A page beside it can declare its path relative to the prefix:
46
+
47
+ ```tsx title="src/app/products/web/index.page.tsx"
48
+ export const route = {
49
+ path: "/",
50
+ name: "products.index",
51
+ } as const;
52
+
53
+ export default function ProductsPage() {
54
+ return <h1>Products</h1>;
55
+ }
56
+ ```
57
+
58
+ The effective URL is `/products`. A page with `route.path = "/:id"` under the same layout is served at `/products/:id`.
59
+
60
+ ## Prefix composition
61
+
62
+ Every positional layout on the page's directory ancestry may export a literal `prefix`. Prefixes compose outermost first, then the page's own route path.
63
+
64
+ ```tsx title="src/app/users/web/layout.tsx"
65
+ export const prefix = "/users";
66
+ ```
67
+
68
+ ```tsx title="src/app/users/web/account/layout.tsx"
69
+ export const prefix = "/account";
70
+ ```
71
+
72
+ ```tsx title="src/app/users/web/account/settings.page.tsx"
73
+ export const route = {
74
+ path: "/settings",
75
+ name: "users.account.settings",
76
+ } as const;
77
+
78
+ export default function SettingsPage() {
79
+ return <h1>Account settings</h1>;
80
+ }
81
+ ```
82
+
83
+ This page's effective URL is `/users/account/settings`.
84
+
85
+ Like page routes, prefixes are read statically at build time. Write `export const prefix = "/account";` directly; computed prefixes are refused.
86
+
87
+ ## Rendering-layout limit
88
+
89
+ Prefix nesting does not imply wrapper nesting. Pages currently support at most one rendering layout—one `layout.tsx` with a default export—on their ancestry path.
90
+
91
+ The two prefix-only layouts above are legal because neither renders. Add a default export to at most one of them. If two layouts on the path have default exports, discovery and boot throw `NestedLayoutsNotSupportedError`, naming the page and both rendering layouts.
92
+
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
+
95
+ ## Why layout state persists
96
+
97
+ 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.
98
+
99
+ `refresh()` has the same property: it re-fetches loaders and swaps the page data while the layout stays mounted. Navigating to a page with a different layout component changes the tree type and remounts that wrapper.
100
+
101
+ ## Loader data
102
+
103
+ `LayoutLoader` receives the full `PageContext` and its return reaches `LayoutProps<typeof loader>["data"]`. Layout, App, and Page loaders run in parallel; a layout loader cannot read the page loader's result.
104
+
105
+ 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).
106
+
107
+ ## Gotchas
108
+
109
+ - **`layout.tsx` is positional.** It applies by directory ancestry; moving a page can change its layout and URL prefix together.
110
+ - **Only one layout on a path may render.** Multiple prefix/middleware-only layouts are fine; multiple default exports are not.
111
+ - **A layout prefix changes the registered URL.** Check the composed path, not only the page's `route.path`.
112
+ - **Keep `prefix` literal.** The build parses it without executing the module.
113
+ - **Use `LayoutProps<typeof loader>`.** Bare `LayoutProps` is for a layout with no loader and gives `data` as `undefined`.
114
+ - **Layout persistence is type-and-position based.** Changing to another layout component remounts it, as normal React reconciliation requires.
115
+
116
+ ## See also
117
+
118
+ - [`create-a-page/SKILL.md`](../create-a-page/SKILL.md) — declare the page path composed after the prefix.
119
+ - [`write-the-root/SKILL.md`](../write-the-root/SKILL.md) — the document and `#root` outside the layout.
120
+ - [`load-page-data/SKILL.md`](../load-page-data/SKILL.md) — `LayoutLoader`, parallel execution, and `shared`.
121
+ - [`navigate-on-the-client/SKILL.md`](../navigate-on-the-client/SKILL.md) — client swaps and `refresh()`.
@@ -0,0 +1,131 @@
1
+ ---
2
+ name: write-the-root
3
+ description: '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`.'
4
+ ---
5
+
6
+ # Warlock — write the root
7
+
8
+ `src/web/root.tsx` is the application document. Its default export renders the complete `<html>` tree and contains the one DOM node the browser hydrates: `#root`.
9
+
10
+ ## The shape
11
+
12
+ ```tsx title="src/web/root.tsx"
13
+ import { Head, Scripts } from "@warlock.js/web";
14
+ import type { AppProps } from "@warlock.js/web";
15
+
16
+ export default function App({ children }: AppProps) {
17
+ return (
18
+ <html lang="en">
19
+ <head>
20
+ <Head />
21
+ <link rel="icon" href="/favicon.svg" />
22
+ </head>
23
+ <body>
24
+ <div id="root">{children}</div>
25
+ <Scripts />
26
+ </body>
27
+ </html>
28
+ );
29
+ }
30
+ ```
31
+
32
+ The root component is synchronous. HTTP work belongs in an `AppLoader`; the component receives its result as `data`.
33
+
34
+ ## `#root` is the hydration boundary
35
+
36
+ The server renders this document shape:
37
+
38
+ ```text
39
+ App document
40
+ └── #root
41
+ └── Layout
42
+ └── Page
43
+ ```
44
+
45
+ The browser hydrates `#root`, not the whole document. The client tree deliberately contains Layout + Page and excludes App because App contains the mount point. Keep exactly one element with `id="root"` around `{children}`. It may be nested inside your own body markup, but it must not be renamed or omitted.
46
+
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
+
49
+ ## `<Head />`
50
+
51
+ `<Head />` renders the resolved page metadata at that position. It takes no props and emits:
52
+
53
+ - `<meta charset="utf-8">`
54
+ - title, description, keywords, canonical, and robots tags
55
+ - Open Graph and Twitter tags
56
+
57
+ Do not also hard-code a `<title>` for the current page; that produces two titles. Static root-wide tags such as a favicon or application name can sit beside `<Head />`.
58
+
59
+ ## `<Scripts />`
60
+
61
+ `<Scripts />` emits the escaped `application/json` payload that hydration and client navigation consume. Put it after `#root`, normally near the end of `<body>`.
62
+
63
+ For a Content Security Policy nonce:
64
+
65
+ ```tsx title="src/web/root.tsx"
66
+ import { Head, Scripts } from "@warlock.js/web";
67
+ import type { AppProps } from "@warlock.js/web";
68
+
69
+ export default function App({ children, shared }: AppProps) {
70
+ return (
71
+ <html lang="en">
72
+ <head>
73
+ <Head />
74
+ </head>
75
+ <body>
76
+ <div id="root">{children}</div>
77
+ <Scripts nonce={(shared as { nonce?: string }).nonce} />
78
+ </body>
79
+ </html>
80
+ );
81
+ }
82
+ ```
83
+
84
+ Prefer declaring `nonce` on `SharedContext` so the cast is unnecessary; see [load-page-data](../load-page-data/SKILL.md). If no prop is supplied, `<Scripts />` falls back to the framework's request nonce slot.
85
+
86
+ `<Scripts />` owns the inline data payload. The separate hydration module is appended by the page route handler. Its published `esm/hydration/index.mjs` file is a build input and must never be imported by application code.
87
+
88
+ ## Add an application loader
89
+
90
+ ```tsx title="src/web/root.tsx"
91
+ import { Head, Scripts } from "@warlock.js/web";
92
+ import type { AppLoader, AppProps } from "@warlock.js/web";
93
+
94
+ export const loader = (async () => {
95
+ return { applicationName: "Warlock Store" };
96
+ }) satisfies AppLoader;
97
+
98
+ export default function App({ data, children }: AppProps<typeof loader>) {
99
+ return (
100
+ <html lang="en">
101
+ <head>
102
+ <Head />
103
+ <meta name="application-name" content={data.applicationName} />
104
+ </head>
105
+ <body>
106
+ <div id="root">{children}</div>
107
+ <Scripts />
108
+ </body>
109
+ </html>
110
+ );
111
+ }
112
+ ```
113
+
114
+ The App loader runs in parallel with layout and page loaders. Its return is for the App component; use `shared` for request data that multiple levels need.
115
+
116
+ ## Gotchas
117
+
118
+ - **The root owns the complete document.** Return `<html>`, `<head>`, and `<body>`, not a fragment.
119
+ - **Keep `{children}` inside `#root`.** Server rendering can still look correct without it, but hydration cannot mount.
120
+ - **Render `<Head />` in a custom root.** It is what turns the page's `metadata` into elements.
121
+ - **Render `<Scripts />` in a custom root.** Without the payload script, the browser cannot hydrate the server-rendered page.
122
+ - **Do not make the component `async`.** Load data with `AppLoader`.
123
+ - **Do not import browser-only state into the root expecting it to persist.** The client hydrates the subtree inside the root, not the root itself.
124
+ - **Stylesheet links are installed separately.** They are inserted before `</head>`; read [serve-styles](../serve-styles/SKILL.md) for the dev/production rules.
125
+
126
+ ## See also
127
+
128
+ - [`create-a-page/SKILL.md`](../create-a-page/SKILL.md) — the Page level rendered under the root.
129
+ - [`use-layouts/SKILL.md`](../use-layouts/SKILL.md) — the persistent wrapper inside `#root`.
130
+ - [`load-page-data/SKILL.md`](../load-page-data/SKILL.md) — `AppLoader` and typed `shared`.
131
+ - [`serve-styles/SKILL.md`](../serve-styles/SKILL.md) — global CSS from `root.tsx`.