@rangojs/router 0.16.0 → 0.17.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/README.md +254 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/navigation-transaction.d.ts +6 -0
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
- package/dist/types/browser/rsc-router.d.ts +1 -1
- package/dist/types/browser/scroll-restoration.d.ts +7 -0
- package/dist/types/cache/cache-exec-scope.d.ts +18 -1
- package/dist/types/cache/cache-scope.d.ts +34 -5
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +53 -13
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +15 -3
- package/dist/types/client.d.ts +4 -4
- package/dist/types/client.rsc.d.ts +4 -5
- package/dist/types/components/DefaultDocument.d.ts +5 -2
- package/dist/types/context-var.d.ts +6 -5
- package/dist/types/escape-script.d.ts +1 -1
- package/dist/types/handles/MetaTags.d.ts +5 -4
- package/dist/types/handles/Scripts.d.ts +8 -7
- package/dist/types/handles/meta.d.ts +2 -2
- package/dist/types/handles/script.d.ts +17 -16
- package/dist/types/href-client.d.ts +1 -1
- package/dist/types/html.d.ts +12 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +3 -2
- package/dist/types/loader-store.d.ts +7 -4
- package/dist/types/reverse.d.ts +36 -4
- package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
- package/dist/types/route-definition/helpers-types.d.ts +33 -9
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +1 -1
- package/dist/types/router/debug-manifest.d.ts +3 -2
- package/dist/types/router/error-handling.d.ts +20 -0
- package/dist/types/router/handler-context.d.ts +6 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +3 -2
- package/dist/types/router/router-context.d.ts +3 -1
- package/dist/types/router/router-interfaces.d.ts +24 -2
- package/dist/types/router/router-options.d.ts +8 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
- package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
- package/dist/types/router/segment-wrappers.d.ts +1 -1
- package/dist/types/rsc/response-route-handler.d.ts +1 -1
- package/dist/types/rsc/rsc-rendering.d.ts +1 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
- package/dist/types/rsc/shell-capture.d.ts +5 -4
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +10 -0
- package/dist/types/server/context.d.ts +33 -3
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +24 -21
- package/dist/types/ssr/index.d.ts +15 -0
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/generated-routes.d.ts +2 -2
- package/dist/types/testing/internal/context.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +32 -6
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
- package/dist/types/testing/vitest.d.ts +35 -1
- package/dist/types/theme/ThemeScript.d.ts +3 -3
- package/dist/types/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/response-types.d.ts +3 -12
- package/dist/types/use-loader.d.ts +12 -3
- package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
- package/dist/types/vite/rango.d.ts +1 -1
- package/dist/vite/index.js +1381 -1346
- package/package.json +2 -2
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +65 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +207 -97
- package/skills/caching/SKILL.md +295 -93
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +53 -31
- package/skills/cloudflare/SKILL.md +72 -20
- package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
- package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
- package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
- package/skills/comparison/SKILL.md +5 -0
- package/skills/comparison/references/framework-comparison.md +39 -33
- package/skills/composability/SKILL.md +38 -29
- package/skills/css/SKILL.md +10 -13
- package/skills/debug-manifest/SKILL.md +74 -31
- package/skills/defer-hydration/SKILL.md +2 -1
- package/skills/deployment-caching/SKILL.md +6 -1
- package/skills/document-cache/SKILL.md +92 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +53 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +23 -3
- package/skills/hooks/handle-and-actions.md +31 -16
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +37 -15
- package/skills/hooks/urls.md +7 -5
- package/skills/host-router/SKILL.md +64 -12
- package/skills/i18n/SKILL.md +60 -34
- package/skills/intercept/SKILL.md +161 -26
- package/skills/layout/SKILL.md +106 -50
- package/skills/links/SKILL.md +51 -19
- package/skills/loader/SKILL.md +242 -106
- package/skills/middleware/SKILL.md +119 -23
- package/skills/migrate-nextjs/SKILL.md +100 -45
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +49 -38
- package/skills/migrate-react-router/component-migration.md +25 -6
- package/skills/migrate-react-router/data-and-actions.md +18 -5
- package/skills/migrate-react-router/route-mapping.md +12 -6
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +28 -14
- package/skills/parallel/SKILL.md +75 -39
- package/skills/ppr/SKILL.md +345 -150
- package/skills/prerender/SKILL.md +189 -100
- package/skills/rango/SKILL.md +65 -34
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +165 -60
- package/skills/router-setup/SKILL.md +418 -240
- package/skills/scripts/SKILL.md +31 -20
- package/skills/server-actions/SKILL.md +220 -59
- package/skills/shell-manifest/SKILL.md +12 -7
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +68 -32
- package/skills/testing/cache-prerender.md +55 -29
- package/skills/testing/client-components.md +59 -6
- package/skills/testing/e2e-parity.md +12 -2
- package/skills/testing/flight.md +19 -11
- package/skills/testing/handles.md +8 -5
- package/skills/testing/loader.md +8 -5
- package/skills/testing/middleware.md +8 -3
- package/skills/testing/predicates.md +120 -0
- package/skills/testing/render-handler.md +70 -22
- package/skills/testing/response-routes.md +11 -4
- package/skills/testing/reverse-and-types.md +20 -14
- package/skills/testing/server-actions.md +12 -3
- package/skills/testing/server-tree.md +9 -0
- package/skills/testing/setup.md +9 -5
- package/skills/theme/SKILL.md +106 -29
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +126 -47
- package/skills/typesafety/generated-files-and-cli.md +26 -14
- package/skills/typesafety/params-and-search.md +44 -19
- package/skills/typesafety/route-types.md +50 -14
- package/skills/use-cache/SKILL.md +200 -82
- package/skills/vercel/SKILL.md +21 -11
- package/skills/view-transitions/SKILL.md +41 -20
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/index.ts +2 -2
- package/src/browser/navigation-bridge.ts +3 -4
- package/src/browser/navigation-transaction.ts +22 -2
- package/src/browser/partial-update.ts +35 -25
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +1 -1
- package/src/browser/scroll-restoration.ts +53 -6
- package/src/cache/cache-exec-scope.ts +37 -4
- package/src/cache/cache-key-utils.ts +1 -6
- package/src/cache/cache-runtime.ts +230 -61
- package/src/cache/cache-scope.ts +76 -12
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +132 -28
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/tag-invalidation.ts +4 -3
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +16 -3
- package/src/client.rsc.tsx +10 -9
- package/src/client.tsx +5 -4
- package/src/components/DefaultDocument.tsx +7 -2
- package/src/context-var.ts +6 -5
- package/src/escape-script.ts +1 -1
- package/src/handles/MetaTags.tsx +13 -11
- package/src/handles/Scripts.tsx +11 -10
- package/src/handles/meta.ts +2 -2
- package/src/handles/script.ts +17 -16
- package/src/href-client.ts +1 -1
- package/src/html.ts +12 -0
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +1 -0
- package/src/loader-store.ts +7 -4
- package/src/prerender/build-shell-capture.ts +8 -11
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +117 -30
- package/src/route-definition/helpers-types.ts +33 -9
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +0 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +34 -25
- package/src/router/intercept-resolution.ts +125 -39
- package/src/router/loader-resolution.ts +56 -11
- package/src/router/match-api.ts +12 -17
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +93 -10
- package/src/router/match-middleware/cache-store.ts +17 -90
- package/src/router/middleware-types.ts +3 -5
- package/src/router/middleware.ts +52 -9
- package/src/router/prerender-match.ts +53 -19
- package/src/router/route-snapshot.ts +11 -1
- package/src/router/router-context.ts +2 -0
- package/src/router/router-interfaces.ts +25 -2
- package/src/router/router-options.ts +8 -5
- package/src/router/segment-resolution/fresh.ts +44 -12
- package/src/router/segment-resolution/loader-cache.ts +205 -45
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +6 -6
- package/src/router/segment-resolution/mask-nested.ts +7 -7
- package/src/router/segment-resolution/revalidation.ts +34 -6
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +8 -45
- package/src/rsc/shell-capture-constants.ts +13 -0
- package/src/rsc/shell-capture.ts +111 -90
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +11 -0
- package/src/server/context.ts +88 -11
- package/src/server/cookie-store.ts +5 -4
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +35 -30
- package/src/ssr/index.tsx +40 -7
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/generated-routes.ts +2 -2
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/internal/flight-client-globals.ts +7 -5
- package/src/testing/render-route.tsx +85 -24
- package/src/testing/run-loader.ts +5 -1
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +8 -6
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +12 -3
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
package/README.md
CHANGED
|
@@ -8,11 +8,19 @@ everything past the core is opt-in.
|
|
|
8
8
|
> releases and are noted in the changelog. npm `latest` is the current 0.x
|
|
9
9
|
> release; the `experimental` tag tracks `main` between tagged releases.
|
|
10
10
|
|
|
11
|
-
This page is a tour
|
|
12
|
-
along the way
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
This page is a tour. It builds one small shop in six steps and covers the
|
|
12
|
+
whole core API along the way:
|
|
13
|
+
|
|
14
|
+
1. **Pages**: nested layouts, named routes, typed params
|
|
15
|
+
2. **Data**: fetching in handlers, and loaders for data that stays live
|
|
16
|
+
3. **Mutations**: Server Actions that work without JavaScript
|
|
17
|
+
4. **Speed**: cached pages with live loaders, prefetching, prerendering
|
|
18
|
+
5. **APIs**: JSON and other responses served from the same URLs as pages
|
|
19
|
+
6. **Tests**: unit tests for loaders, e2e tests in dev and production
|
|
20
|
+
|
|
21
|
+
Everything else is opt-in and linked at the end. This page shows how the API
|
|
22
|
+
feels to use. For why it's designed this way, read
|
|
23
|
+
[Why Rango](https://github.com/rangojs/rango/blob/main/packages/rangojs-router/docs/why-rango.md).
|
|
16
24
|
|
|
17
25
|
## Start a new app
|
|
18
26
|
|
|
@@ -36,25 +44,46 @@ or newer.
|
|
|
36
44
|
If you are adding Rango to an existing Vite RSC project:
|
|
37
45
|
|
|
38
46
|
```bash
|
|
39
|
-
npm install @rangojs/router@latest react
|
|
47
|
+
npm install @rangojs/router@latest react react-dom
|
|
48
|
+
npm install -D vite
|
|
40
49
|
```
|
|
41
50
|
|
|
42
51
|
```ts
|
|
43
52
|
// vite.config.ts
|
|
44
|
-
import react from "@vitejs/plugin-react";
|
|
45
53
|
import { defineConfig } from "vite";
|
|
46
54
|
import { rango } from "@rangojs/router/vite";
|
|
47
55
|
|
|
48
56
|
export default defineConfig({
|
|
49
|
-
plugins: [
|
|
57
|
+
plugins: [rango()],
|
|
50
58
|
});
|
|
51
59
|
```
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
That is the whole Node setup. `rango()` includes `@vitejs/plugin-rsc` and
|
|
62
|
+
supplies the client and server entries, so you don't need an `index.html` or
|
|
63
|
+
entry files. It finds the router by looking for the `createRouter()` call in
|
|
64
|
+
your source. Add `@vitejs/plugin-react` only if you want Fast Refresh or the
|
|
65
|
+
React Compiler.
|
|
66
|
+
|
|
67
|
+
For Cloudflare Workers, use the `cloudflare` preset together with
|
|
68
|
+
`@cloudflare/vite-plugin`:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { cloudflare } from "@cloudflare/vite-plugin";
|
|
72
|
+
|
|
73
|
+
export default defineConfig({
|
|
74
|
+
plugins: [
|
|
75
|
+
rango({ preset: "cloudflare" }),
|
|
76
|
+
cloudflare({
|
|
77
|
+
viteEnvironment: { name: "rsc", childEnvironments: ["ssr"] },
|
|
78
|
+
}),
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The `vercel` preset turns a plain `vite build` into a ready-to-deploy
|
|
84
|
+
`.vercel/output` (Build Output API). See the
|
|
85
|
+
[`/cloudflare`](./skills/cloudflare/SKILL.md) and
|
|
86
|
+
[`/vercel`](./skills/vercel/SKILL.md) skills.
|
|
58
87
|
|
|
59
88
|
## Using the skills with your coding agent
|
|
60
89
|
|
|
@@ -75,60 +104,98 @@ is at `skills/catalog.json`.
|
|
|
75
104
|
|
|
76
105
|
## 1. Pages
|
|
77
106
|
|
|
78
|
-
A router is a tree. `path()` places a page, `layout()` wraps children,
|
|
79
|
-
`{ name }` gives a route an identity
|
|
107
|
+
A router is a tree. `path()` places a page, `layout()` wraps its children,
|
|
108
|
+
and `{ name }` gives a route an identity. Layouts nest, so each part of the
|
|
109
|
+
site wraps only what it owns:
|
|
80
110
|
|
|
81
111
|
```tsx
|
|
82
112
|
// src/router.tsx
|
|
83
113
|
import { createRouter, urls } from "@rangojs/router";
|
|
84
114
|
import { Document } from "./document";
|
|
115
|
+
import { SiteLayout } from "./layouts/site";
|
|
85
116
|
import { ShopLayout } from "./layouts/shop";
|
|
86
117
|
import { HomePage } from "./routes/home";
|
|
87
|
-
import { ProductPage } from "./routes/product";
|
|
118
|
+
import { ProductList, ProductPage } from "./routes/product";
|
|
88
119
|
|
|
89
120
|
const urlpatterns = urls(({ path, layout }) => [
|
|
90
|
-
layout(<
|
|
121
|
+
layout(<SiteLayout />, () => [
|
|
91
122
|
path("/", HomePage, { name: "home" }),
|
|
92
|
-
|
|
123
|
+
|
|
124
|
+
layout(<ShopLayout />, () => [
|
|
125
|
+
path("/products", ProductList, { name: "products" }),
|
|
126
|
+
path("/products/:slug", ProductPage, { name: "product" }),
|
|
127
|
+
]),
|
|
93
128
|
]),
|
|
94
129
|
]);
|
|
95
130
|
|
|
96
131
|
export const router = createRouter({ document: Document }).routes(urlpatterns);
|
|
97
132
|
```
|
|
98
133
|
|
|
134
|
+
A layout renders its children through `<Outlet />`. `SiteLayout` wraps every
|
|
135
|
+
page, and `ShopLayout` adds the category sidebar only to the product routes:
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
// src/layouts/site.tsx
|
|
139
|
+
import { Link, Outlet } from "@rangojs/router/client";
|
|
140
|
+
|
|
141
|
+
export function SiteLayout() {
|
|
142
|
+
return (
|
|
143
|
+
<>
|
|
144
|
+
<header>
|
|
145
|
+
<Link to="/">Home</Link> <Link to="/products">Shop</Link>
|
|
146
|
+
</header>
|
|
147
|
+
<Outlet /> {/* the home page or ShopLayout */}
|
|
148
|
+
</>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
99
153
|
```tsx
|
|
100
154
|
// src/layouts/shop.tsx
|
|
101
155
|
import { Outlet } from "@rangojs/router/client";
|
|
102
156
|
|
|
103
157
|
export function ShopLayout() {
|
|
104
158
|
return (
|
|
105
|
-
<div>
|
|
106
|
-
<
|
|
159
|
+
<div className="shop">
|
|
160
|
+
<CategorySidebar />
|
|
107
161
|
<main>
|
|
108
|
-
<Outlet /> {/*
|
|
162
|
+
<Outlet /> {/* the product list or a product page */}
|
|
109
163
|
</main>
|
|
110
164
|
</div>
|
|
111
165
|
);
|
|
112
166
|
}
|
|
113
167
|
```
|
|
114
168
|
|
|
169
|
+
When you navigate from `/products` to `/products/espresso-cup`, only the
|
|
170
|
+
innermost segment changes. Both layouts stay mounted, so they keep their
|
|
171
|
+
client state and aren't re-rendered on the server. A layout can also be a
|
|
172
|
+
server function `(ctx) => <Shell />` when it needs request data, and it can
|
|
173
|
+
carry its own loaders, middleware, and error boundaries. The
|
|
174
|
+
[`/layout` skill](./skills/layout/SKILL.md) covers these, and
|
|
175
|
+
[`/parallel`](./skills/parallel/SKILL.md) covers named slots such as a
|
|
176
|
+
sidebar and main column that load independently.
|
|
177
|
+
|
|
178
|
+
Outside all layouts is the document, the HTML shell passed to
|
|
179
|
+
`createRouter({ document })`:
|
|
180
|
+
|
|
115
181
|
```tsx
|
|
116
182
|
// src/document.tsx
|
|
117
183
|
"use client";
|
|
118
184
|
|
|
119
185
|
import type { ReactNode } from "react";
|
|
120
|
-
import {
|
|
186
|
+
import { Html } from "@rangojs/router/client";
|
|
121
187
|
|
|
122
188
|
export function Document({ children }: { children: ReactNode }) {
|
|
123
189
|
return (
|
|
124
190
|
<html lang="en">
|
|
125
191
|
<head>
|
|
126
|
-
<
|
|
127
|
-
<Scripts />
|
|
192
|
+
<Html.Meta />
|
|
193
|
+
<Html.Scripts />
|
|
128
194
|
</head>
|
|
129
195
|
<body>
|
|
130
|
-
<Scripts position="body" />
|
|
196
|
+
<Html.Scripts position="body" />
|
|
131
197
|
{children}
|
|
198
|
+
<Html.ScrollRestoration />
|
|
132
199
|
</body>
|
|
133
200
|
</html>
|
|
134
201
|
);
|
|
@@ -157,16 +224,18 @@ const url = ctx.reverse("product", { slug: "espresso-cup" });
|
|
|
157
224
|
// "/products/espresso-cup" — name and params compile-time checked
|
|
158
225
|
```
|
|
159
226
|
|
|
160
|
-
Rename `/products/:slug` to `/shop/:slug` in the one place it's defined and
|
|
161
|
-
every
|
|
162
|
-
|
|
163
|
-
`<Link to={href("/")}>Home</Link
|
|
227
|
+
Rename `/products/:slug` to `/shop/:slug` in the one place it's defined, and
|
|
228
|
+
every URL built with `ctx.reverse()` or `useReverse()` follows. In client
|
|
229
|
+
components, `href()` checks a literal path against the registered patterns
|
|
230
|
+
(`<Link to={href("/")}>Home</Link>`). After a rename, a stale `href()` path is
|
|
231
|
+
a compile error, not a broken link.
|
|
164
232
|
|
|
165
233
|
The tree is also lazy-first, which is the shape serverless cold starts want.
|
|
166
234
|
`include()` mounts a whole route module under a prefix — and with the async
|
|
167
235
|
form, `include("/shop", () => import("./shop"))`, the group is code-split:
|
|
168
236
|
its module doesn't load or run until a request matches it, a group nobody
|
|
169
|
-
visits never evaluates at all, and warm requests
|
|
237
|
+
visits never evaluates at all, and warm requests re-evaluate zero route
|
|
238
|
+
definitions.
|
|
170
239
|
Boot cost stays flat as the app grows — one module body at startup, not one
|
|
171
240
|
per group — while matching stays an `O(path length)` prefix trie, identical
|
|
172
241
|
in dev and production. None of this is assumed: the trie is benchmarked
|
|
@@ -230,16 +299,26 @@ export function StockBadge() {
|
|
|
230
299
|
}
|
|
231
300
|
```
|
|
232
301
|
|
|
233
|
-
Loaders run in parallel with the handler and stream
|
|
234
|
-
segment
|
|
235
|
-
|
|
236
|
-
|
|
302
|
+
Loaders run in parallel with the handler and stream their results.
|
|
303
|
+
`loading()` makes the segment render a skeleton first and stream the data
|
|
304
|
+
into it. Without `loading()`, the segment has no skeleton, and the HTML
|
|
305
|
+
normally arrives with the data already in place. To guarantee that a
|
|
306
|
+
loader's data, head tags, or a real 404 are in the first HTML flush, register
|
|
307
|
+
it with `loader(StockLoader, { ssr: false })`.
|
|
237
308
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
309
|
+
When a loader's data changes, every component that reads it updates too.
|
|
310
|
+
Calling `load()` from `useLoader()` refetches it on the client, and
|
|
311
|
+
`createLoader(fn, true)` makes a loader fetchable directly from client code
|
|
312
|
+
with `useFetchLoader()`. See the [`/loader` skill](./skills/loader/SKILL.md).
|
|
313
|
+
|
|
314
|
+
The rule of thumb:
|
|
315
|
+
|
|
316
|
+
- Fetch in the **handler** when the data belongs to the rendered page. If
|
|
317
|
+
you cache the page (step 4), that data is cached with it.
|
|
318
|
+
- Put data in a **loader** when it needs to stay independent of the page:
|
|
319
|
+
shared with client components, fresh on every request even when the page is
|
|
320
|
+
cached, refetchable from the client, or revalidated separately after
|
|
321
|
+
actions.
|
|
243
322
|
|
|
244
323
|
## 3. Mutations
|
|
245
324
|
|
|
@@ -251,8 +330,12 @@ JavaScript:
|
|
|
251
330
|
// src/actions/cart.ts
|
|
252
331
|
"use server";
|
|
253
332
|
|
|
254
|
-
export
|
|
333
|
+
export type CartState = { added?: string } | null;
|
|
334
|
+
|
|
335
|
+
export async function addToCart(_prev: CartState, formData: FormData) {
|
|
336
|
+
const productId = String(formData.get("productId"));
|
|
255
337
|
await db.cart.insert({ productId });
|
|
338
|
+
return { added: productId };
|
|
256
339
|
}
|
|
257
340
|
```
|
|
258
341
|
|
|
@@ -263,26 +346,36 @@ import { useActionState } from "react";
|
|
|
263
346
|
import { addToCart } from "../actions/cart";
|
|
264
347
|
|
|
265
348
|
export function AddToCart({ productId }: { productId: string }) {
|
|
266
|
-
const [, action, pending] = useActionState(
|
|
349
|
+
const [state, action, pending] = useActionState(addToCart, null);
|
|
267
350
|
return (
|
|
268
351
|
<form action={action}>
|
|
352
|
+
<input type="hidden" name="productId" value={productId} />
|
|
269
353
|
<button disabled={pending}>{pending ? "Adding…" : "Add to cart"}</button>
|
|
354
|
+
{state?.added && <p>Added.</p>}
|
|
270
355
|
</form>
|
|
271
356
|
);
|
|
272
357
|
}
|
|
273
358
|
```
|
|
274
359
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
360
|
+
Pass `addToCart` to the hook directly. Wrapping it in a client-side
|
|
361
|
+
closure such as `() => addToCart(id)` turns it into a function that only
|
|
362
|
+
exists in the browser. The form then has no server URL to post to before
|
|
363
|
+
JavaScript loads.
|
|
364
|
+
|
|
365
|
+
After an action, the route's segments and all loaders re-run by default, so
|
|
366
|
+
the UI reflects the new state. Parent layouts are skipped. `revalidate()`
|
|
367
|
+
lets you change that for a segment or loader. It matches actions by
|
|
368
|
+
**reference**, so renaming an action is a compile error, not a check that
|
|
369
|
+
silently stops matching:
|
|
279
370
|
|
|
280
371
|
```tsx
|
|
281
372
|
import * as CartActions from "./actions/cart";
|
|
282
373
|
|
|
283
|
-
path("/
|
|
284
|
-
|
|
285
|
-
|
|
374
|
+
path("/products/:slug", ProductPage, { name: "product" }, () => [
|
|
375
|
+
// After an action, re-run the cart summary only for cart actions.
|
|
376
|
+
// On plain navigation, `undefined` keeps the default behavior.
|
|
377
|
+
loader(CartSummaryLoader, () => [
|
|
378
|
+
revalidate((ctx) => (ctx.isAction() ? ctx.isAction(CartActions) : undefined)),
|
|
286
379
|
]),
|
|
287
380
|
]),
|
|
288
381
|
```
|
|
@@ -302,21 +395,31 @@ choice from step 2 pays off: handler data freezes with the shell, the
|
|
|
302
395
|
|
|
303
396
|
```tsx
|
|
304
397
|
const urlpatterns = urls(({ path, layout, loader, loading, cache }) => [
|
|
305
|
-
layout(<
|
|
398
|
+
layout(<SiteLayout />, () => [
|
|
306
399
|
path("/", HomePage, { name: "home" }),
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
400
|
+
|
|
401
|
+
layout(<ShopLayout />, () => [
|
|
402
|
+
path("/products", ProductList, { name: "products" }),
|
|
403
|
+
cache({ ttl: 600, swr: 3600, tags: ["products"] }, () => [
|
|
404
|
+
path("/products/:slug", ProductPage, { name: "product" }, () => [
|
|
405
|
+
loader(StockLoader), // never cached: re-runs on every hit
|
|
406
|
+
loading(<ProductSkeleton />),
|
|
407
|
+
]),
|
|
311
408
|
]),
|
|
312
409
|
]),
|
|
313
410
|
]),
|
|
314
411
|
]);
|
|
315
412
|
```
|
|
316
413
|
|
|
317
|
-
|
|
318
|
-
`CFCacheStore`
|
|
319
|
-
|
|
414
|
+
Set up a store once on the router. Use `MemorySegmentCacheStore` in
|
|
415
|
+
development, `CFCacheStore` on Cloudflare (Cache API with optional KV), or
|
|
416
|
+
`VercelCacheStore` on Vercel. The memory store expires entries at `ttl` and
|
|
417
|
+
does not serve stale content in the background, so you will only see `swr`
|
|
418
|
+
work on the Cloudflare and Vercel stores. The
|
|
419
|
+
[`/caching` skill](./skills/caching/SKILL.md) covers setup.
|
|
420
|
+
|
|
421
|
+
To invalidate, call `updateTag()` with the tag from the action that changes
|
|
422
|
+
the data:
|
|
320
423
|
|
|
321
424
|
```tsx
|
|
322
425
|
// src/actions/products.ts
|
|
@@ -329,27 +432,40 @@ export async function renameProduct(id: string, name: string) {
|
|
|
329
432
|
}
|
|
330
433
|
```
|
|
331
434
|
|
|
332
|
-
|
|
435
|
+
In production, links prefetch when they scroll into view. You can change
|
|
436
|
+
that per link with `prefetch`: `"hover"`, `"viewport"`, `"render"`,
|
|
437
|
+
`"adaptive"` (hover on pointer devices, viewport on touch), or `"none"`:
|
|
333
438
|
|
|
334
439
|
```tsx
|
|
335
|
-
<Link to={url} prefetch="
|
|
440
|
+
<Link to={url} prefetch="adaptive">
|
|
336
441
|
{product.name}
|
|
337
442
|
</Link>
|
|
338
443
|
```
|
|
339
444
|
|
|
340
|
-
|
|
341
|
-
loading flash
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
445
|
+
When a navigation has been fully prefetched, the page appears finished, with
|
|
446
|
+
no skeleton or loading flash. If you click while a prefetch is still loading,
|
|
447
|
+
the navigation reuses that request instead of starting a new one. Every
|
|
448
|
+
action clears the prefetch caches by default, so a prefetched page can't
|
|
449
|
+
show data from before a mutation.
|
|
450
|
+
|
|
451
|
+
You can do the same caching work at other times:
|
|
452
|
+
|
|
453
|
+
- **At build time:** `Prerender()` renders the page during `vite build` and
|
|
454
|
+
stores it in the same format as a runtime cache entry. Loaders still run
|
|
455
|
+
on every request. The browser can't tell a prerendered page from a cached
|
|
456
|
+
one. See the [`/prerender` skill](./skills/prerender/SKILL.md).
|
|
457
|
+
- **As an HTML shell:** `{ name: "product", ppr: true }` on a path stores
|
|
458
|
+
the static HTML around the loaders. On a hit, the router sends that shell
|
|
459
|
+
once middleware has run, then streams in only the live parts. See the
|
|
460
|
+
[`/ppr` skill](./skills/ppr/SKILL.md).
|
|
461
|
+
- **Per function:** `"use cache"` caches a single function or component,
|
|
462
|
+
with named profiles. See the [`/use-cache` skill](./skills/use-cache/SKILL.md).
|
|
348
463
|
|
|
349
464
|
## 5. An API, when you need one
|
|
350
465
|
|
|
351
|
-
Response routes live in the same tree
|
|
352
|
-
`path.xml()`, `path.image()`, `path.stream()
|
|
466
|
+
Response routes live in the same tree: `path.json()`, `path.text()`,
|
|
467
|
+
`path.html()`, `path.xml()`, `path.md()`, `path.image()`, `path.stream()`,
|
|
468
|
+
and `path.any()` for raw `Response`s such as WebSocket upgrades:
|
|
353
469
|
|
|
354
470
|
```tsx
|
|
355
471
|
path("/products/:slug", ProductPage, { name: "product" }),
|
|
@@ -370,11 +486,52 @@ type Product = RouteResponse<typeof urlpatterns, "productJson">;
|
|
|
370
486
|
See the [`/api-client` skill](./skills/api-client/SKILL.md) for a small typed
|
|
371
487
|
client over these endpoints.
|
|
372
488
|
|
|
489
|
+
## 6. Tests
|
|
490
|
+
|
|
491
|
+
Everything above can be tested at its own layer, through the same public
|
|
492
|
+
APIs your app uses. A loader runs in plain Node with the real request
|
|
493
|
+
context, and you supply the params:
|
|
494
|
+
|
|
495
|
+
```ts
|
|
496
|
+
// src/loaders/stock.test.ts
|
|
497
|
+
import { runLoader } from "@rangojs/router/testing";
|
|
498
|
+
import { StockLoader } from "./stock";
|
|
499
|
+
|
|
500
|
+
test("reports stock for a product", async () => {
|
|
501
|
+
const stock = await runLoader(StockLoader, {
|
|
502
|
+
params: { slug: "espresso-cup" },
|
|
503
|
+
});
|
|
504
|
+
expect(stock.inStock).toBe(true);
|
|
505
|
+
});
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
End to end, `parityDescribe` runs one test body against the dev server and
|
|
509
|
+
the production build. `expectParity` submits the form once with JavaScript
|
|
510
|
+
and once without, then checks that both show the same result:
|
|
511
|
+
|
|
512
|
+
```ts
|
|
513
|
+
// e2e/cart.test.ts
|
|
514
|
+
parityDescribe("add to cart", (f) => {
|
|
515
|
+
test("works the same with and without JavaScript", async ({ page }) => {
|
|
516
|
+
await page.goto(f.url("/products/espresso-cup"));
|
|
517
|
+
await expectParity(
|
|
518
|
+
page,
|
|
519
|
+
{ submit: { testId: "add-to-cart-form" } },
|
|
520
|
+
{ observe: ["cart-count"] },
|
|
521
|
+
);
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
`runMiddleware`, `renderHandler` (Server Component handlers rendered to a
|
|
527
|
+
real Flight tree), `renderRoute` (DOM tests) and the cache assertions follow
|
|
528
|
+
the same pattern. See the [`/testing` skill](./skills/testing/SKILL.md).
|
|
529
|
+
|
|
373
530
|
## Everything else, when you need it
|
|
374
531
|
|
|
375
|
-
That was the core: `path`/`layout`/`include`, names, loaders, actions
|
|
376
|
-
`revalidate`, `cache`, response routes.
|
|
377
|
-
when
|
|
532
|
+
That was the core: `path`/`layout`/`include`, names, loaders, actions and
|
|
533
|
+
`revalidate`, `cache`, response routes, and tests. Everything else is opt-in.
|
|
534
|
+
Use it when you need it:
|
|
378
535
|
|
|
379
536
|
| I need to… | Skill |
|
|
380
537
|
| ----------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
@@ -404,25 +561,28 @@ mental model that ties it together.
|
|
|
404
561
|
|
|
405
562
|
### Imports and subpaths
|
|
406
563
|
|
|
407
|
-
| Export
|
|
408
|
-
|
|
|
409
|
-
| `@rangojs/router`
|
|
410
|
-
| `@rangojs/router/client`
|
|
411
|
-
| `@rangojs/router/cache`
|
|
412
|
-
| `@rangojs/router/theme`
|
|
413
|
-
| `@rangojs/router/host`
|
|
414
|
-
| `@rangojs/router/vercel`
|
|
415
|
-
| `@rangojs/router/
|
|
416
|
-
| `@rangojs/router/
|
|
417
|
-
| `@rangojs/router/
|
|
418
|
-
| `@rangojs/router/
|
|
564
|
+
| Export | Description |
|
|
565
|
+
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
566
|
+
| `@rangojs/router` | Server/RSC core and shared types: `createRouter`, `urls`, `createLoader`, `Handler`, `Prerender`, `Meta` |
|
|
567
|
+
| `@rangojs/router/client` | Client: `Link`, `Outlet`, `href`, `useNavigation`, `useLoader`, `Html` |
|
|
568
|
+
| `@rangojs/router/cache` | Cache: `CFCacheStore`, `VercelCacheStore`, `MemorySegmentCacheStore`, `createDocumentCacheMiddleware` |
|
|
569
|
+
| `@rangojs/router/theme` | Theme: `useTheme`, `ThemeProvider`, `ThemeScript` |
|
|
570
|
+
| `@rangojs/router/host` | Host routing: `createHostRouter`, `defineHosts`, `isNoRouteMatchError` |
|
|
571
|
+
| `@rangojs/router/vercel` | Vercel: `createVercelTracing` (phase spans via `@vercel/otel`'s global tracer) |
|
|
572
|
+
| `@rangojs/router/cloudflare` | Cloudflare: `createCloudflareTracing` |
|
|
573
|
+
| `@rangojs/router/vite` | Vite plugin: `rango()` |
|
|
574
|
+
| `@rangojs/router/testing` | Consumer testing primitives: `runLoader`, `runMiddleware`, `dispatch`, cache assertions |
|
|
575
|
+
| `@rangojs/router/testing/*` | `/dom` (`renderRoute`), `/flight` (`renderHandler`), `/flight-matchers`, `/e2e` (`createRangoE2E`), `/vitest` (config helpers) |
|
|
576
|
+
| `@rangojs/router/host/testing` | Test helpers for host routers |
|
|
577
|
+
| `@rangojs/router/rsc` | Advanced server pipeline APIs: `createRSCHandler`, request-context access |
|
|
578
|
+
| `@rangojs/router/ssr` | Advanced SSR bridge APIs: `createSSRHandler` |
|
|
419
579
|
|
|
420
580
|
Use only subpaths that are explicitly exported; avoid deep imports.
|
|
421
581
|
|
|
422
582
|
The root entry is conditionally resolved: server-only APIs (`createRouter`,
|
|
423
583
|
`urls`, `redirect`, `Prerender`, `cookies`) run under the `react-server`
|
|
424
584
|
condition and throw guidance errors elsewhere. If you hit a root-entrypoint
|
|
425
|
-
stub error: hooks and components (`Link`, `Outlet`, `useLoader`, `
|
|
585
|
+
stub error: hooks and components (`Link`, `Outlet`, `useLoader`, `Html`)
|
|
426
586
|
live in `@rangojs/router/client`; cache APIs in `@rangojs/router/cache`;
|
|
427
587
|
host APIs in `@rangojs/router/host`.
|
|
428
588
|
|
|
@@ -431,14 +591,19 @@ host APIs in `@rangojs/router/host`.
|
|
|
431
591
|
The Vite plugin generates `router.named-routes.gen.ts` automatically (on dev
|
|
432
592
|
startup, HMR, and builds), registering route names, params, and search
|
|
433
593
|
schemas globally via `Rango.GeneratedRouteMap`. That powers `Handler<"name">`,
|
|
434
|
-
`ctx.reverse()`, and `RouteParams<"name">` with no
|
|
594
|
+
`ctx.reverse()`, `href()`, `Rango.Path` and `RouteParams<"name">` with no
|
|
595
|
+
manual registration. `RouteResponse<typeof urlpatterns, "name">` takes the
|
|
596
|
+
patterns directly, so it needs no registration either.
|
|
435
597
|
|
|
436
|
-
|
|
437
|
-
`
|
|
598
|
+
Two things are opt-in augmentations. `Rango.Env` types `ctx.env`.
|
|
599
|
+
`Rango.RegisteredRoutes` is needed only for `Rango.PathResponse` (looking up
|
|
600
|
+
a response payload by path):
|
|
438
601
|
|
|
439
602
|
```ts
|
|
440
603
|
// router.tsx
|
|
441
|
-
|
|
604
|
+
type AppBindings = { DB: D1Database };
|
|
605
|
+
|
|
606
|
+
export const router = createRouter<AppBindings>({}).routes(urlpatterns);
|
|
442
607
|
|
|
443
608
|
// The alias is required: an interface heritage clause cannot take a `typeof`
|
|
444
609
|
// type query directly (TS1109), so extend through a named alias.
|
|
@@ -446,7 +611,7 @@ type AppRoutes = typeof router.routeMap;
|
|
|
446
611
|
|
|
447
612
|
declare global {
|
|
448
613
|
namespace Rango {
|
|
449
|
-
interface Env extends
|
|
614
|
+
interface Env extends AppBindings {}
|
|
450
615
|
interface RegisteredRoutes extends AppRoutes {}
|
|
451
616
|
}
|
|
452
617
|
}
|