@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2
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/AGENTS.md +6 -10
- package/README.md +289 -938
- package/dist/bin/rango.js +271 -46
- package/dist/vite/index.js +673 -193
- package/package.json +10 -8
- package/skills/api-client/SKILL.md +1 -1
- package/skills/breadcrumbs/SKILL.md +31 -14
- package/skills/cache-guide/SKILL.md +5 -2
- package/skills/caching/SKILL.md +59 -4
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/debug-manifest/SKILL.md +1 -1
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +9 -1
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +8 -8
- package/skills/hooks/SKILL.md +54 -892
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +4 -4
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +46 -14
- package/skills/layout/SKILL.md +27 -10
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +23 -1
- package/skills/middleware/SKILL.md +7 -3
- package/skills/migrate-nextjs/SKILL.md +167 -6
- package/skills/migrate-react-router/SKILL.md +59 -677
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/observability/SKILL.md +9 -1
- package/skills/parallel/SKILL.md +23 -4
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +28 -18
- package/skills/rango/SKILL.md +84 -25
- package/skills/response-routes/SKILL.md +15 -1
- package/skills/route/SKILL.md +71 -4
- package/skills/router-setup/SKILL.md +14 -3
- package/skills/scripts/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +3 -2
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +1 -1
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/handles.md +4 -2
- package/skills/testing/render-handler.md +15 -14
- package/skills/testing/reverse-and-types.md +8 -7
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -919
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +30 -3
- package/skills/vercel/SKILL.md +1 -1
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/browser/event-controller.ts +62 -10
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +65 -16
- package/src/browser/navigation-client.ts +32 -2
- package/src/browser/navigation-store.ts +128 -14
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +76 -17
- package/src/browser/prefetch/cache.ts +51 -11
- package/src/browser/prefetch/fetch.ts +59 -21
- package/src/browser/prefetch/queue.ts +19 -4
- package/src/browser/react/Link.tsx +13 -3
- package/src/browser/react/NavigationProvider.tsx +108 -4
- package/src/browser/response-adapter.ts +38 -9
- package/src/browser/rsc-router.tsx +54 -4
- package/src/browser/scroll-restoration.ts +7 -5
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +22 -10
- package/src/browser/types.ts +54 -1
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/index.ts +3 -1
- package/src/build/route-trie.ts +35 -7
- package/src/build/route-types/include-resolution.ts +347 -47
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-runtime.ts +262 -71
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +243 -20
- package/src/cache/document-cache.ts +54 -21
- package/src/cache/index.ts +1 -0
- package/src/cache/memory-segment-store.ts +110 -3
- package/src/cache/profile-registry.ts +15 -0
- package/src/cache/read-through-swr.ts +15 -1
- package/src/cache/segment-codec.ts +4 -4
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/types.ts +158 -0
- package/src/cache/vercel/vercel-cache-store.ts +401 -124
- package/src/client.rsc.tsx +0 -3
- package/src/client.tsx +0 -3
- package/src/cloudflare/tracing.ts +7 -8
- package/src/defer.ts +11 -22
- package/src/handle.ts +37 -15
- package/src/handles/MetaTags.tsx +16 -82
- package/src/handles/breadcrumbs.ts +12 -14
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +7 -8
- package/src/handles/meta.ts +7 -44
- package/src/host/errors.ts +15 -0
- package/src/host/index.ts +1 -0
- package/src/index.rsc.ts +8 -2
- package/src/index.ts +19 -13
- package/src/internal-debug.ts +11 -8
- package/src/prerender.ts +17 -4
- package/src/redirect-origin.ts +14 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/route-content-wrapper.tsx +12 -5
- package/src/route-definition/dsl-helpers.ts +21 -32
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +43 -43
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/resolve-handler-use.ts +0 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -4
- package/src/route-types.ts +0 -5
- package/src/router/find-match.ts +86 -8
- package/src/router/instrument.ts +9 -4
- package/src/router/lazy-includes.ts +72 -12
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/manifest.ts +56 -11
- package/src/router/match-api.ts +76 -32
- package/src/router/match-handlers.ts +181 -135
- package/src/router/match-middleware/background-revalidation.ts +40 -23
- package/src/router/match-middleware/cache-store.ts +39 -24
- package/src/router/match-result.ts +35 -15
- package/src/router/middleware.ts +64 -38
- package/src/router/navigation-snapshot.ts +7 -5
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +53 -64
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +11 -5
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +23 -8
- package/src/router/route-snapshot.ts +14 -2
- package/src/router/router-context.ts +3 -1
- package/src/router/router-interfaces.ts +32 -1
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +39 -3
- package/src/router/segment-resolution/loader-cache.ts +93 -2
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +3 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +6 -8
- package/src/router/telemetry.ts +9 -1
- package/src/router/tracing.ts +14 -5
- package/src/router/trie-matching.ts +19 -11
- package/src/router/url-params.ts +13 -0
- package/src/router.ts +47 -16
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler.ts +60 -33
- package/src/rsc/manifest-init.ts +1 -1
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/progressive-enhancement.ts +61 -4
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +429 -37
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +30 -0
- package/src/segment-loader-promise.ts +18 -0
- package/src/segment-system.tsx +149 -14
- package/src/server/context.ts +67 -9
- package/src/server/cookie-store.ts +73 -1
- package/src/server/loader-registry.ts +13 -1
- package/src/server/request-context.ts +169 -10
- package/src/ssr/index.tsx +462 -178
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/testing/collect-handle.ts +14 -8
- package/src/testing/dispatch.ts +152 -40
- package/src/testing/generated-routes.ts +27 -11
- package/src/testing/index.ts +6 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/render-route.tsx +13 -10
- package/src/testing/run-transition-when.ts +164 -0
- package/src/theme/ThemeProvider.tsx +36 -26
- package/src/types/handler-context.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/route-config.ts +19 -7
- package/src/types/segments.ts +100 -0
- package/src/urls/include-helper.ts +10 -8
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +44 -12
- package/src/urls/path-helper.ts +5 -0
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +43 -18
- package/src/urls/urls-function.ts +0 -1
- package/src/vercel/tracing.ts +7 -7
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/index.ts +7 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugins/vercel-output.ts +114 -25
- package/src/vite/plugins/version-injector.ts +22 -7
- package/src/vite/plugins/virtual-entries.ts +80 -22
- package/src/vite/rango.ts +29 -19
- package/src/vite/router-discovery.ts +171 -43
- package/src/vite/utils/prerender-utils.ts +17 -4
- package/src/vite/utils/shared-utils.ts +47 -0
- package/src/network-error-thrower.tsx +0 -18
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# State and Cache Control Hooks
|
|
2
|
+
|
|
3
|
+
## State Hooks
|
|
4
|
+
|
|
5
|
+
### useLocationState()
|
|
6
|
+
|
|
7
|
+
Read type-safe state from history:
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
"use client";
|
|
11
|
+
import { useLocationState, createLocationState } from "@rangojs/router/client";
|
|
12
|
+
|
|
13
|
+
// Define typed state (all export patterns supported)
|
|
14
|
+
// Keys are auto-injected by the Vite plugin -- no manual key needed.
|
|
15
|
+
export const ProductState = createLocationState<{
|
|
16
|
+
name: string;
|
|
17
|
+
price: number;
|
|
18
|
+
}>();
|
|
19
|
+
|
|
20
|
+
// Also valid: const ProductState = createLocationState<...>();
|
|
21
|
+
// export { ProductState };
|
|
22
|
+
// Also valid: export { ProductState as MyState };
|
|
23
|
+
|
|
24
|
+
function ProductHeader() {
|
|
25
|
+
const state = useLocationState(ProductState);
|
|
26
|
+
// { name: string; price: number } | undefined
|
|
27
|
+
|
|
28
|
+
if (state) {
|
|
29
|
+
return (
|
|
30
|
+
<h1>
|
|
31
|
+
{state.name} - ${state.price}
|
|
32
|
+
</h1>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return <h1>Loading...</h1>;
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Pass state through Link:
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { Link } from "@rangojs/router/client";
|
|
43
|
+
import { ProductState } from "./state";
|
|
44
|
+
|
|
45
|
+
<Link to="/product/123" state={[ProductState({ name: "Widget", price: 99 })]}>
|
|
46
|
+
View Product
|
|
47
|
+
</Link>;
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Pass typed state just in time (getter evaluated at click time, not render time):
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
"use client"; // JIT state requires a client component (getter can't cross RSC boundary)
|
|
54
|
+
|
|
55
|
+
import { Link } from "@rangojs/router/client";
|
|
56
|
+
import { ProductState } from "./state";
|
|
57
|
+
|
|
58
|
+
// The getter is stored lazily and only called when the user clicks the link.
|
|
59
|
+
// This is useful for capturing values that change after render (e.g., scroll
|
|
60
|
+
// position, form state, ref values).
|
|
61
|
+
<Link
|
|
62
|
+
to="/product/123"
|
|
63
|
+
state={[ProductState(() => ({ name: product.name, price: product.price }))]}
|
|
64
|
+
>
|
|
65
|
+
View Product
|
|
66
|
+
</Link>;
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Plain state can also be evaluated just in time (also requires a client component):
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
<Link to="/product/123" state={() => ({ from: window.location.pathname })}>
|
|
73
|
+
View Product
|
|
74
|
+
</Link>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Flash State (read-once)
|
|
78
|
+
|
|
79
|
+
Create a location state with `{ flash: true }` for read-once state that
|
|
80
|
+
auto-clears after first render. Ideal for flash messages (success/error
|
|
81
|
+
notifications after redirect):
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
// location-states.ts
|
|
85
|
+
import { createLocationState } from "@rangojs/router";
|
|
86
|
+
|
|
87
|
+
export const FlashMessage = createLocationState<{ text: string }>({
|
|
88
|
+
flash: true,
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Read flash state with `useLocationState` (same hook as persistent state):
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
"use client";
|
|
96
|
+
import { useLocationState } from "@rangojs/router/client";
|
|
97
|
+
import { FlashMessage } from "../location-states";
|
|
98
|
+
|
|
99
|
+
function FlashBanner() {
|
|
100
|
+
const flash = useLocationState(FlashMessage);
|
|
101
|
+
// { text: string } | undefined
|
|
102
|
+
|
|
103
|
+
if (!flash) return null;
|
|
104
|
+
return <div className="flash">{flash.text}</div>;
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Flash behavior is determined by the definition (`{ flash: true }`), not by which
|
|
109
|
+
hook reads it. `useLocationState` reads the value synchronously during render,
|
|
110
|
+
then clears it from `history.state` via `replaceState` in a `useEffect`.
|
|
111
|
+
Multiple components reading the same flash definition all see the value.
|
|
112
|
+
Pressing back/forward will not re-show the flash since it was cleared.
|
|
113
|
+
|
|
114
|
+
Set flash state from the server via `redirect()` with state:
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
// In a route handler
|
|
118
|
+
import { redirect, createLocationState } from "@rangojs/router";
|
|
119
|
+
|
|
120
|
+
export const FlashMessage = createLocationState<{ text: string }>({
|
|
121
|
+
flash: true,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Handler
|
|
125
|
+
(ctx) => {
|
|
126
|
+
return redirect("/dashboard", {
|
|
127
|
+
state: [FlashMessage({ text: "Item saved!" })],
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Or via `ctx.setLocationState()` on any response:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
(ctx) => {
|
|
136
|
+
ctx.setLocationState(FlashMessage({ text: "Welcome back!" }));
|
|
137
|
+
return <Dashboard />;
|
|
138
|
+
};
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### .read() (non-hook access)
|
|
142
|
+
|
|
143
|
+
Read current location state outside React components (client-side only):
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
import { FlashMessage, ProductState } from "../location-states";
|
|
147
|
+
|
|
148
|
+
// Returns TState | undefined. Returns undefined during SSR.
|
|
149
|
+
const flash = FlashMessage.read();
|
|
150
|
+
const product = ProductState.read();
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
> **Hydration:** `.read()` returns `undefined` on the server but may return
|
|
154
|
+
> a real value on the first client render (history state survives reload).
|
|
155
|
+
> Do not call `.read()` directly during the initial render of a component;
|
|
156
|
+
> call it from an event handler or inside a `useEffect` post-mount. For
|
|
157
|
+
> reactive hydration-safe access, use `useLocationState()` instead.
|
|
158
|
+
|
|
159
|
+
### .write() / .delete() (static, non-reactive)
|
|
160
|
+
|
|
161
|
+
Static counterparts to `.read()`. Both mutate the current history entry's
|
|
162
|
+
`history.state` via `replaceState`, preserving any other keys (router
|
|
163
|
+
bookkeeping, other location state slots). Both are client-only; they throw
|
|
164
|
+
when called on the server.
|
|
165
|
+
|
|
166
|
+
Neither dispatches an event, so components reading via `useLocationState`
|
|
167
|
+
will NOT re-render until the next navigation/popstate. Pair with `.read()`
|
|
168
|
+
(or a fresh mount via back/forward/reload) instead.
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
"use client";
|
|
172
|
+
import { ProductState } from "./state";
|
|
173
|
+
|
|
174
|
+
// Persisted across hard refresh and back/forward of this entry.
|
|
175
|
+
ProductState.write({ name: "Widget", price: 9.99 });
|
|
176
|
+
|
|
177
|
+
// Read later (or on next mount).
|
|
178
|
+
const current = ProductState.read();
|
|
179
|
+
|
|
180
|
+
// Manually clear the slot. Idempotent if it isn't set.
|
|
181
|
+
ProductState.delete();
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
| Method | Updates `history.state` | Fires `useLocationState` rerender | SSR behavior |
|
|
185
|
+
| ----------- | ----------------------- | --------------------------------- | ------------------- |
|
|
186
|
+
| `.read()` | no | n/a (returns snapshot) | returns `undefined` |
|
|
187
|
+
| `.write()` | yes (replace this slot) | no | throws |
|
|
188
|
+
| `.delete()` | yes (remove this slot) | no | throws |
|
|
189
|
+
|
|
190
|
+
## Cache Control
|
|
191
|
+
|
|
192
|
+
### invalidateClientCache()
|
|
193
|
+
|
|
194
|
+
Force the client's caches to miss after a mutation the router can't see (a REST
|
|
195
|
+
call, a WebSocket push, a login). It is a plain function, not a hook, so it works
|
|
196
|
+
from module-level callbacks too. Imported from the root entry `@rangojs/router`,
|
|
197
|
+
it is selected by export conditions: in a client component it marks the caches
|
|
198
|
+
stale immediately; from a handler/server component it writes a rotated
|
|
199
|
+
`Set-Cookie` for the responding client.
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
"use client";
|
|
203
|
+
import { invalidateClientCache } from "@rangojs/router";
|
|
204
|
+
|
|
205
|
+
function SaveButton() {
|
|
206
|
+
const handleSave = async () => {
|
|
207
|
+
await fetch("/api/data", {
|
|
208
|
+
method: "POST",
|
|
209
|
+
body: JSON.stringify(data),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Invalidate the client's caches after the mutation
|
|
213
|
+
invalidateClientCache();
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
return <button onClick={handleSave}>Save</button>;
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
A module-level subscription works the same way (no component needed):
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import { invalidateClientCache } from "@rangojs/router";
|
|
224
|
+
|
|
225
|
+
socket.on("catalog-updated", () => invalidateClientCache());
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Use cases**: REST API mutations, WebSocket updates, non-RSC data changes.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# URL Hooks
|
|
2
|
+
|
|
3
|
+
### useParams()
|
|
4
|
+
|
|
5
|
+
Access route params from the current URL:
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
"use client";
|
|
9
|
+
import { useParams } from "@rangojs/router/client";
|
|
10
|
+
|
|
11
|
+
// Route: /product/:productId
|
|
12
|
+
function ProductPage() {
|
|
13
|
+
const params = useParams();
|
|
14
|
+
// { productId: "123" }
|
|
15
|
+
|
|
16
|
+
return <h1>Product {params.productId}</h1>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Annotate the expected shape via a generic
|
|
20
|
+
function ProductPageTyped() {
|
|
21
|
+
const { productId } = useParams<{ productId: string }>();
|
|
22
|
+
return <h1>Product {productId}</h1>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// With selector for performance (re-renders only when selected value changes)
|
|
26
|
+
function ProductId() {
|
|
27
|
+
const productId = useParams((p) => p.productId);
|
|
28
|
+
return <span>ID: {productId}</span>;
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Returns merged params from all matched route segments as a `Readonly<T>` map. Updates on navigation commit (not during pending navigation).
|
|
33
|
+
|
|
34
|
+
### usePathname()
|
|
35
|
+
|
|
36
|
+
Access the current URL pathname:
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
"use client";
|
|
40
|
+
import { usePathname } from "@rangojs/router/client";
|
|
41
|
+
|
|
42
|
+
function CurrentPage() {
|
|
43
|
+
const pathname = usePathname();
|
|
44
|
+
// "/product/123" (no search params)
|
|
45
|
+
|
|
46
|
+
return <span>Current path: {pathname}</span>;
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Returns the pathname string without search params or hash. Updates on navigation commit.
|
|
51
|
+
|
|
52
|
+
### useSearchParams()
|
|
53
|
+
|
|
54
|
+
Access the current URL search params:
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
"use client";
|
|
58
|
+
import { useSearchParams } from "@rangojs/router/client";
|
|
59
|
+
|
|
60
|
+
function SearchResults() {
|
|
61
|
+
const searchParams = useSearchParams();
|
|
62
|
+
const query = searchParams.get("q"); // "react"
|
|
63
|
+
const page = searchParams.get("page"); // "2"
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div>
|
|
67
|
+
Searching for: {query}, page {page}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Returns a `ReadonlyURLSearchParams` (URLSearchParams without mutation methods). During SSR, returns empty params and syncs from the browser URL on mount.
|
|
74
|
+
|
|
75
|
+
### useHref()
|
|
76
|
+
|
|
77
|
+
Mount-aware href for client components inside `include()` scopes:
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
"use client";
|
|
81
|
+
import { useHref, href, Link } from "@rangojs/router/client";
|
|
82
|
+
|
|
83
|
+
// Inside include("/shop", shopPatterns)
|
|
84
|
+
function ShopNav() {
|
|
85
|
+
const href = useHref();
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<>
|
|
89
|
+
{/* Local paths - auto-prefixed with /shop */}
|
|
90
|
+
<Link to={href("/cart")}>Cart</Link>
|
|
91
|
+
<Link to={href("/product/widget")}>Widget</Link>
|
|
92
|
+
</>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Use `useHref()` for local navigation. Use the bare `href()` function for absolute paths.
|
|
98
|
+
|
|
99
|
+
### useMount()
|
|
100
|
+
|
|
101
|
+
Returns the current `include()` mount path:
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
"use client";
|
|
105
|
+
import { useMount } from "@rangojs/router/client";
|
|
106
|
+
|
|
107
|
+
function MountInfo() {
|
|
108
|
+
const mount = useMount(); // "/shop" inside include("/shop", ...)
|
|
109
|
+
return <span>Mounted at: {mount}</span>;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### useReverse(routes)
|
|
114
|
+
|
|
115
|
+
Mount-aware local reverse for client components. Import the generated `routes` map from a `urls()` module's `.gen.ts` and call `reverse("name", params?)` — the leading dot is optional. Auto-fills params from `useParams()`; explicit params override.
|
|
116
|
+
|
|
117
|
+
> Per-module `*.gen.ts` files are **CLI opt-in and not Vite-watched** — run `rango generate <urls-file>` (or wire it into `predev`) and re-run it whenever the module's routes change. See `/links` for the full generated-file setup and exposure-boundary rules.
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
"use client";
|
|
121
|
+
import { Link, useReverse } from "@rangojs/router/client";
|
|
122
|
+
import { routes as blogRoutes } from "../urls/blog.gen.js";
|
|
123
|
+
|
|
124
|
+
function BlogNav() {
|
|
125
|
+
const reverse = useReverse(blogRoutes);
|
|
126
|
+
return (
|
|
127
|
+
<nav>
|
|
128
|
+
<Link to={reverse("index")}>Blog</Link>
|
|
129
|
+
<Link to={reverse("post", { postId: "hello" })}>Post</Link>
|
|
130
|
+
</nav>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
See `/links` for the full URL generation guide. `ctx.reverse()` is server-only; on the client, prefer `useReverse(routes)` for in-module names and pass URLs as props for cross-module ones.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: host-router
|
|
3
|
-
description: Multi-app host routing with domain/subdomain patterns
|
|
3
|
+
description: Multi-app host routing with domain/subdomain patterns. Use when running multiple apps behind one domain or across subdomains, or routing requests to different apps based on hostname.
|
|
4
4
|
argument-hint:
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -176,17 +176,17 @@ router.fallback().map((request) => {
|
|
|
176
176
|
});
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
-
For unmatched hosts without `hostOverride`, catch `NoRouteMatchError` in your worker fetch
|
|
179
|
+
For unmatched hosts without `hostOverride`, catch `NoRouteMatchError` in your worker fetch. Use the `isNoRouteMatchError()` guard rather than a bare `instanceof`: a workspace with a duplicated `@rangojs/router` copy can throw the error with a different class identity, and `instanceof` would then turn the 404 into an opaque 500.
|
|
180
180
|
|
|
181
181
|
```typescript
|
|
182
|
-
import {
|
|
182
|
+
import { isNoRouteMatchError } from "@rangojs/router/host";
|
|
183
183
|
|
|
184
184
|
export default {
|
|
185
185
|
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
|
186
186
|
try {
|
|
187
187
|
return await router.match(request, { env, ctx });
|
|
188
188
|
} catch (err) {
|
|
189
|
-
if (err
|
|
189
|
+
if (isNoRouteMatchError(err)) {
|
|
190
190
|
return new Response("Not Found", { status: 404 });
|
|
191
191
|
}
|
|
192
192
|
throw err;
|
package/skills/i18n/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: i18n
|
|
3
|
-
description: Locale-aware routing with `include("/:locale?", ...)`, locale resolution chains, and react-intl integration
|
|
3
|
+
description: Locale-aware routing with `include("/:locale?", ...)`, locale resolution chains, and react-intl integration. Use when building a multi-language app, routes need a locale segment, or wiring up react-intl translations.
|
|
4
4
|
argument-hint: "[topic]"
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: intercept
|
|
3
|
-
description: Define intercept routes for modals, slide-overs, and soft navigation patterns in @rangojs/router
|
|
3
|
+
description: Define intercept routes for modals, slide-overs, and soft navigation patterns in @rangojs/router. Use when opening a route as a modal/overlay on top of the current page while keeping the URL shareable, or asking "how do I show this page in a modal".
|
|
4
4
|
argument-hint: [@slot-name] [route-to-intercept]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -8,6 +8,13 @@ argument-hint: [@slot-name] [route-to-intercept]
|
|
|
8
8
|
|
|
9
9
|
Intercept routes render a different component during soft navigation (client-side) while preserving the background route. Hard navigation (direct URL) shows the full page.
|
|
10
10
|
|
|
11
|
+
## Not this skill if…
|
|
12
|
+
|
|
13
|
+
- You want a slot that ALWAYS renders alongside the page (sidebar, multi-column
|
|
14
|
+
layout) — that is a permanent `parallel()` slot: see `/parallel`.
|
|
15
|
+
- You want the same component regardless of soft vs hard navigation —
|
|
16
|
+
intercepts only swap on soft navigation; see `/parallel`.
|
|
17
|
+
|
|
11
18
|
## Basic Intercept
|
|
12
19
|
|
|
13
20
|
```typescript
|
|
@@ -142,18 +149,41 @@ layout(ProductLayout, () => [
|
|
|
142
149
|
]);
|
|
143
150
|
```
|
|
144
151
|
|
|
145
|
-
## Conditional Intercept with when
|
|
152
|
+
## Conditional Intercept with the `when` config
|
|
153
|
+
|
|
154
|
+
Only intercept based on navigation context. `when` is the 4th argument
|
|
155
|
+
(an `InterceptConfig` object); the other use-items go in the 5th-argument
|
|
156
|
+
callback.
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
intercept(
|
|
160
|
+
"@modal",
|
|
161
|
+
"product",
|
|
162
|
+
<ProductModal />,
|
|
163
|
+
// Only intercept when coming from a different section
|
|
164
|
+
{ when: ({ from }) => !from.pathname.startsWith("/shop/product/") },
|
|
165
|
+
() => [
|
|
166
|
+
loader(ProductLoader),
|
|
167
|
+
]
|
|
168
|
+
)
|
|
169
|
+
```
|
|
146
170
|
|
|
147
|
-
|
|
171
|
+
`when` is a match-time selector receiving `{ from, to, params, segments, ... }`.
|
|
172
|
+
Pass an array of predicates for AND logic (all must return true). Omit `when`
|
|
173
|
+
entirely and the intercept always activates.
|
|
148
174
|
|
|
149
175
|
```typescript
|
|
150
176
|
intercept(
|
|
151
177
|
"@modal",
|
|
152
178
|
"product",
|
|
153
179
|
<ProductModal />,
|
|
180
|
+
{
|
|
181
|
+
when: [
|
|
182
|
+
({ from }) => from.pathname.startsWith("/shop"),
|
|
183
|
+
({ params }) => params.slug !== "featured",
|
|
184
|
+
],
|
|
185
|
+
},
|
|
154
186
|
() => [
|
|
155
|
-
// Only intercept when coming from a different section
|
|
156
|
-
when(({ from }) => !from.pathname.startsWith("/shop/product/")),
|
|
157
187
|
loader(ProductLoader),
|
|
158
188
|
]
|
|
159
189
|
)
|
|
@@ -242,10 +272,13 @@ layout(ShopLayout, () => [
|
|
|
242
272
|
]),
|
|
243
273
|
|
|
244
274
|
// This intercept is also pre-rendered at build time
|
|
245
|
-
intercept(
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
275
|
+
intercept(
|
|
276
|
+
"@modal",
|
|
277
|
+
".detail",
|
|
278
|
+
<ProductModal />,
|
|
279
|
+
{ when: ({ from }) => from.pathname.startsWith("/shop") },
|
|
280
|
+
() => [loader(ProductLoader)],
|
|
281
|
+
),
|
|
249
282
|
])
|
|
250
283
|
```
|
|
251
284
|
|
|
@@ -253,8 +286,8 @@ Build-time behavior:
|
|
|
253
286
|
|
|
254
287
|
- The intercept handler (`<ProductModal />`) is resolved with BuildContext
|
|
255
288
|
- Result is stored under the key `"detail/paramHash/i"` (intercept variant)
|
|
256
|
-
- `when
|
|
257
|
-
- `when
|
|
289
|
+
- `when` config conditions are skipped at build time (all intercepts pre-rendered unconditionally)
|
|
290
|
+
- `when` is still evaluated at runtime by the intercept-resolution middleware
|
|
258
291
|
|
|
259
292
|
Runtime behavior:
|
|
260
293
|
|
|
@@ -305,7 +338,6 @@ export const shopPatterns = urls(({
|
|
|
305
338
|
intercept,
|
|
306
339
|
loader,
|
|
307
340
|
loading,
|
|
308
|
-
when,
|
|
309
341
|
}) => [
|
|
310
342
|
layout(<ShopLayout />, () => [
|
|
311
343
|
parallel({
|
|
@@ -317,8 +349,8 @@ export const shopPatterns = urls(({
|
|
|
317
349
|
"@modal",
|
|
318
350
|
"product", // Route name (without prefix)
|
|
319
351
|
<ProductModalContent />,
|
|
352
|
+
{ when: ({ from }) => !from.pathname.startsWith("/shop/product/") },
|
|
320
353
|
() => [
|
|
321
|
-
when(({ from }) => !from.pathname.startsWith("/shop/product/")),
|
|
322
354
|
layout(<ModalWrapper />),
|
|
323
355
|
loading(<ProductModalSkeleton />),
|
|
324
356
|
loader(ProductLoader, () => [cache()]),
|
|
@@ -338,7 +370,7 @@ export const shopPatterns = urls(({
|
|
|
338
370
|
|
|
339
371
|
## Handler-attached `.use`
|
|
340
372
|
|
|
341
|
-
Intercept handlers can carry their own middleware, loaders, loading state, error/notFound boundaries, and even nested `layout`/`route
|
|
373
|
+
Intercept handlers can carry their own middleware, loaders, loading state, error/notFound boundaries, and even nested `layout`/`route` defaults via `.use` — useful for self-contained modal components that travel with their own data and chrome. (Conditional activation is set via the `when` config on the mount-site `intercept()` call, not inside `.use`.)
|
|
342
374
|
|
|
343
375
|
```typescript
|
|
344
376
|
const QuickViewModal: Handler = async (ctx) => {
|
package/skills/layout/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: layout
|
|
3
|
-
description: Define layout routes that wrap child routes in @rangojs/router
|
|
3
|
+
description: Define layout routes that wrap child routes in @rangojs/router. Use when sharing a persistent UI shell (nav, sidebar) across nested routes, or asking how to wrap child pages with a common layout.
|
|
4
4
|
argument-hint: [component]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -147,12 +147,21 @@ A layout as a child of `path()` wraps the route content and can read
|
|
|
147
147
|
data set by the route handler via `ctx.get()`. The handler always
|
|
148
148
|
executes before its children.
|
|
149
149
|
|
|
150
|
-
This
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
150
|
+
This is the recommended way to pass handler data downward, and it is
|
|
151
|
+
safe under partial action revalidation with zero configuration: orphan
|
|
152
|
+
layouts (and their parallels) belong to the route entry, and on an
|
|
153
|
+
action the whole entry re-runs together by default — route segment,
|
|
154
|
+
loaders, and `belongsToRoute` children all seed revalidate-true, with
|
|
155
|
+
handler-first ordering preserved. Producer and consumer cannot desync
|
|
156
|
+
unless you narrow one side with a predicate that returns a hard `false`
|
|
157
|
+
(then put the same contract on both — see "Revalidation Contracts").
|
|
158
|
+
|
|
159
|
+
Data from an **outer** handler or layout entry is the opposite case:
|
|
160
|
+
outer entries do not revalidate on actions by default (parent-chain
|
|
161
|
+
skip). If an orphan layout depends on data established above its own
|
|
162
|
+
route entry, that outer segment must share a revalidation contract, or
|
|
163
|
+
the orphan must guard/reload the data independently. See `/rango` →
|
|
164
|
+
"Passing data down the tree" for the full safest-first ladder.
|
|
156
165
|
|
|
157
166
|
```typescript
|
|
158
167
|
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
@@ -191,7 +200,10 @@ orphan layouts to read them.
|
|
|
191
200
|
|
|
192
201
|
## Layout Revalidation
|
|
193
202
|
|
|
194
|
-
|
|
203
|
+
Standalone `layout()` entries don't revalidate by default — on an action,
|
|
204
|
+
parent-chain segments are skipped unless a `revalidate()` opts them in.
|
|
205
|
+
(Orphan layouts inside a `path()` are the opposite: they ride along with
|
|
206
|
+
the route entry by default.) Control with `revalidate()`:
|
|
195
207
|
|
|
196
208
|
```typescript
|
|
197
209
|
layout(<ShopLayout />, () => [
|
|
@@ -218,8 +230,13 @@ their `ctx.set()` state.
|
|
|
218
230
|
|
|
219
231
|
### Revalidation Contracts
|
|
220
232
|
|
|
221
|
-
|
|
222
|
-
|
|
233
|
+
Contracts are the tool for cross-entry sharing — the bottom rung of the
|
|
234
|
+
data-passing ladder (`/rango` → "Passing data down the tree"). Before
|
|
235
|
+
writing one, check whether the producer can move down a rung: into the
|
|
236
|
+
consumer's own entry as an orphan layout, into middleware, or into a
|
|
237
|
+
loader. When the data genuinely must flow from an outer entry, define
|
|
238
|
+
named revalidation functions and reuse them on both producer and
|
|
239
|
+
consumer segments:
|
|
223
240
|
|
|
224
241
|
```typescript
|
|
225
242
|
// revalidation-contracts.ts
|
package/skills/links/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: links
|
|
3
|
-
description: URL generation with ctx.reverse (server default), href (client), useHref (mounted), useMount, useReverse, and scopedReverse
|
|
3
|
+
description: URL generation with ctx.reverse (server default), href (client), useHref (mounted), useMount, useReverse, and scopedReverse. Use when generating a link to a route by name instead of hardcoding a path, or a link breaks after routes move or get mounted elsewhere.
|
|
4
4
|
argument-hint: [ctx.reverse|href|useHref|useMount|useReverse|scopedReverse]
|
|
5
5
|
---
|
|
6
6
|
|
package/skills/loader/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: loader
|
|
3
|
-
description: Define data loaders for fetching data in routes with createLoader
|
|
3
|
+
description: Define data loaders for fetching data in routes with createLoader. Use when pages need per-request data that stays fresh, data should stream while the page renders, or client components need reactive server data.
|
|
4
4
|
argument-hint: [loader]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -11,6 +11,13 @@ Loaders fetch data on the server and stream it to the client. For mutations
|
|
|
11
11
|
`/server-actions`. Loaders re-resolve after an action runs, so the typical
|
|
12
12
|
flow is _action mutates → loader re-reads → UI updates_.
|
|
13
13
|
|
|
14
|
+
## Not this skill if…
|
|
15
|
+
|
|
16
|
+
- You want to mutate state — mutations are `"use server"` actions: see
|
|
17
|
+
`/server-actions`. Loaders read per-request live data.
|
|
18
|
+
- You want to cache a function's return value — loaders are fresh every request
|
|
19
|
+
by default; caching one function is `"use cache"`: see `/use-cache`.
|
|
20
|
+
|
|
14
21
|
## Creating a Loader
|
|
15
22
|
|
|
16
23
|
```typescript
|
|
@@ -140,6 +147,11 @@ same memoized result — loaders never run twice per request.
|
|
|
140
147
|
- The handler output depends on the loader data. If the route is inside
|
|
141
148
|
`cache()`, the handler is cached with the loader result baked in —
|
|
142
149
|
defeating the live data guarantee.
|
|
150
|
+
- The same holds under a PPR shell capture (`/ppr`): handler consumption is
|
|
151
|
+
the BAKED lane — the loader executes at capture (identity reads permitted)
|
|
152
|
+
and the rendered value is a capture-time copy; `useLoader` client-side is
|
|
153
|
+
the live lane. One rule across `cache()`, `"use cache"`, and PPR: the
|
|
154
|
+
consumption-lane rule (`/rango` → Invariants).
|
|
143
155
|
- Non-cacheable variable reads (`createVar({ cache: false })`) inside the
|
|
144
156
|
handler still throw, even if the data came from a loader.
|
|
145
157
|
- Prefer DSL `loader()` + client `useLoader()` for data that depends on
|
|
@@ -671,6 +683,16 @@ export const SearchLoader = createLoader(async (ctx) => {
|
|
|
671
683
|
}, true); // true = fetchable
|
|
672
684
|
```
|
|
673
685
|
|
|
686
|
+
> **No registration needed — and no worker-entry import.** A fetchable loader
|
|
687
|
+
> does not have to be registered with `loader()` in the route DSL, and it does
|
|
688
|
+
> not have to be imported by any server module. Importing it into the client
|
|
689
|
+
> component that calls `useFetchLoader()` / `load()` is enough. Rango discovers
|
|
690
|
+
> every `createLoader(fn, true)` at build time and registers it for the
|
|
691
|
+
> `_rsc_loader` endpoint, so a loader reachable only through a client component
|
|
692
|
+
> still resolves in production — on both the generated entry and a hand-written
|
|
693
|
+
> worker entry (e.g. a Cloudflare `worker.rsc.tsx`). You do **not** need to
|
|
694
|
+
> force-import the loader in your worker entry to make it resolve.
|
|
695
|
+
|
|
674
696
|
### Fetchable Loader with Middleware
|
|
675
697
|
|
|
676
698
|
Pass an options object instead of `true` to attach per-loader middleware.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: middleware
|
|
3
|
-
description: Define middleware for authentication, logging, and request processing in @rangojs/router
|
|
3
|
+
description: Define middleware for authentication, logging, and request processing in @rangojs/router. Use when gating routes behind auth checks, logging requests, or running shared logic before a handler runs.
|
|
4
4
|
argument-hint: [middleware-name]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -60,8 +60,12 @@ data itself.
|
|
|
60
60
|
### Revalidation Contracts with Middleware-Backed Trees
|
|
61
61
|
|
|
62
62
|
Middleware can establish request-level context (`ctx.set`) for segments that
|
|
63
|
-
execute in the current render pass.
|
|
64
|
-
|
|
63
|
+
execute in the current render pass. Because route middleware wraps **every**
|
|
64
|
+
render pass — normal renders, post-action revalidation, PE re-renders — its
|
|
65
|
+
variables are never stale: middleware is the safest `ctx.set` rung on the
|
|
66
|
+
data-passing ladder (`/rango` → "Passing data down the tree"). But it does
|
|
67
|
+
not change partial revalidation boundaries between handler/layout/parallel
|
|
68
|
+
segments.
|
|
65
69
|
|
|
66
70
|
For shared segment data, use named revalidation contracts on both the producer
|
|
67
71
|
and consumer segments, even when middleware is present in the chain.
|