@rangojs/router 0.0.0-experimental.fb4fdc18 → 0.0.0-experimental.fce7fbd1
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 +9 -9
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +914 -485
- package/package.json +55 -11
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +214 -18
- package/skills/host-router/SKILL.md +45 -20
- package/skills/intercept/SKILL.md +26 -4
- package/skills/layout/SKILL.md +6 -7
- package/skills/links/SKILL.md +173 -17
- package/skills/loader/SKILL.md +149 -6
- package/skills/middleware/SKILL.md +13 -9
- package/skills/migrate-nextjs/SKILL.md +1 -1
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +5 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +242 -26
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +58 -9
- package/skills/route/SKILL.md +13 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +53 -41
- package/skills/testing/SKILL.md +599 -0
- package/skills/typesafety/SKILL.md +310 -26
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/event-controller.ts +42 -66
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +6 -6
- package/src/browser/navigation-client.ts +12 -15
- package/src/browser/navigation-store.ts +7 -8
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +9 -19
- package/src/browser/react/NavigationProvider.tsx +29 -40
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-params.ts +3 -4
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +14 -1
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +30 -16
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +2 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +49 -6
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +10 -8
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +26 -13
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +6 -4
- package/src/index.ts +13 -6
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +65 -41
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +238 -263
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +37 -14
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/handler-context.ts +4 -42
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +2 -2
- package/src/router/loader-resolution.ts +16 -2
- package/src/router/match-handlers.ts +62 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +32 -30
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +1 -1
- package/src/router/middleware.ts +46 -78
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +43 -1
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +19 -6
- package/src/router/segment-resolution/revalidation.ts +19 -6
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/types.ts +8 -0
- package/src/router.ts +37 -21
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +20 -65
- package/src/rsc/helpers.ts +22 -2
- package/src/rsc/index.ts +1 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/response-route-handler.ts +32 -52
- package/src/rsc/rsc-rendering.ts +27 -53
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +13 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +2 -2
- package/src/search-params.ts +4 -4
- package/src/segment-system.tsx +121 -65
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +118 -51
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +10 -0
- package/src/static-handler.ts +1 -1
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +193 -0
- package/src/testing/render-route.tsx +536 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +170 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +183 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +56 -11
- package/src/types/index.ts +1 -0
- package/src/types/segments.ts +18 -1
- package/src/urls/include-helper.ts +10 -53
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +11 -3
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +70 -48
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +19 -25
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +3 -7
- package/src/vite/plugins/client-ref-hashing.ts +12 -1
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
- package/src/vite/plugins/expose-action-id.ts +2 -2
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-internal-ids.ts +47 -67
- package/src/vite/plugins/performance-tracks.ts +12 -16
- package/src/vite/plugins/use-cache-transform.ts +13 -11
- package/src/vite/plugins/version-injector.ts +2 -12
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +67 -15
- package/src/vite/router-discovery.ts +208 -63
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-compiler
|
|
3
|
+
description: Enable the React Compiler in a Rango app the @vitejs/plugin-rsc way — a separate @rolldown/plugin-babel running reactCompilerPreset(), ordered after react() and before the plugin that supplies @vitejs/plugin-rsc. Use when a consumer wants to turn React Compiler on, hits the dead plugin-react v6 `react({ babel })` path, or is unsure why server components aren't being compiled.
|
|
4
|
+
argument-hint:
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# React Compiler
|
|
8
|
+
|
|
9
|
+
React Compiler is **opt-in** in Rango. The plugin pipeline is fully compatible —
|
|
10
|
+
you just add one more plugin. The catch on a current Rango stack (Vite 8 +
|
|
11
|
+
`@vitejs/plugin-react` v6) is that **v6 dropped its internal Babel for oxc**, so
|
|
12
|
+
the way the React docs and most blog posts show it — `react({ babel: { plugins:
|
|
13
|
+
[...] } })` — silently does nothing. The compiler has to be its own top-level
|
|
14
|
+
plugin.
|
|
15
|
+
|
|
16
|
+
## The shape (read first)
|
|
17
|
+
|
|
18
|
+
- The compiler is a **Babel** plugin, run via
|
|
19
|
+
[`@rolldown/plugin-babel`](https://www.npmjs.com/package/@rolldown/plugin-babel)
|
|
20
|
+
with `reactCompilerPreset()` from `@vitejs/plugin-react`.
|
|
21
|
+
- **Ordering is load-bearing:** put `babel(...)` **after `react()`** and
|
|
22
|
+
**before the plugin that supplies `@vitejs/plugin-rsc`**. In a default Rango
|
|
23
|
+
app that plugin is `rango()` itself; in a Cloudflare app it is
|
|
24
|
+
`@cloudflare/vite-plugin`.
|
|
25
|
+
- **It is client-only.** `reactCompilerPreset()` gates itself to the client
|
|
26
|
+
environment. Server/RSC components are not compiled, and that is the upstream
|
|
27
|
+
example's behavior — not a Rango limitation. See
|
|
28
|
+
[What gets compiled](#what-gets-compiled-client-only).
|
|
29
|
+
- **Rango's build-time prerender is unaffected.** You do not need to do anything
|
|
30
|
+
special. See [Prerender](#interaction-with-build-time-prerender).
|
|
31
|
+
|
|
32
|
+
## Step 1: Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pnpm add -D @rolldown/plugin-babel @babel/core babel-plugin-react-compiler
|
|
36
|
+
# TypeScript users also want the Babel core types:
|
|
37
|
+
pnpm add -D @types/babel__core
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
React 19 ships `react/compiler-runtime` in-tree, so there is **no** extra runtime
|
|
41
|
+
to install and **no** `target` option to set. Only pass `target: '17' | '18'` to
|
|
42
|
+
`reactCompilerPreset()` if you are on an older React.
|
|
43
|
+
|
|
44
|
+
## Step 2: Wire it in
|
|
45
|
+
|
|
46
|
+
### Default (non-Cloudflare) app
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// vite.config.ts
|
|
50
|
+
import { defineConfig } from "vite";
|
|
51
|
+
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
|
|
52
|
+
import babel from "@rolldown/plugin-babel";
|
|
53
|
+
import { rango } from "@rangojs/router/vite";
|
|
54
|
+
|
|
55
|
+
export default defineConfig({
|
|
56
|
+
plugins: [
|
|
57
|
+
react(),
|
|
58
|
+
babel({ presets: [reactCompilerPreset()] }),
|
|
59
|
+
rango(), // supplies @vitejs/plugin-rsc
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Cloudflare app
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// vite.config.ts
|
|
68
|
+
import { cloudflare } from "@cloudflare/vite-plugin";
|
|
69
|
+
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
|
|
70
|
+
import babel from "@rolldown/plugin-babel";
|
|
71
|
+
import { defineConfig } from "vite";
|
|
72
|
+
import { rango } from "@rangojs/router/vite";
|
|
73
|
+
|
|
74
|
+
export default defineConfig({
|
|
75
|
+
plugins: [
|
|
76
|
+
react(),
|
|
77
|
+
babel({ presets: [reactCompilerPreset()] }),
|
|
78
|
+
rango({ preset: "cloudflare" }),
|
|
79
|
+
cloudflare({
|
|
80
|
+
/* ... */
|
|
81
|
+
}), // supplies @vitejs/plugin-rsc
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## What gets compiled (client-only)
|
|
87
|
+
|
|
88
|
+
`reactCompilerPreset()` carries
|
|
89
|
+
`rolldown.applyToEnvironmentHook: (env) => env.config.consumer === "client"`, so
|
|
90
|
+
even though the babel plugin is top-level, the transform runs **only in the
|
|
91
|
+
`client` environment**:
|
|
92
|
+
|
|
93
|
+
| Environment | `consumer` | Compiled? |
|
|
94
|
+
| ----------- | ---------- | --------- |
|
|
95
|
+
| client | `client` | Yes |
|
|
96
|
+
| ssr | `server` | No |
|
|
97
|
+
| rsc | `server` | No |
|
|
98
|
+
|
|
99
|
+
This matches the upstream `@vitejs/plugin-rsc` example. If you genuinely need to
|
|
100
|
+
compile **server** components, you would have to invoke
|
|
101
|
+
`babel-plugin-react-compiler` yourself without the preset's
|
|
102
|
+
`applyToEnvironmentHook` — that is outside what the example does and is not
|
|
103
|
+
covered here.
|
|
104
|
+
|
|
105
|
+
## Options
|
|
106
|
+
|
|
107
|
+
`reactCompilerPreset()` forwards to `babel-plugin-react-compiler`:
|
|
108
|
+
|
|
109
|
+
| Option | Effect |
|
|
110
|
+
| ------------------------------- | -------------------------------------------------------------------------------------- |
|
|
111
|
+
| `compilationMode: 'annotation'` | Compile only components marked with the `"use memo"` directive, not every eligible one |
|
|
112
|
+
| `target: '17' \| '18'` | Emit `react-compiler-runtime` calls for React < 19. Omit on React 19+. |
|
|
113
|
+
|
|
114
|
+
## Interaction with build-time prerender
|
|
115
|
+
|
|
116
|
+
Nothing to configure. Rango's discovery/prerender step runs a throwaway temp Vite
|
|
117
|
+
server (`createTempRscServer`) that forwards only your **resolution** plugins
|
|
118
|
+
(`resolveId` / `load`). A pure transform plugin like `@rolldown/plugin-babel` is
|
|
119
|
+
intentionally **not** forwarded — and that is correct: the temp runner only
|
|
120
|
+
produces **data** (serialized Flight payloads + the route manifest), not shipped
|
|
121
|
+
code, and React Compiler is a memoization-only transform that does not change
|
|
122
|
+
rendered output. Your shipped client bundle still gets compiled, because the
|
|
123
|
+
babel plugin lives in your app's top-level plugin array alongside `react()`.
|
|
124
|
+
|
|
125
|
+
## Step 3: Verify the compiler actually ran
|
|
126
|
+
|
|
127
|
+
A compiled module imports the cache allocator from `react/compiler-runtime` and
|
|
128
|
+
calls `_c(n)`. Those two appear in **every** compiled module, so they are the
|
|
129
|
+
reliable per-module signal in dev:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
pnpm dev
|
|
133
|
+
# fetch any client component module straight from Vite and look for the markers:
|
|
134
|
+
curl -s "http://localhost:5173/src/components/SomeClientComponent.tsx" \
|
|
135
|
+
| grep -E "compiler-runtime|_c\("
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
For a production build, grep the built client bundle for the compiler's
|
|
139
|
+
input-independent cache check, which has a **zero baseline** without the compiler:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
pnpm build
|
|
143
|
+
grep -r "Symbol.for(\"react.memo_cache_sentinel\")" dist/client/assets/ | head
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Note the **comparison** form `$[i] === Symbol.for("react.memo_cache_sentinel")`
|
|
147
|
+
is only emitted for components with input-independent JSX, so it is reliable over
|
|
148
|
+
the **whole** client bundle, not necessarily in one chosen module. (React core
|
|
149
|
+
also defines that symbol once with a single `=` assignment, so count comparisons,
|
|
150
|
+
not the bare string.) Run the same grep over `dist/rsc` / `dist/ssr` and you
|
|
151
|
+
should find **none** — that is the client-only contract.
|
|
152
|
+
|
|
153
|
+
## Troubleshooting
|
|
154
|
+
|
|
155
|
+
| Symptom | Cause / fix |
|
|
156
|
+
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
157
|
+
| Nothing is compiled; no `compiler-runtime` import anywhere | You used `react({ babel: { plugins: [...] } })`. plugin-react v6 has no internal Babel — add `@rolldown/plugin-babel` as its own plugin. |
|
|
158
|
+
| Client compiled, but server/RSC components are not | Expected. `reactCompilerPreset()` is client-only (see the table). Not a bug. |
|
|
159
|
+
| `Cannot find module 'babel-plugin-react-compiler'` (or `@babel/core`) | Install the peer deps from Step 1; they are not bundled by `reactCompilerPreset()`. |
|
|
160
|
+
| Build pulls in `react-compiler-runtime` | You set `target: '17'`/`'18'` on React 19. Drop `target` — React 19 ships `react/compiler-runtime` in-tree. |
|
|
161
|
+
| Output looks compiled but a component misbehaves | The component likely breaks the Rules of React. Fix the component, or scope the compiler with `compilationMode: 'annotation'` while you do. |
|
|
162
|
+
|
|
163
|
+
## Reference
|
|
164
|
+
|
|
165
|
+
A worked, tested wiring (dev + production e2e markers, incl. the client-only
|
|
166
|
+
contract) lives in the `@rangojs/router` repo: `docs/react-compiler.md` and the
|
|
167
|
+
`react-compiler.test.ts` files under `e2e/e2e-basic`, `tests/cloudflare-basic`,
|
|
168
|
+
and `tests/vite-rsc-demo`.
|
|
@@ -236,22 +236,69 @@ type ProductsData = RouteResponse<typeof apiPatterns, "products">;
|
|
|
236
236
|
// = ResponseEnvelope<{ id: string; name: string; price: number }[]>
|
|
237
237
|
```
|
|
238
238
|
|
|
239
|
-
### PathResponse (global lookup by URL pattern)
|
|
239
|
+
### Rango.PathResponse (global lookup by URL pattern or concrete path)
|
|
240
240
|
|
|
241
|
-
|
|
241
|
+
`Rango.PathResponse` is ambient (no import) and reads from `RegisteredRoutes`,
|
|
242
|
+
which carries response payload metadata. That surface is **not** auto-wired —
|
|
243
|
+
without the augmentation below, `Rango.PathResponse` falls back to the generated
|
|
244
|
+
path/search map, or to a permissive map when nothing is generated. Either way, it
|
|
245
|
+
has no response payload metadata, so response routes resolve to
|
|
246
|
+
`ResponseEnvelope<never>`:
|
|
242
247
|
|
|
243
248
|
```typescript
|
|
244
|
-
|
|
249
|
+
// router.tsx
|
|
250
|
+
export const router = createRouter({ document: Document }).routes(urlpatterns);
|
|
245
251
|
|
|
252
|
+
declare global {
|
|
253
|
+
namespace Rango {
|
|
254
|
+
interface RegisteredRoutes extends typeof router.routeMap {}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
With that in place, look up the response type by URL pattern (ambient, no import):
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
246
262
|
// After include("/api", apiPatterns) in main urls
|
|
247
|
-
type Health = PathResponse<"/api/health">;
|
|
263
|
+
type Health = Rango.PathResponse<"/api/health">;
|
|
248
264
|
// = ResponseEnvelope<{ status: string; timestamp: number }>
|
|
249
265
|
|
|
250
266
|
// RSC routes return ResponseEnvelope<never>
|
|
251
|
-
type Home = PathResponse<"/">;
|
|
267
|
+
type Home = Rango.PathResponse<"/">;
|
|
252
268
|
// = ResponseEnvelope<never>
|
|
253
269
|
```
|
|
254
270
|
|
|
271
|
+
`Rango.PathResponse` also accepts a **concrete path**, so it types a `fetch`
|
|
272
|
+
wrapper whose response is inferred from the path you pass:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import { href } from "@rangojs/router/client";
|
|
276
|
+
|
|
277
|
+
async function get<T extends Rango.Path>(
|
|
278
|
+
path: T,
|
|
279
|
+
): Promise<Rango.PathResponse<T>> {
|
|
280
|
+
return fetch(href(path)).then((r) => r.json());
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const product = await get("/api/products/42"); // ResponseEnvelope<Product>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Pattern keys (`/:id`) match exactly; a concrete path under a _nested_ dynamic
|
|
287
|
+
route can match several patterns and union their responses.
|
|
288
|
+
|
|
289
|
+
`Rango.PathResponse` reports the JSON **wire** shape, not the handler's raw
|
|
290
|
+
return: `path.json()` serializes with `JSON.stringify`, so a handler returning
|
|
291
|
+
`{ createdAt: Date }` resolves to `ResponseEnvelope<{ createdAt: string }>`. This
|
|
292
|
+
runs through the ambient `Rango.JsonSerialize<T>` transform (`Date -> string`,
|
|
293
|
+
honors `toJSON()`, drops functions/`undefined`, `bigint -> never`). The
|
|
294
|
+
`RouteResponse` surface below applies the same `Rango.JsonSerialize` transform, so
|
|
295
|
+
both response lookups report the identical wire shape.
|
|
296
|
+
|
|
297
|
+
For local/scoped response typing without global augmentation, prefer
|
|
298
|
+
`RouteResponse<typeof patterns, "routeName">` (see the section above) — it reads
|
|
299
|
+
the response payload straight from the `urls()` patterns and needs no
|
|
300
|
+
`RegisteredRoutes` wiring.
|
|
301
|
+
|
|
255
302
|
### ParamsFor with Response Routes
|
|
256
303
|
|
|
257
304
|
```typescript
|
|
@@ -361,14 +408,16 @@ export const urlpatterns = urls(({ path, include }) => [
|
|
|
361
408
|
|
|
362
409
|
```typescript
|
|
363
410
|
import type { RouteResponse } from "@rangojs/router";
|
|
364
|
-
import type {
|
|
411
|
+
import type { ParamsFor } from "@rangojs/router/client";
|
|
365
412
|
|
|
366
|
-
// Scoped (before mount) -- use the module directly
|
|
413
|
+
// Scoped (before mount) -- use the module directly, no global wiring needed
|
|
367
414
|
type Stats = RouteResponse<typeof blogApiPatterns, "stats">;
|
|
368
415
|
// = ResponseEnvelope<{ views: number; visitors: number }>
|
|
369
416
|
|
|
370
|
-
// After mounting -- names get prefixed
|
|
371
|
-
|
|
417
|
+
// After mounting -- names get prefixed.
|
|
418
|
+
// Rango.PathResponse needs `RegisteredRoutes extends typeof router.routeMap` (see above),
|
|
419
|
+
// otherwise it resolves to ResponseEnvelope<never>.
|
|
420
|
+
type BlogStats = Rango.PathResponse<"/blog/api/stats">;
|
|
372
421
|
// = ResponseEnvelope<{ views: number; visitors: number }>
|
|
373
422
|
|
|
374
423
|
// Params work through nested includes
|
package/skills/route/SKILL.md
CHANGED
|
@@ -234,14 +234,22 @@ Cacheable vars (the default) can be read freely inside cache scopes.
|
|
|
234
234
|
|
|
235
235
|
### Revalidation Contracts for Handler Data
|
|
236
236
|
|
|
237
|
+
> **Scope: `revalidate()` is a partial-render concern, not a cache concern.**
|
|
238
|
+
> It decides whether this segment re-runs and streams to the client on a
|
|
239
|
+
> navigation or action — never whether a cached value is stale. The cache
|
|
240
|
+
> decides hit/miss/ttl/swr independently and never reads `revalidate()`. See
|
|
241
|
+
> `/cache-guide` → "Two axes" and `/rango` → "The shape of rango".
|
|
242
|
+
|
|
237
243
|
Handler-first guarantees apply within a single full render pass. For partial
|
|
238
244
|
action revalidation, define named revalidation contracts and reuse them on both
|
|
239
245
|
the producer route and the consumer child segments.
|
|
240
246
|
|
|
241
247
|
```typescript
|
|
242
248
|
// revalidation-contracts.ts
|
|
249
|
+
// Defer (|| undefined), not ?? false: a hard `false` short-circuits the chain,
|
|
250
|
+
// so when the same segment composes multiple contracts the later ones never run.
|
|
243
251
|
export const revalidateCheckoutData = ({ actionId }) =>
|
|
244
|
-
actionId?.includes("src/actions/checkout.ts#")
|
|
252
|
+
actionId?.includes("src/actions/checkout.ts#") || undefined;
|
|
245
253
|
|
|
246
254
|
path("/checkout", CheckoutPage, { name: "checkout" }, () => [
|
|
247
255
|
revalidate(revalidateCheckoutData), // producer (route handler) reruns
|
|
@@ -270,9 +278,6 @@ path("/checkout", CheckoutPage, { name: "checkout" }, () => [
|
|
|
270
278
|
]);
|
|
271
279
|
```
|
|
272
280
|
|
|
273
|
-
For scope/revalidation guarantees and non-guarantees, see:
|
|
274
|
-
[docs/execution-model.md](../../docs/internal/execution-model.md)
|
|
275
|
-
|
|
276
281
|
## Redirects
|
|
277
282
|
|
|
278
283
|
### Basic redirect
|
|
@@ -403,6 +408,10 @@ urls(({ path, layout }) => [
|
|
|
403
408
|
])
|
|
404
409
|
```
|
|
405
410
|
|
|
411
|
+
## View Transitions
|
|
412
|
+
|
|
413
|
+
A route can configure its own `transition()` — the wrap goes around the route's component itself (routes are leaves; they have no separate default outlet channel). If the route component renders a `<ParallelOutlet />` directly, that slot remains inside the route's VT subtree, so prefer mounting parallel slots in a layout when combining intercept modals with route-level transitions. See [skills/view-transitions](../view-transitions/SKILL.md) for examples and the wrap-location rules across layouts, routes, and slots.
|
|
414
|
+
|
|
406
415
|
## Handler-attached `.use`
|
|
407
416
|
|
|
408
417
|
Page handlers can carry their own loader, middleware, error boundaries, parallels, and other defaults via a `.use` callback — so the page is self-contained and reusable across mount sites without re-wiring the same items.
|
|
@@ -71,7 +71,7 @@ urls(
|
|
|
71
71
|
## Router Options
|
|
72
72
|
|
|
73
73
|
```typescript
|
|
74
|
-
interface
|
|
74
|
+
interface RangoOptions<TEnv> {
|
|
75
75
|
// URL patterns from urls() function
|
|
76
76
|
urls: UrlPatterns;
|
|
77
77
|
|
|
@@ -405,7 +405,7 @@ interface AppBindings {
|
|
|
405
405
|
KV: KVNamespace;
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
// Variables declared via
|
|
408
|
+
// Variables declared via global namespace augmentation
|
|
409
409
|
interface AppVariables {
|
|
410
410
|
user?: { id: string; name: string };
|
|
411
411
|
}
|
|
@@ -417,7 +417,7 @@ const router = createRouter<AppBindings>({
|
|
|
417
417
|
|
|
418
418
|
// Register types globally for implicit typing
|
|
419
419
|
declare global {
|
|
420
|
-
namespace
|
|
420
|
+
namespace Rango {
|
|
421
421
|
interface Env extends AppBindings {}
|
|
422
422
|
interface Vars extends AppVariables {}
|
|
423
423
|
}
|
|
@@ -32,35 +32,37 @@ Actions mutate state; route handlers and loaders read the latest state. After
|
|
|
32
32
|
an action finishes, Rango performs a server-side revalidation render for the
|
|
33
33
|
matched route so the UI receives fresh segment output and loader data.
|
|
34
34
|
|
|
35
|
-
The main control point is `revalidate((
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
The main control point is `revalidate((ctx) => ...)` on the segment that owns
|
|
36
|
+
the data. Match specific actions by imported reference with `ctx.isAction()`;
|
|
37
|
+
use raw `actionId` only when you intentionally need path or directory matching.
|
|
38
|
+
This applies to `path()` handlers, `layout()` handlers, `parallel()` slots,
|
|
39
|
+
`intercept()` routes, and loader registrations:
|
|
38
40
|
|
|
39
41
|
```typescript
|
|
40
42
|
// urls.tsx — path/layout/parallel/intercept/loader/revalidate are passed in by urls()
|
|
41
43
|
import { urls } from "@rangojs/router";
|
|
44
|
+
import * as CartActions from "./actions/cart";
|
|
42
45
|
|
|
43
46
|
export const urlpatterns = urls(({ path, loader, revalidate }) => [
|
|
44
47
|
// The loader belongs to the route that consumes its data — nest it inside
|
|
45
48
|
// the owning path() so the segment owns its data dependency.
|
|
46
49
|
path("/cart", CartPage, { name: "cart" }, () => [
|
|
47
|
-
revalidate(
|
|
48
|
-
({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
|
|
49
|
-
),
|
|
50
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
50
51
|
loader(CartLoader, () => [
|
|
51
|
-
revalidate(
|
|
52
|
-
({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
|
|
53
|
-
),
|
|
52
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
54
53
|
]),
|
|
55
54
|
]),
|
|
56
55
|
]);
|
|
57
56
|
```
|
|
58
57
|
|
|
59
|
-
|
|
58
|
+
`ctx.isAction()` resolves the imported action reference the same way the router
|
|
59
|
+
derives `actionId`, so it matches in both dev and production and survives action
|
|
60
|
+
renames/moves as type errors instead of silent substring drift.
|
|
61
|
+
|
|
62
|
+
For module-level `"use server"` files, the raw `actionId` passed to every
|
|
60
63
|
server-side `revalidate()` predicate is path-bearing in the server/RSC
|
|
61
|
-
environment in both dev and production: `src/actions/cart.ts#addToCart`. This
|
|
62
|
-
|
|
63
|
-
predicates can filter by action file, directory, or export name.
|
|
64
|
+
environment in both dev and production: `src/actions/cart.ts#addToCart`. This is
|
|
65
|
+
the escape hatch for broad filters by action file, directory, or export name.
|
|
64
66
|
|
|
65
67
|
Actions and the follow-up revalidation render share one request context.
|
|
66
68
|
Values written in the action with `ctx.set(MyVar, value)` or `ctx.set("key",
|
|
@@ -91,15 +93,18 @@ export async function switchTenant(tenantId: string) {
|
|
|
91
93
|
```typescript
|
|
92
94
|
// urls.tsx
|
|
93
95
|
import { urls } from "@rangojs/router";
|
|
96
|
+
import * as TenantActions from "./actions/tenant";
|
|
94
97
|
import { ChangedTenant } from "./context";
|
|
95
98
|
|
|
96
99
|
export const urlpatterns = urls(({ path, revalidate }) => [
|
|
97
100
|
path("/dashboard/:tenantId", DashboardPage, { name: "dashboard" }, () => [
|
|
98
|
-
revalidate(
|
|
99
|
-
(
|
|
100
|
-
|
|
101
|
-
context.get(ChangedTenant) === context.params.tenantId
|
|
102
|
-
|
|
101
|
+
revalidate((ctx) => {
|
|
102
|
+
if (!ctx.isAction(TenantActions)) return undefined;
|
|
103
|
+
return (
|
|
104
|
+
ctx.context.get(ChangedTenant) === ctx.context.params.tenantId ||
|
|
105
|
+
undefined
|
|
106
|
+
);
|
|
107
|
+
}),
|
|
103
108
|
]),
|
|
104
109
|
]);
|
|
105
110
|
```
|
|
@@ -380,13 +385,18 @@ re-render so the UI updates. Rango runs the action, then evaluates
|
|
|
380
385
|
`revalidate()` on matched segments and loaders. Each path, layout, parallel,
|
|
381
386
|
intercept, or loader rule decides whether that piece re-renders/re-resolves.
|
|
382
387
|
|
|
383
|
-
|
|
384
|
-
|
|
388
|
+
Use `ctx.isAction()` for specific actions or modules. It accepts one action,
|
|
389
|
+
several actions, or a namespace import (`import * as CartActions`). Pair it with
|
|
390
|
+
`|| undefined` for "revalidate on match, otherwise defer to defaults/downstream
|
|
391
|
+
rules."
|
|
385
392
|
|
|
386
393
|
```typescript
|
|
387
394
|
// urls.tsx — inside the urls() callback. Nest each loader inside the path(),
|
|
388
395
|
// layout(), or parallel() that owns its data so the route tree mirrors the
|
|
389
396
|
// data dependencies.
|
|
397
|
+
import * as AccountActions from "./actions/account";
|
|
398
|
+
import * as CartActions from "./actions/cart";
|
|
399
|
+
|
|
390
400
|
urls(({ path, loader, revalidate }) => [
|
|
391
401
|
path("/", HomePage, { name: "home" }, () => [
|
|
392
402
|
// Loader data re-runs by default after any action. Opt out with revalidate(() => false).
|
|
@@ -395,36 +405,37 @@ urls(({ path, loader, revalidate }) => [
|
|
|
395
405
|
|
|
396
406
|
// Re-render the cart page handler AND re-resolve its loader after cart actions
|
|
397
407
|
path("/cart", CartPage, { name: "cart" }, () => [
|
|
398
|
-
revalidate(
|
|
399
|
-
({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
|
|
400
|
-
),
|
|
408
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
401
409
|
loader(CartLoader, () => [
|
|
402
|
-
revalidate(
|
|
403
|
-
({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
|
|
404
|
-
),
|
|
410
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
405
411
|
]),
|
|
406
412
|
]),
|
|
407
413
|
|
|
408
|
-
// Re-run after any action
|
|
414
|
+
// Re-run after any action exported by the account actions module
|
|
409
415
|
path("/account", AccountPage, { name: "account" }, () => [
|
|
410
416
|
loader(AccountLoader, () => [
|
|
411
|
-
revalidate(
|
|
412
|
-
({ actionId }) => actionId?.startsWith("src/actions/account/") ?? false,
|
|
413
|
-
),
|
|
417
|
+
revalidate((ctx) => ctx.isAction(AccountActions) || undefined),
|
|
414
418
|
]),
|
|
415
419
|
]),
|
|
416
420
|
]);
|
|
417
421
|
```
|
|
418
422
|
|
|
419
|
-
`actionId`
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
423
|
+
The raw `actionId` string stays available for broad path filters:
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
// Match any action under src/actions/account/, including modules not imported here.
|
|
427
|
+
revalidate(
|
|
428
|
+
({ actionId }) => actionId?.startsWith("src/actions/account/") || undefined,
|
|
429
|
+
);
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
For actions exported from a module-level `"use server"` file, the ID is prefixed
|
|
433
|
+
with the source file path (`src/actions/cart.ts#addToCart`). **Inline `"use
|
|
434
|
+
server"` actions** (declared inside an RSC component) intentionally keep their
|
|
435
|
+
hashed IDs — file paths are withheld from the client for security. If you need
|
|
436
|
+
file-path-based revalidation predicates, define the action in a module-level
|
|
437
|
+
`"use server"` file rather than inline. See `/loader` for the full revalidation
|
|
438
|
+
contract (deferred returns, soft suggestions).
|
|
428
439
|
|
|
429
440
|
### Cross-segment dependencies
|
|
430
441
|
|
|
@@ -434,8 +445,9 @@ stale context. Share the same `revalidate` predicate on both producer and
|
|
|
434
445
|
consumer:
|
|
435
446
|
|
|
436
447
|
```typescript
|
|
437
|
-
|
|
438
|
-
|
|
448
|
+
import * as CartActions from "./actions/cart";
|
|
449
|
+
|
|
450
|
+
const revalidateCart = (ctx) => ctx.isAction(CartActions) || undefined;
|
|
439
451
|
|
|
440
452
|
urls(({ path, layout, loader, revalidate }) => [
|
|
441
453
|
layout(CartLayout, () => [
|