@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.
- package/CHANGELOG.md +49 -0
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/esm/build/contribution.d.mts +55 -0
- package/esm/build/contribution.mjs +132 -0
- package/esm/build/contribution.mjs.map +1 -0
- package/esm/build/discover-pages.mjs +294 -0
- package/esm/build/discover-pages.mjs.map +1 -0
- package/esm/build/generate-client-registry.mjs +123 -0
- package/esm/build/generate-client-registry.mjs.map +1 -0
- package/esm/build/generate-pages-barrel.mjs +330 -0
- package/esm/build/generate-pages-barrel.mjs.map +1 -0
- package/esm/build/read-route-exports.mjs +212 -0
- package/esm/build/read-route-exports.mjs.map +1 -0
- package/esm/client/build-hydrated-tree.mjs +113 -0
- package/esm/client/build-hydrated-tree.mjs.map +1 -0
- package/esm/client/hydrate-page.mjs +73 -0
- package/esm/client/hydrate-page.mjs.map +1 -0
- package/esm/client/navigation/current-route.d.mts +91 -0
- package/esm/client/navigation/current-route.mjs +66 -0
- package/esm/client/navigation/current-route.mjs.map +1 -0
- package/esm/client/navigation/fetch-page-data.mjs +97 -0
- package/esm/client/navigation/fetch-page-data.mjs.map +1 -0
- package/esm/client/navigation/navigation-root.mjs +196 -0
- package/esm/client/navigation/navigation-root.mjs.map +1 -0
- package/esm/client/navigation/prefetch.mjs +110 -0
- package/esm/client/navigation/prefetch.mjs.map +1 -0
- package/esm/client/navigation/refresh.d.mts +40 -0
- package/esm/client/navigation/refresh.mjs +146 -0
- package/esm/client/navigation/refresh.mjs.map +1 -0
- package/esm/client/navigation/verbs.d.mts +55 -0
- package/esm/client/navigation/verbs.mjs +66 -0
- package/esm/client/navigation/verbs.mjs.map +1 -0
- package/esm/client/runtime/index.d.mts +4 -0
- package/esm/client/runtime/index.mjs +4 -0
- package/esm/client/runtime/manifest.d.mts +8 -0
- package/esm/client/runtime/manifest.mjs +77 -0
- package/esm/client/runtime/manifest.mjs.map +1 -0
- package/esm/client/runtime/matcher.d.mts +7 -0
- package/esm/client/runtime/matcher.mjs +169 -0
- package/esm/client/runtime/matcher.mjs.map +1 -0
- package/esm/client/runtime/types.d.mts +22 -0
- package/esm/components/default-app.mjs +32 -0
- package/esm/components/default-app.mjs.map +1 -0
- package/esm/components/document-context.d.mts +1 -0
- package/esm/components/document-context.mjs +24 -0
- package/esm/components/document-context.mjs.map +1 -0
- package/esm/components/head.d.mts +14 -0
- package/esm/components/head.mjs +63 -0
- package/esm/components/head.mjs.map +1 -0
- package/esm/components/link.d.mts +157 -0
- package/esm/components/link.mjs +194 -0
- package/esm/components/link.mjs.map +1 -0
- package/esm/components/scripts.d.mts +14 -0
- package/esm/components/scripts.mjs +21 -0
- package/esm/components/scripts.mjs.map +1 -0
- package/esm/connector/index.d.mts +6 -0
- package/esm/connector/index.mjs +7 -0
- package/esm/context.d.mts +107 -0
- package/esm/hydration/index.d.mts +1 -0
- package/esm/hydration/index.mjs +33 -0
- package/esm/hydration/index.mjs.map +1 -0
- package/esm/hydration-payload.d.mts +1 -0
- package/esm/hydration-payload.mjs +72 -0
- package/esm/hydration-payload.mjs.map +1 -0
- package/esm/index.d.mts +35 -0
- package/esm/index.mjs +12 -0
- package/esm/loaders.d.mts +47 -0
- package/esm/metadata.d.mts +54 -0
- package/esm/props.d.mts +42 -0
- package/esm/route.d.mts +30 -0
- package/esm/routing/compose-route-path.mjs +33 -0
- package/esm/routing/compose-route-path.mjs.map +1 -0
- package/esm/routing/data-request.mjs +52 -0
- package/esm/routing/data-request.mjs.map +1 -0
- package/esm/routing/layout-policy.mjs +78 -0
- package/esm/routing/layout-policy.mjs.map +1 -0
- package/esm/routing/navigator.mjs +21 -0
- package/esm/routing/navigator.mjs.map +1 -0
- package/esm/routing/query-string.d.mts +250 -0
- package/esm/routing/query-string.mjs +340 -0
- package/esm/routing/query-string.mjs.map +1 -0
- package/esm/routing/route-identity.mjs +68 -0
- package/esm/routing/route-identity.mjs.map +1 -0
- package/esm/routing/route-table.d.mts +34 -0
- package/esm/routing/route-table.mjs +182 -0
- package/esm/routing/route-table.mjs.map +1 -0
- package/esm/routing/router-events.d.mts +159 -0
- package/esm/routing/router-events.mjs +83 -0
- package/esm/routing/router-events.mjs.map +1 -0
- package/esm/server/buffered-response.mjs +115 -0
- package/esm/server/buffered-response.mjs.map +1 -0
- package/esm/server/build-hydration-payload.mjs +27 -0
- package/esm/server/build-hydration-payload.mjs.map +1 -0
- package/esm/server/client-asset-url-prefix.d.mts +22 -0
- package/esm/server/client-asset-url-prefix.mjs +23 -0
- package/esm/server/client-asset-url-prefix.mjs.map +1 -0
- package/esm/server/create-page-module-loader.mjs +49 -0
- package/esm/server/create-page-module-loader.mjs.map +1 -0
- package/esm/server/create-page-route-handler.mjs +107 -0
- package/esm/server/create-page-route-handler.mjs.map +1 -0
- package/esm/server/dev-server.mjs +167 -0
- package/esm/server/dev-server.mjs.map +1 -0
- package/esm/server/execute-page-request.d.mts +1 -0
- package/esm/server/execute-page-request.mjs +166 -0
- package/esm/server/execute-page-request.mjs.map +1 -0
- package/esm/server/execute-page-request.types.d.mts +1 -0
- package/esm/server/hydration-client-url.d.mts +37 -0
- package/esm/server/hydration-client-url.mjs +101 -0
- package/esm/server/hydration-client-url.mjs.map +1 -0
- package/esm/server/index.mjs +13 -0
- package/esm/server/install-page-routes-from-manifest.mjs +133 -0
- package/esm/server/install-page-routes-from-manifest.mjs.map +1 -0
- package/esm/server/install-page-routes.d.mts +1 -0
- package/esm/server/install-page-routes.mjs +6 -0
- package/esm/server/install-production-page-routes.mjs +55 -0
- package/esm/server/install-production-page-routes.mjs.map +1 -0
- package/esm/server/match-page-route.mjs +48 -0
- package/esm/server/match-page-route.mjs.map +1 -0
- package/esm/server/page-context.d.mts +1 -0
- package/esm/server/page-context.mjs +33 -0
- package/esm/server/page-context.mjs.map +1 -0
- package/esm/server/page-manifest.d.mts +102 -0
- package/esm/server/page-manifest.mjs +31 -0
- package/esm/server/page-manifest.mjs.map +1 -0
- package/esm/server/render-page.mjs +214 -0
- package/esm/server/render-page.mjs.map +1 -0
- package/esm/server/resolve-page-metadata.mjs +33 -0
- package/esm/server/resolve-page-metadata.mjs.map +1 -0
- package/esm/server/resolve-validation-data.mjs +27 -0
- package/esm/server/resolve-validation-data.mjs.map +1 -0
- package/esm/server/settle-page-response.d.mts +1 -0
- package/esm/server/settle-page-response.mjs +90 -0
- package/esm/server/settle-page-response.mjs.map +1 -0
- package/esm/server/stylesheet-urls.mjs +65 -0
- package/esm/server/stylesheet-urls.mjs.map +1 -0
- package/esm/server/web-connector-factory.d.mts +25 -0
- package/esm/server/web-connector-factory.mjs +121 -0
- package/esm/server/web-connector-factory.mjs.map +1 -0
- package/esm/server/web-connector.d.mts +34 -0
- package/esm/server/web-connector.mjs +516 -0
- package/esm/server/web-connector.mjs.map +1 -0
- package/esm/shared.d.mts +22 -0
- package/esm/shared.mjs +223 -0
- package/esm/shared.mjs.map +1 -0
- package/esm/validation.d.mts +31 -0
- package/esm/vite/app-convention-aliases.mjs +43 -0
- package/esm/vite/app-convention-aliases.mjs.map +1 -0
- package/esm/vite/build-client.d.mts +38 -0
- package/esm/vite/build-client.mjs +72 -0
- package/esm/vite/build-client.mjs.map +1 -0
- package/esm/vite/gate-a-resolve.d.mts +57 -0
- package/esm/vite/gate-a-resolve.mjs +834 -0
- package/esm/vite/gate-a-resolve.mjs.map +1 -0
- package/esm/vite/gate-b-secrets.d.mts +57 -0
- package/esm/vite/gate-b-secrets.mjs +299 -0
- package/esm/vite/gate-b-secrets.mjs.map +1 -0
- package/esm/vite/gate-c-verify.d.mts +118 -0
- package/esm/vite/gate-c-verify.mjs +263 -0
- package/esm/vite/gate-c-verify.mjs.map +1 -0
- package/esm/vite/hydration-entries.d.mts +17 -0
- package/esm/vite/hydration-entries.mjs +45 -0
- package/esm/vite/hydration-entries.mjs.map +1 -0
- package/esm/vite/index.d.mts +126 -0
- package/esm/vite/index.mjs +146 -0
- package/esm/vite/index.mjs.map +1 -0
- package/esm/vite/page-registry-plugin.d.mts +43 -0
- package/esm/vite/page-registry-plugin.mjs +138 -0
- package/esm/vite/page-registry-plugin.mjs.map +1 -0
- package/esm/vite/projection.d.mts +31 -0
- package/esm/vite/projection.mjs +384 -0
- package/esm/vite/projection.mjs.map +1 -0
- package/llms-full.txt +1041 -0
- package/llms.txt +21 -0
- package/package.json +77 -0
- package/skills/add-web-to-an-app/SKILL.md +103 -0
- package/skills/create-a-page/SKILL.md +141 -0
- package/skills/load-page-data/SKILL.md +183 -0
- package/skills/navigate-on-the-client/SKILL.md +182 -0
- package/skills/serve-styles/SKILL.md +145 -0
- package/skills/use-layouts/SKILL.md +121 -0
- package/skills/write-the-root/SKILL.md +131 -0
package/llms.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Warlock Web
|
|
2
|
+
|
|
3
|
+
> Package: `@warlock.js/web`
|
|
4
|
+
|
|
5
|
+
> SSR React pages served by the same Warlock HTTP server that serves the API — a backend framework that renders React, not a React framework.
|
|
6
|
+
|
|
7
|
+
> First published release: 5.0.0. `@warlock.js/web` had never been published to npm before this release.
|
|
8
|
+
|
|
9
|
+
> Consumer entry points: `@warlock.js/web`, `@warlock.js/web/client/runtime`, `@warlock.js/web/connector`, and `@warlock.js/web/vite`. The published `@warlock.js/web/hydration` subpath is a framework build input; application code never imports it.
|
|
10
|
+
|
|
11
|
+
> Deliberate non-goals: server actions are a v2 design decision rather than a missing v1 feature; page routes reject regex parameters, optional parameters, and multiple parameters in one segment.
|
|
12
|
+
|
|
13
|
+
## Skills
|
|
14
|
+
|
|
15
|
+
- [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`. Triggers: `*.page.tsx`, `route`, `PageLoader`, `PageProps`, `PageMetadata`; "create a page", "add an SSR route", "make a React page", "type page loader data"; typical import `import type { PageLoader, PageProps } from "@warlock.js/web"`. Skip: root document shell — `@warlock.js/web/write-the-root/SKILL.md`; layout wrappers and prefixes — `@warlock.js/web/use-layouts/SKILL.md`; loader lifecycle and `shared` — `@warlock.js/web/load-page-data/SKILL.md`; competing frameworks `next`, `remix`, `react-router` file routes.
|
|
17
|
+
- [load-page-data](@warlock.js/web/load-page-data/SKILL.md): Load App, Layout, and Page data with `AppLoader`, `LayoutLoader`, and `PageLoader`; type component `data`, validate page input, short-circuit with the buffered response, and publish request-scoped browser-safe values through `shared`. Triggers: `PageLoader`, `LayoutLoader`, `AppLoader`, `PageProps`, `shared`, `useShared`, `validation`, `request.validated`; "load page data", "pass server data to React", "share request data", "redirect from a loader". Skip: page module basics — `@warlock.js/web/create-a-page/SKILL.md`; layouts — `@warlock.js/web/use-layouts/SKILL.md`; mutation follow-up — `@warlock.js/web/navigate-on-the-client/SKILL.md`; competing loaders Next data functions, Remix loaders, React Server Components.
|
|
18
|
+
- [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
|
+
- [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`, 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.
|
|
21
|
+
- [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
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@warlock.js/web",
|
|
3
|
+
"description": "Warlock.js web layer for the SSR page pipeline and client runtime.",
|
|
4
|
+
"warlock": {
|
|
5
|
+
"environment": "universal"
|
|
6
|
+
},
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@babel/parser": "^7.29.7",
|
|
9
|
+
"@babel/types": "^7.29.7",
|
|
10
|
+
"magic-string": "^0.30.21"
|
|
11
|
+
},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"@vitejs/plugin-react": "^5.2.0",
|
|
14
|
+
"@warlock.js/core": "5.0.0",
|
|
15
|
+
"@warlock.js/seal": "5.0.0",
|
|
16
|
+
"react": "*",
|
|
17
|
+
"react-dom": "*",
|
|
18
|
+
"vite": ">=7.3.5"
|
|
19
|
+
},
|
|
20
|
+
"peerDependenciesMeta": {
|
|
21
|
+
"vite": {
|
|
22
|
+
"optional": true
|
|
23
|
+
},
|
|
24
|
+
"@vitejs/plugin-react": {
|
|
25
|
+
"optional": true
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/warlockjs/web"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"warlock",
|
|
34
|
+
"ssr",
|
|
35
|
+
"react",
|
|
36
|
+
"web"
|
|
37
|
+
],
|
|
38
|
+
"author": "hassanzohdy",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"version": "5.0.0",
|
|
41
|
+
"type": "module",
|
|
42
|
+
"main": "./esm/index.mjs",
|
|
43
|
+
"module": "./esm/index.mjs",
|
|
44
|
+
"types": "./esm/index.d.mts",
|
|
45
|
+
"exports": {
|
|
46
|
+
".": {
|
|
47
|
+
"import": {
|
|
48
|
+
"types": "./esm/index.d.mts",
|
|
49
|
+
"default": "./esm/index.mjs"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"./hydration": {
|
|
53
|
+
"import": {
|
|
54
|
+
"types": "./esm/hydration/index.d.mts",
|
|
55
|
+
"default": "./esm/hydration/index.mjs"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"./client/runtime": {
|
|
59
|
+
"import": {
|
|
60
|
+
"types": "./esm/client/runtime/index.d.mts",
|
|
61
|
+
"default": "./esm/client/runtime/index.mjs"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"./connector": {
|
|
65
|
+
"import": {
|
|
66
|
+
"types": "./esm/connector/index.d.mts",
|
|
67
|
+
"default": "./esm/connector/index.mjs"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"./vite": {
|
|
71
|
+
"import": {
|
|
72
|
+
"types": "./esm/vite/index.d.mts",
|
|
73
|
+
"default": "./esm/vite/index.mjs"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-web-to-an-app
|
|
3
|
+
description: '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`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Warlock — add web to an app
|
|
7
|
+
|
|
8
|
+
`warlock add web` turns an existing Warlock API application into an application that can also serve SSR React pages. It installs the web and React packages, adds the Vite development peers, scaffolds the smallest page layer, and registers the late-phase web connector.
|
|
9
|
+
|
|
10
|
+
## The shape
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
yarn warlock add web
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
On the ordinary path it leaves this application-owned shape:
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
src/
|
|
20
|
+
web/
|
|
21
|
+
root.tsx
|
|
22
|
+
home.page.tsx
|
|
23
|
+
warlock.config.ts
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The generated home page declares `route = "/"`. The generated root owns the document, renders `<Head />`, keeps `{children}` inside `#root`, and renders `<Scripts />`.
|
|
27
|
+
|
|
28
|
+
## Connector registration
|
|
29
|
+
|
|
30
|
+
The command adds `webConnector()` to `warlock.config.ts`. If the config cannot be patched safely, add the same wiring yourself:
|
|
31
|
+
|
|
32
|
+
```ts title="warlock.config.ts"
|
|
33
|
+
import { defineConfig } from "@warlock.js/core";
|
|
34
|
+
import { webConnector } from "@warlock.js/web/connector";
|
|
35
|
+
|
|
36
|
+
export default defineConfig({
|
|
37
|
+
connectors: [webConnector()],
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Keep the connector in `warlock.config.ts`. The build reads this same array to collect web's build contribution, while dev/start use it to boot the runtime. Registering it again from `src/app/main.ts` boots it twice and duplicates page routes.
|
|
42
|
+
|
|
43
|
+
## What happens to an existing `GET "/"`
|
|
44
|
+
|
|
45
|
+
The stock project template already owns `/` with a JSON welcome route. The page stub also owns `/`, and two `GET` handlers for one path fail at request time. The installer preserves both surfaces by changing the existing JSON route to `/welcome` before it writes the page.
|
|
46
|
+
|
|
47
|
+
The automatic move is intentionally narrow. It only inspects:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
src/app/shared/routes.ts
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
and only rewrites exactly one top-level declaration shaped like this:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
router.get("/", homePageController);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
After the move:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
router.get("/welcome", homePageController);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Indented `router.get("/", ...)` declarations inside a prefixed group are not root collisions and are not touched.
|
|
66
|
+
|
|
67
|
+
## Collision outcomes
|
|
68
|
+
|
|
69
|
+
- No `src/app/shared/routes.ts`, or no top-level `GET "/"`: create the root page normally.
|
|
70
|
+
- Exactly one recognized top-level `GET "/"` and no top-level `GET "/welcome"`: move it to `/welcome`, then create the home page.
|
|
71
|
+
- More than one top-level `GET "/"`, an existing top-level `GET "/welcome"`, an unreadable/unwritable routes file, or an unrecognized rewrite: create `root.tsx`, do not create `home.page.tsx`, set a failing exit code, and still register the connector.
|
|
72
|
+
|
|
73
|
+
On the refusal path, free `/` yourself and create a page with either `route = "/"` or another literal route.
|
|
74
|
+
|
|
75
|
+
## Re-running the command
|
|
76
|
+
|
|
77
|
+
`src/web/root.tsx` is the scaffold sentinel. If it already exists, the command skips the `src/web` scaffold rather than overwriting a human-owned root. Connector registration is independently idempotent: if the config already mentions `webConnector`, it is left alone.
|
|
78
|
+
|
|
79
|
+
## Verify the result
|
|
80
|
+
|
|
81
|
+
Read the files rather than assuming the command could patch every application shape:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
yarn warlock routes --method GET --path /
|
|
85
|
+
yarn warlock routes --method GET --path /welcome
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The desired result is one page route at `/` and, when the stock JSON route existed, one API route at `/welcome`.
|
|
89
|
+
|
|
90
|
+
## Gotchas
|
|
91
|
+
|
|
92
|
+
- **The collision scan is not codebase-wide.** A root route declared outside `src/app/shared/routes.ts` is not moved. Check `warlock routes` after installation.
|
|
93
|
+
- **`/welcome` must be free before relocation.** The installer refuses to trade one duplicate route for another.
|
|
94
|
+
- **A partial scaffold exits non-zero.** `root.tsx` and connector registration may still have been written; complete only the missing home page after resolving the collision.
|
|
95
|
+
- **Do not register the connector twice.** Use `warlock.config.ts`, not an additional `connectorsManager.register(...)` call.
|
|
96
|
+
- **Do not import the hydration entry.** The connector and build contribution own it.
|
|
97
|
+
|
|
98
|
+
## See also
|
|
99
|
+
|
|
100
|
+
- [`create-a-page/SKILL.md`](../create-a-page/SKILL.md) — write the page after installation.
|
|
101
|
+
- [`write-the-root/SKILL.md`](../write-the-root/SKILL.md) — customize the generated document shell.
|
|
102
|
+
- [`serve-styles/SKILL.md`](../serve-styles/SKILL.md) — add global CSS from the root.
|
|
103
|
+
- [`@warlock.js/core/run-app/SKILL.md`](@warlock.js/core/run-app/SKILL.md) — run dev, build, and production.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-a-page
|
|
3
|
+
description: 'Create an SSR React page under `src/web/**` or `src/app/<module>/web/**` with a literal `route`, a default component, an optional typed `loader`, and page `metadata`. Triggers: `*.page.tsx`, `route`, `PageLoader`, `PageProps`, `PageMetadata`; "create a page", "add an SSR route", "make a React page", "type page loader data"; typical import `import type { PageLoader, PageProps } from "@warlock.js/web"`. Skip: root document shell — `@warlock.js/web/write-the-root/SKILL.md`; layout wrappers and prefixes — `@warlock.js/web/use-layouts/SKILL.md`; loader lifecycle and `shared` — `@warlock.js/web/load-page-data/SKILL.md`; competing frameworks `next`, `remix`, `react-router` file routes.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Warlock — create a page
|
|
7
|
+
|
|
8
|
+
A page is any `*.page.tsx` beneath the global `src/web/` tree or a module's `src/app/<module>/web/` tree. Its URL is declared by `route`; its default export renders React.
|
|
9
|
+
|
|
10
|
+
## The shape
|
|
11
|
+
|
|
12
|
+
```tsx title="src/app/products/web/product-details.page.tsx"
|
|
13
|
+
import type { PageLoader, PageMetadata, PageProps } from "@warlock.js/web";
|
|
14
|
+
|
|
15
|
+
export const route = {
|
|
16
|
+
path: "/products/:id",
|
|
17
|
+
name: "products.details",
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
export const loader = (async ({ request, response }) => {
|
|
21
|
+
const id = request.input("id");
|
|
22
|
+
|
|
23
|
+
response.header("cache-control", "private, max-age=60");
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
product: {
|
|
27
|
+
id,
|
|
28
|
+
name: `Product ${id}`,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}) satisfies PageLoader<undefined, typeof route>;
|
|
32
|
+
|
|
33
|
+
export const metadata: PageMetadata<typeof loader> = ({ data }) => ({
|
|
34
|
+
title: data.product.name,
|
|
35
|
+
description: `Details for ${data.product.name}`,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export default function ProductDetailsPage({ data }: PageProps<typeof loader>) {
|
|
39
|
+
return (
|
|
40
|
+
<main>
|
|
41
|
+
<h1>{data.product.name}</h1>
|
|
42
|
+
<p>Product id: {data.product.id}</p>
|
|
43
|
+
</main>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Use `satisfies PageLoader`, not `: PageLoader`. `satisfies` checks the context contract while retaining the loader's concrete return type, which is how `PageProps<typeof loader>` knows the shape of `data`.
|
|
49
|
+
|
|
50
|
+
## The minimum page
|
|
51
|
+
|
|
52
|
+
```tsx title="src/web/contact.page.tsx"
|
|
53
|
+
export const route = "/contact";
|
|
54
|
+
|
|
55
|
+
export default function ContactPage() {
|
|
56
|
+
return (
|
|
57
|
+
<main>
|
|
58
|
+
<h1>Contact</h1>
|
|
59
|
+
<a href="mailto:support@example.com">support@example.com</a>
|
|
60
|
+
</main>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The `route` export is required. Development skips a page without it; the production discovery pass refuses to build it.
|
|
66
|
+
|
|
67
|
+
## Route declarations
|
|
68
|
+
|
|
69
|
+
Use either a bare path or a literal object:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
export const route = "/products";
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
export const route = {
|
|
77
|
+
path: "/products/:id",
|
|
78
|
+
name: "products.details",
|
|
79
|
+
} as const;
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
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
|
+
|
|
84
|
+
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
|
+
|
|
86
|
+
## Page-route grammar
|
|
87
|
+
|
|
88
|
+
Page routes deliberately accept less than API routes:
|
|
89
|
+
|
|
90
|
+
- Supported: `/`, static segments, whole-segment params such as `/products/:id`, the exact wildcard `*`, and a terminal wildcard such as `/docs/*`.
|
|
91
|
+
- Not supported: regex params, optional params, multiple params in one segment, params mixed with text, doubled or trailing slashes, and non-terminal wildcards.
|
|
92
|
+
|
|
93
|
+
Write two pages for an optional segment. Validate a constrained parameter in the page instead of putting a regex in its path.
|
|
94
|
+
|
|
95
|
+
## Metadata
|
|
96
|
+
|
|
97
|
+
`metadata` may be a static object or a function of the resolved loader data and readonly `shared` payload:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import type { PageMetadata } from "@warlock.js/web";
|
|
101
|
+
|
|
102
|
+
export const metadata: PageMetadata = {
|
|
103
|
+
title: "Products",
|
|
104
|
+
description: "Browse the product catalogue",
|
|
105
|
+
robots: "index,follow",
|
|
106
|
+
openGraph: {
|
|
107
|
+
type: "website",
|
|
108
|
+
image: "/images/catalogue-card.png",
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Supported fields are `title`, `description`, `keywords`, `canonical`, `robots`, `openGraph`, and `twitter`. Function metadata runs after a successful loader. If a loader fails, Warlock uses error metadata instead of calling the page function with missing data.
|
|
114
|
+
|
|
115
|
+
## The client boundary
|
|
116
|
+
|
|
117
|
+
The browser boundary is decided by the import graph, not by the file's location. A `*.page.tsx` is universal:
|
|
118
|
+
|
|
119
|
+
- `route`, `middleware`, `validation`, `loader`, and `metadata` are stripped from the client projection.
|
|
120
|
+
- The default component and any other surviving exports form the client graph.
|
|
121
|
+
- An import used only by a stripped server export is removed with it.
|
|
122
|
+
- An import also used by the component survives and therefore must be browser-safe.
|
|
123
|
+
|
|
124
|
+
Keep server-only repository and service reads inside `loader`. Do not read them from module-scope initializers or the default component.
|
|
125
|
+
|
|
126
|
+
## Gotchas
|
|
127
|
+
|
|
128
|
+
- **Do not derive URLs from filenames.** The file chooses discovery; `route` chooses the public URL.
|
|
129
|
+
- **Keep `route` literal.** A computed route cannot be discovered without executing app code and is refused.
|
|
130
|
+
- **Do not annotate the loader with `: PageLoader`.** That erases the return type `PageProps` needs.
|
|
131
|
+
- **Components receive data, not HTTP objects.** `request` and `response` belong to loaders; the component also renders in the browser.
|
|
132
|
+
- **A default component is synchronous.** Fetch in the loader, then render its result.
|
|
133
|
+
- **There are no server actions.** Mutations remain ordinary API requests; call `refresh()` after a successful mutation.
|
|
134
|
+
- **Do not import the hydration entry.** `esm/hydration/index.mjs` is a framework build input, not a consumer API. The public low-level runtime subpath is `@warlock.js/web/client/runtime`.
|
|
135
|
+
|
|
136
|
+
## See also
|
|
137
|
+
|
|
138
|
+
- [`load-page-data/SKILL.md`](../load-page-data/SKILL.md) — validation, loader context, response short-circuits, and `shared`.
|
|
139
|
+
- [`use-layouts/SKILL.md`](../use-layouts/SKILL.md) — positional layouts, prefixes, and persistence.
|
|
140
|
+
- [`navigate-on-the-client/SKILL.md`](../navigate-on-the-client/SKILL.md) — `Link`, `href`, navigation, and `refresh()`.
|
|
141
|
+
- [`serve-styles/SKILL.md`](../serve-styles/SKILL.md) — CSS imports in page and root modules.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: load-page-data
|
|
3
|
+
description: 'Load App, Layout, and Page data with `AppLoader`, `LayoutLoader`, and `PageLoader`; type component `data`, validate page input, short-circuit with the buffered response, and publish request-scoped browser-safe values through `shared`. Triggers: `PageLoader`, `LayoutLoader`, `AppLoader`, `PageProps`, `shared`, `useShared`, `validation`, `request.validated`; "load page data", "pass server data to React", "share request data", "redirect from a loader". Skip: page module basics — `@warlock.js/web/create-a-page/SKILL.md`; layouts — `@warlock.js/web/use-layouts/SKILL.md`; mutation follow-up — `@warlock.js/web/navigate-on-the-client/SKILL.md`; competing loaders Next data functions, Remix loaders, React Server Components.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Warlock — load page data
|
|
7
|
+
|
|
8
|
+
Loaders run on the server and return serializable data to their own component level. Use the level-specific loader type with `satisfies`, then pass `typeof loader` to the matching props type.
|
|
9
|
+
|
|
10
|
+
## The shape
|
|
11
|
+
|
|
12
|
+
```tsx title="src/app/products/web/product-details.page.tsx"
|
|
13
|
+
import { v } from "@warlock.js/seal";
|
|
14
|
+
import type { PageLoader, PageProps } from "@warlock.js/web";
|
|
15
|
+
|
|
16
|
+
export const route = {
|
|
17
|
+
path: "/products/:id",
|
|
18
|
+
name: "products.details",
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
export const validation = {
|
|
22
|
+
schema: v.object({
|
|
23
|
+
id: v.string().minLength(2),
|
|
24
|
+
}),
|
|
25
|
+
validating: ["params"],
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
export const loader = (async ({ request, response, shared }) => {
|
|
29
|
+
const { id } = request.validated();
|
|
30
|
+
|
|
31
|
+
if (id === "missing") {
|
|
32
|
+
return response.notFound();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
response.header("cache-control", "private, max-age=60");
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
product: {
|
|
39
|
+
id,
|
|
40
|
+
name: `Product ${id}`,
|
|
41
|
+
},
|
|
42
|
+
locale: (shared as { locale?: string }).locale ?? "en",
|
|
43
|
+
};
|
|
44
|
+
}) satisfies PageLoader<typeof validation, typeof route>;
|
|
45
|
+
|
|
46
|
+
export default function ProductDetailsPage({ data }: PageProps<typeof loader>) {
|
|
47
|
+
return (
|
|
48
|
+
<article lang={data.locale}>
|
|
49
|
+
<h1>{data.product.name}</h1>
|
|
50
|
+
</article>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`LoaderShortCircuit` values from `notFound()` and redirects are excluded from `PageProps["data"]`, so the component sees only the successful loader return.
|
|
56
|
+
|
|
57
|
+
## Three loader levels
|
|
58
|
+
|
|
59
|
+
| Module | Contract | Component props |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| `src/web/root.tsx` | `AppLoader` | `AppProps<typeof loader>` |
|
|
62
|
+
| positional `layout.tsx` | `LayoutLoader` | `LayoutProps<typeof loader>` |
|
|
63
|
+
| `*.page.tsx` | `PageLoader` | `PageProps<typeof loader>` |
|
|
64
|
+
|
|
65
|
+
All receive one context object with `request`, `response`, and `shared`. Page loaders add generics that connect their sibling `validation` and `route` exports to `request.validated()` and `request.input()`.
|
|
66
|
+
|
|
67
|
+
## Validation
|
|
68
|
+
|
|
69
|
+
A page's `validation` export has:
|
|
70
|
+
|
|
71
|
+
- `schema`: a Seal validator.
|
|
72
|
+
- `validating`: any ordered subset of `"body"`, `"query"`, `"params"`, and `"headers"`.
|
|
73
|
+
|
|
74
|
+
When `validating` is absent or empty, pages validate query + params, with params winning a duplicate key. Validation runs before loaders and a failure short-circuits with 422.
|
|
75
|
+
|
|
76
|
+
`request.validated()` uses the schema's output type, so fields with `.default(...)` are present. `request.input("id")` is narrowed from a literal route path when the loader uses `typeof route`.
|
|
77
|
+
|
|
78
|
+
## Loader response surface
|
|
79
|
+
|
|
80
|
+
Each loader gets its own buffered response because App, Layout, and Page loaders execute in parallel. The public loader methods are:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
response.header("cache-control", "private, max-age=60");
|
|
84
|
+
response.setStatusCode(201);
|
|
85
|
+
return response.redirect("/login");
|
|
86
|
+
return response.permanentRedirect("/products");
|
|
87
|
+
return response.notFound();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Do not continue after a redirect or `notFound`; return the result. Surviving buffers are committed root to leaf. For the same header key, the leafward write wins. A loader that throws or a lower level discarded by a short-circuit does not leak its buffered writes.
|
|
91
|
+
|
|
92
|
+
## Declare the shared payload
|
|
93
|
+
|
|
94
|
+
`SharedContext` ships empty and has no index signature. Augment it once with everything the browser is allowed to receive:
|
|
95
|
+
|
|
96
|
+
```ts title="src/web/types.ts"
|
|
97
|
+
declare module "@warlock.js/web" {
|
|
98
|
+
interface SharedContext {
|
|
99
|
+
locale: string;
|
|
100
|
+
user?: {
|
|
101
|
+
name: string;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export {};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Required keys need an unconditional middleware writer. Optional keys may be written conditionally:
|
|
110
|
+
|
|
111
|
+
```tsx title="src/web/root.tsx"
|
|
112
|
+
import { Head, Scripts, shared as writableShared } from "@warlock.js/web";
|
|
113
|
+
import type { AppProps } from "@warlock.js/web";
|
|
114
|
+
import "./types";
|
|
115
|
+
|
|
116
|
+
const publishLocale = async () => {
|
|
117
|
+
writableShared.locale = "en";
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const middleware = [publishLocale];
|
|
121
|
+
|
|
122
|
+
export default function App({ children, shared }: AppProps) {
|
|
123
|
+
return (
|
|
124
|
+
<html lang={shared.locale}>
|
|
125
|
+
<head>
|
|
126
|
+
<Head />
|
|
127
|
+
</head>
|
|
128
|
+
<body>
|
|
129
|
+
<div id="root">{children}</div>
|
|
130
|
+
<Scripts />
|
|
131
|
+
</body>
|
|
132
|
+
</html>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Shared lifecycle
|
|
138
|
+
|
|
139
|
+
`shared` looks global but resolves through the current request's store on every access. Two requests never share its target.
|
|
140
|
+
|
|
141
|
+
The request pipeline is:
|
|
142
|
+
|
|
143
|
+
1. App, layout, and page middleware run outermost first and write `shared`.
|
|
144
|
+
2. `shared` is normalized, checked, and sealed.
|
|
145
|
+
3. App, layout, and page loaders run in parallel and may only read it.
|
|
146
|
+
4. Components receive a readonly snapshot through props or `useShared()`.
|
|
147
|
+
|
|
148
|
+
Use `useShared()` in a deep component that is not already receiving level props:
|
|
149
|
+
|
|
150
|
+
```tsx title="src/web/components/locale-label.tsx"
|
|
151
|
+
import { useShared } from "@warlock.js/web";
|
|
152
|
+
|
|
153
|
+
export function LocaleLabel() {
|
|
154
|
+
const shared = useShared();
|
|
155
|
+
|
|
156
|
+
return <span>Locale: {shared.locale}</span>;
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Only put browser-safe data in `shared`: scalars, arrays, plain objects, or values with a valid `toJSON()` contract. Functions, `Date`, `Map`, `Set`, and arbitrary class instances are rejected. Prefer a narrow Resource output over a model.
|
|
161
|
+
|
|
162
|
+
## Parallelism rules
|
|
163
|
+
|
|
164
|
+
- App, Layout, and Page loaders cannot read each other's return values.
|
|
165
|
+
- A loader may read `shared` because middleware completed and it was sealed first.
|
|
166
|
+
- A loader must not write `shared`; a post-seal write throws.
|
|
167
|
+
- Loader response mutations are buffered per level and settled deterministically.
|
|
168
|
+
|
|
169
|
+
## Gotchas
|
|
170
|
+
|
|
171
|
+
- **Use `satisfies`, not a type annotation.** Preserve the return type for component props.
|
|
172
|
+
- **Return client-safe data.** Components render again in the browser; models and server handles do not survive the wire.
|
|
173
|
+
- **Write `shared` in middleware only.** Loaders run after the seal.
|
|
174
|
+
- **Required shared keys need unconditional writers.** The type is a promise for every request.
|
|
175
|
+
- **Do not use loader return values as cross-level communication.** The loaders run in parallel.
|
|
176
|
+
- **Server actions are not supported.** POST to an ordinary Warlock API route and call `refresh()` after success.
|
|
177
|
+
|
|
178
|
+
## See also
|
|
179
|
+
|
|
180
|
+
- [`create-a-page/SKILL.md`](../create-a-page/SKILL.md) — the complete page module.
|
|
181
|
+
- [`write-the-root/SKILL.md`](../write-the-root/SKILL.md) — `AppLoader`, `<Head />`, and `<Scripts />`.
|
|
182
|
+
- [`use-layouts/SKILL.md`](../use-layouts/SKILL.md) — `LayoutLoader` and persistent wrappers.
|
|
183
|
+
- [`navigate-on-the-client/SKILL.md`](../navigate-on-the-client/SKILL.md) — re-fetch loaders with `refresh()`.
|