@rangojs/router 0.16.0 → 0.18.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 +255 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/history-state.d.ts +16 -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-key-utils.d.ts +17 -2
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +132 -19
- package/dist/types/cache/cache-tag.d.ts +92 -7
- package/dist/types/cache/cf/cf-base64.d.ts +6 -0
- package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
- package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
- package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
- package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +54 -13
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +110 -21
- package/dist/types/cache/tag-invalidation.d.ts +30 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +220 -44
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
- 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/prerender/build-shell-capture.d.ts +11 -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/instrument.d.ts +8 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +9 -3
- 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 +66 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
- package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
- 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/full-payload.d.ts +14 -0
- package/dist/types/rsc/handler.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-build-manifest.d.ts +8 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
- package/dist/types/rsc/shell-capture.d.ts +151 -19
- package/dist/types/rsc/shell-serve.d.ts +140 -17
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +17 -0
- package/dist/types/server/context.d.ts +139 -15
- package/dist/types/server/cookie-store.d.ts +21 -0
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +182 -53
- package/dist/types/ssr/index.d.ts +24 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- 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/run-loader.d.ts +18 -3
- package/dist/types/testing/serve-shell-request.d.ts +132 -0
- package/dist/types/testing/shell-status.d.ts +23 -10
- 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 +28 -1
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/pattern-types.d.ts +22 -18
- 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/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1420 -1348
- package/package.json +2 -2
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +75 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +306 -124
- package/skills/caching/SKILL.md +530 -98
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +65 -38
- package/skills/cloudflare/SKILL.md +87 -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 +75 -57
- 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 +52 -31
- package/skills/deployment-caching/SKILL.md +11 -3
- package/skills/document-cache/SKILL.md +98 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +55 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +23 -3
- package/skills/hooks/handle-and-actions.md +34 -17
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +39 -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 +386 -128
- package/skills/middleware/SKILL.md +129 -26
- package/skills/migrate-nextjs/SKILL.md +156 -70
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +51 -39
- package/skills/migrate-react-router/component-migration.md +28 -7
- package/skills/migrate-react-router/data-and-actions.md +21 -6
- package/skills/migrate-react-router/route-mapping.md +19 -10
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +92 -14
- package/skills/parallel/SKILL.md +88 -47
- package/skills/ppr/SKILL.md +866 -379
- package/skills/prerender/SKILL.md +210 -108
- package/skills/rango/SKILL.md +115 -65
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +170 -60
- package/skills/router-setup/SKILL.md +420 -240
- package/skills/scripts/SKILL.md +39 -21
- package/skills/server-actions/SKILL.md +231 -59
- package/skills/shell-manifest/SKILL.md +28 -14
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +73 -33
- package/skills/testing/cache-prerender.md +60 -33
- 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 +26 -6
- 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/serve-shell-request.md +188 -0
- 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 +155 -30
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +137 -50
- 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 +224 -83
- package/skills/vercel/SKILL.md +35 -13
- package/skills/view-transitions/SKILL.md +42 -21
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/history-state.ts +23 -0
- 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 +6 -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 +23 -8
- package/src/cache/cache-policy.ts +22 -0
- package/src/cache/cache-runtime.ts +264 -68
- package/src/cache/cache-scope.ts +419 -43
- package/src/cache/cache-tag.ts +375 -11
- package/src/cache/cf/cf-base64.ts +36 -4
- package/src/cache/cf/cf-cache-constants.ts +12 -0
- package/src/cache/cf/cf-cache-store.ts +990 -348
- package/src/cache/cf/cf-cache-types.ts +32 -4
- package/src/cache/cf/cf-shell-frame.ts +258 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +133 -28
- package/src/cache/index.ts +2 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +217 -27
- package/src/cache/tag-invalidation.ts +108 -22
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +225 -44
- package/src/cache/vercel/vercel-cache-store.ts +545 -61
- 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 +73 -22
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +121 -33
- 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 +71 -28
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +141 -43
- package/src/router/loader-resolution.ts +115 -39
- package/src/router/match-api.ts +40 -26
- package/src/router/match-handlers.ts +5 -0
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +110 -10
- package/src/router/match-middleware/cache-store.ts +48 -101
- package/src/router/middleware-types.ts +9 -6
- package/src/router/middleware.ts +75 -13
- package/src/router/prerender-match.ts +59 -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 +62 -21
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +407 -76
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +208 -56
- package/src/router/segment-resolution/mask-nested.ts +138 -44
- package/src/router/segment-resolution/revalidation.ts +61 -16
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/full-payload.ts +25 -1
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-cache-serve.ts +9 -6
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +608 -225
- package/src/rsc/shell-build-manifest.ts +27 -9
- package/src/rsc/shell-capture-constants.ts +73 -6
- package/src/rsc/shell-capture.ts +1068 -296
- package/src/rsc/shell-serve.ts +316 -34
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +18 -0
- package/src/server/context.ts +300 -32
- package/src/server/cookie-store.ts +87 -61
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +297 -67
- package/src/ssr/index.tsx +136 -44
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +14 -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 +68 -6
- package/src/testing/serve-shell-request.ts +378 -0
- package/src/testing/shell-status.ts +34 -12
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +28 -1
- package/src/types/loader-types.ts +8 -6
- package/src/urls/pattern-types.ts +22 -18
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +12 -3
- package/src/vite/discovery/shell-prerender-phase.ts +9 -0
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
- package/src/vite/router-discovery.ts +14 -4
package/skills/i18n/SKILL.md
CHANGED
|
@@ -32,6 +32,12 @@ import { menuRoutes } from "./menu";
|
|
|
32
32
|
export const urlpatterns = urls(({ include }) => [
|
|
33
33
|
include("/:locale?", menuRoutes, { name: "menu" }),
|
|
34
34
|
]);
|
|
35
|
+
|
|
36
|
+
// menu.tsx
|
|
37
|
+
export const menuRoutes = urls(({ path }) => [
|
|
38
|
+
path("/", MenuIndex, { name: "index" }),
|
|
39
|
+
path("/c/:slug", MenuCategory, { name: "category" }),
|
|
40
|
+
]);
|
|
35
41
|
```
|
|
36
42
|
|
|
37
43
|
URLs that match:
|
|
@@ -59,7 +65,8 @@ URLs that match:
|
|
|
59
65
|
Absent optionals are `undefined` (not `""`), so `??` coalesces correctly:
|
|
60
66
|
|
|
61
67
|
```typescript
|
|
62
|
-
import { Handler } from "@rangojs/router";
|
|
68
|
+
import type { Handler } from "@rangojs/router";
|
|
69
|
+
import { resolveLocale } from "../lib/locale";
|
|
63
70
|
|
|
64
71
|
export const MenuIndex: Handler<"menu.index"> = (ctx) => {
|
|
65
72
|
// ctx.params.locale is `string | undefined`
|
|
@@ -87,9 +94,9 @@ import { cookies, headers } from "@rangojs/router";
|
|
|
87
94
|
|
|
88
95
|
export const SUPPORTED_LOCALES = ["en", "gb", "fr"] as const;
|
|
89
96
|
export type Locale = (typeof SUPPORTED_LOCALES)[number];
|
|
90
|
-
const DEFAULT_LOCALE: Locale = "en";
|
|
97
|
+
export const DEFAULT_LOCALE: Locale = "en";
|
|
91
98
|
|
|
92
|
-
const isSupported = (v: string): v is Locale =>
|
|
99
|
+
export const isSupported = (v: string): v is Locale =>
|
|
93
100
|
(SUPPORTED_LOCALES as readonly string[]).includes(v);
|
|
94
101
|
|
|
95
102
|
export function resolveLocale(ctx: {
|
|
@@ -104,24 +111,35 @@ export function resolveLocale(ctx: {
|
|
|
104
111
|
const accept = headers().get("accept-language") ?? "";
|
|
105
112
|
for (const tag of accept.split(",")) {
|
|
106
113
|
const code = tag.split(";")[0].trim().split("-")[0];
|
|
107
|
-
if (isSupported(code)) return code
|
|
114
|
+
if (isSupported(code)) return code;
|
|
108
115
|
}
|
|
109
116
|
return DEFAULT_LOCALE;
|
|
110
117
|
}
|
|
111
118
|
```
|
|
112
119
|
|
|
120
|
+
`cookies()` and `headers()` read the current request, so the helper works in
|
|
121
|
+
handlers, layouts, loaders, and middleware without passing the request in.
|
|
122
|
+
|
|
113
123
|
If you want to redirect to the canonical URL when the resolved locale
|
|
114
124
|
doesn't match the URL (e.g., user has `gb` cookie but visits `/`), do
|
|
115
|
-
that in
|
|
125
|
+
that in global middleware (`router.use()`). Global middleware runs before
|
|
126
|
+
route matching, and its `ctx.params` come from its own pattern — not from
|
|
127
|
+
`include("/:locale?")` — so read the locale segment from the pathname:
|
|
116
128
|
|
|
117
129
|
```typescript
|
|
130
|
+
// router.tsx
|
|
118
131
|
import { redirect } from "@rangojs/router";
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
132
|
+
import { DEFAULT_LOCALE, isSupported, resolveLocale } from "./lib/locale";
|
|
133
|
+
|
|
134
|
+
router.use(async (ctx, next) => {
|
|
135
|
+
if (ctx.request.method !== "GET") return next(); // leave actions alone
|
|
136
|
+
const first = ctx.url.pathname.split("/")[1] ?? "";
|
|
137
|
+
if (!isSupported(first)) {
|
|
138
|
+
const resolved = resolveLocale({ params: {} }); // cookie → Accept-Language → default
|
|
139
|
+
if (resolved !== DEFAULT_LOCALE) {
|
|
140
|
+
const rest = ctx.url.pathname === "/" ? "" : ctx.url.pathname;
|
|
141
|
+
return redirect(`/${resolved}${rest}${ctx.url.search}`);
|
|
142
|
+
}
|
|
125
143
|
}
|
|
126
144
|
await next();
|
|
127
145
|
});
|
|
@@ -138,9 +156,16 @@ ctx.reverse("menu.index", { locale: "" }); // → "/"
|
|
|
138
156
|
ctx.reverse("menu.index", { locale: undefined }); // → "/"
|
|
139
157
|
ctx.reverse("menu.index", { locale: "en" }); // → "/en"
|
|
140
158
|
ctx.reverse("menu.category", { locale: "en", slug: "breads" }); // → "/en/c/breads"
|
|
141
|
-
ctx.reverse("menu.category", { slug: "breads" }); // → "/c/breads"
|
|
159
|
+
ctx.reverse("menu.category", { slug: "breads" }); // → "/c/breads" on an unprefixed request
|
|
142
160
|
```
|
|
143
161
|
|
|
162
|
+
`ctx.reverse()` auto-fills missing params from the current request, so the
|
|
163
|
+
last call returns `/en/c/breads` when the current URL is `/en/...`. That is
|
|
164
|
+
what you want for in-locale links; to leave the locale, pass it explicitly
|
|
165
|
+
(`{ locale: "fr" }`, or `undefined`/`""` for the bare default). Middleware and
|
|
166
|
+
response routes are the exception: their `ctx.reverse` fills nothing from the
|
|
167
|
+
request, so pass `locale` there explicitly.
|
|
168
|
+
|
|
144
169
|
If the active locale is the app default and your URL strategy hides it
|
|
145
170
|
(`"en"` → `/`, others → `/<locale>`), normalize before calling reverse:
|
|
146
171
|
|
|
@@ -177,28 +202,27 @@ export async function loadMessages(locale: Locale) {
|
|
|
177
202
|
|
|
178
203
|
### Server layout: hand off to the client provider
|
|
179
204
|
|
|
205
|
+
A layout handler receives the handler context (not props) and renders its
|
|
206
|
+
children with `<Outlet />`. It sees the matched route's params, including
|
|
207
|
+
`locale` from the include below it:
|
|
208
|
+
|
|
180
209
|
```tsx
|
|
181
|
-
// layouts/intl-layout.tsx (server
|
|
182
|
-
import type {
|
|
210
|
+
// layouts/intl-layout.tsx (server)
|
|
211
|
+
import type { Handler } from "@rangojs/router";
|
|
212
|
+
import { Outlet } from "@rangojs/router/client";
|
|
183
213
|
import { resolveLocale } from "../lib/locale";
|
|
184
214
|
import { loadMessages } from "../lib/messages";
|
|
185
215
|
import { IntlClientProvider } from "../components/intl-client-provider";
|
|
186
216
|
|
|
187
|
-
export async
|
|
188
|
-
ctx,
|
|
189
|
-
children,
|
|
190
|
-
}: {
|
|
191
|
-
ctx: any;
|
|
192
|
-
children: ReactNode;
|
|
193
|
-
}) {
|
|
217
|
+
export const IntlLayout: Handler = async (ctx) => {
|
|
194
218
|
const locale = resolveLocale(ctx);
|
|
195
219
|
const messages = await loadMessages(locale);
|
|
196
220
|
return (
|
|
197
221
|
<IntlClientProvider locale={locale} messages={messages}>
|
|
198
|
-
|
|
222
|
+
<Outlet />
|
|
199
223
|
</IntlClientProvider>
|
|
200
224
|
);
|
|
201
|
-
}
|
|
225
|
+
};
|
|
202
226
|
```
|
|
203
227
|
|
|
204
228
|
### Client provider
|
|
@@ -252,20 +276,22 @@ export const urlpatterns = urls(({ layout, include }) => [
|
|
|
252
276
|
```
|
|
253
277
|
|
|
254
278
|
`<FormattedMessage>`, `useIntl()`, etc. work in any client component
|
|
255
|
-
under the layout. Server components can use
|
|
256
|
-
|
|
279
|
+
under the layout. Server components and handlers can't use React context
|
|
280
|
+
providers from the client; call `createIntl({ locale, messages })` (from
|
|
281
|
+
`@formatjs/intl` or `react-intl`) directly with the same `messages` map for
|
|
282
|
+
server-rendered text.
|
|
257
283
|
|
|
258
284
|
## Common Pitfalls
|
|
259
285
|
|
|
260
|
-
| Pitfall | Fix
|
|
261
|
-
| ------------------------------------------------------------- |
|
|
262
|
-
| `ctx.params.locale === ""` returns `false` | Absent optionals are `undefined`, not `""`. Use `=== undefined` or `??`.
|
|
263
|
-
|
|
|
264
|
-
|
|
|
265
|
-
|
|
|
266
|
-
|
|
|
267
|
-
|
|
|
268
|
-
|
|
|
286
|
+
| Pitfall | Fix |
|
|
287
|
+
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
288
|
+
| `ctx.params.locale === ""` returns `false` | Absent optionals are `undefined`, not `""`. Use `=== undefined` or `??`. |
|
|
289
|
+
| Unknown locale (e.g. `/de`) matches as `locale: "de"` | Add a constraint: `:locale(en\|gb\|fr)?`. Unknown values now 404. |
|
|
290
|
+
| Link meant for the default locale keeps `/en` | `ctx.reverse()` auto-fills the current `locale`. Pass `{ locale: undefined }` (or `""`) to drop it. |
|
|
291
|
+
| Reverse produces `//c/breads` for absent locale | `reverse()` collapses `undefined`/`""` segments — should not happen. File a bug. |
|
|
292
|
+
| Locale switcher loses search params | Append `ctx.url.search` to the reversed URL, or pass `Object.fromEntries(ctx.searchParams)` as the third argument. |
|
|
293
|
+
| Global middleware's `ctx.params.locale` is always `undefined` | Global middleware params come from its own pattern. Parse the first path segment (see "Locale Resolution"). |
|
|
294
|
+
| Route middleware doesn't see actions | Route middleware doesn't wrap action execution. Use global `router.use()` for action-time locale logic. |
|
|
269
295
|
|
|
270
296
|
## Cross-references
|
|
271
297
|
|
|
@@ -8,6 +8,18 @@ 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
|
+
`intercept(slot, routeName, handler, config?, use?)`:
|
|
12
|
+
|
|
13
|
+
- `slot` — the `@name` slot the intercept renders into; the layout places it
|
|
14
|
+
with `<ParallelOutlet name="@modal" />`.
|
|
15
|
+
- `routeName` — the target route. A dot prefix (`".product"`) resolves inside
|
|
16
|
+
the current `include()` scope; a bare name (`"product"`) is the global route
|
|
17
|
+
name. Declare the intercept in a layout that wraps the target route — the
|
|
18
|
+
router finds intercepts by walking up the target route's own layout chain.
|
|
19
|
+
- `handler` — the component or handler rendered in the slot.
|
|
20
|
+
- `config` — optional `{ when }` (see "Conditional Intercept"); pass the `use`
|
|
21
|
+
callback as the 4th argument when there is no config.
|
|
22
|
+
|
|
11
23
|
## Not this skill if…
|
|
12
24
|
|
|
13
25
|
- You want a slot that ALWAYS renders alongside the page (sidebar, multi-column
|
|
@@ -60,7 +72,8 @@ export const urlpatterns = urls(({ path, layout, intercept, loader, loading }) =
|
|
|
60
72
|
|
|
61
73
|
## Intercept with Layout
|
|
62
74
|
|
|
63
|
-
Wrap intercept content in a modal layout
|
|
75
|
+
Wrap intercept content in a modal layout. The layout renders the intercept
|
|
76
|
+
content through `<Outlet />` (not a `children` prop):
|
|
64
77
|
|
|
65
78
|
```typescript
|
|
66
79
|
intercept(
|
|
@@ -68,21 +81,29 @@ intercept(
|
|
|
68
81
|
"product",
|
|
69
82
|
<ProductModalContent />,
|
|
70
83
|
() => [
|
|
71
|
-
layout(<ModalWrapper />), // Wraps the modal content
|
|
84
|
+
layout(<ModalWrapper />), // Wraps the modal content via <Outlet />
|
|
72
85
|
loader(ProductLoader),
|
|
73
86
|
loading(<ProductModalSkeleton />),
|
|
74
87
|
]
|
|
75
88
|
)
|
|
76
89
|
```
|
|
77
90
|
|
|
91
|
+
Only the first `layout()` in an intercept's `use()` is applied. It is modal
|
|
92
|
+
chrome only: a `layout(Chrome, () => [...])` with its own `use()` items throws.
|
|
93
|
+
Attach loaders and the rest to the intercept itself.
|
|
94
|
+
|
|
78
95
|
## Intercept Middleware
|
|
79
96
|
|
|
80
97
|
Intercepts support their own middleware chain via the use callback. The full chain for an intercept request is:
|
|
81
98
|
|
|
82
99
|
```
|
|
83
|
-
global mw (router.use) -> route mw (urls middleware()) -> intercept mw -> intercept
|
|
100
|
+
global mw (router.use) -> route mw (urls middleware()) -> intercept mw -> intercept loaders + handler
|
|
84
101
|
```
|
|
85
102
|
|
|
103
|
+
The intercept's loaders are started before its handler is called, so they run
|
|
104
|
+
in parallel with it. If intercept middleware returns a `Response` (e.g. a
|
|
105
|
+
redirect), the intercept stops there.
|
|
106
|
+
|
|
86
107
|
```typescript
|
|
87
108
|
intercept(
|
|
88
109
|
"@modal",
|
|
@@ -108,22 +129,55 @@ If an intercept depends on data established by an outer layout/handler,
|
|
|
108
129
|
revalidate that outer segment too or reload/guard the data inside the
|
|
109
130
|
intercept.
|
|
110
131
|
|
|
132
|
+
### Revalidating an Intercept
|
|
133
|
+
|
|
134
|
+
An intercept has no segment-level `revalidate()`. Its handler and layout
|
|
135
|
+
re-render every time the intercept resolves live — each intercepted
|
|
136
|
+
navigation, and each action submitted while the modal is open. When the
|
|
137
|
+
segment cache or a prerendered intercept variant serves the modal, the stored
|
|
138
|
+
handler output is reused and only the loaders run. Each loader decides for
|
|
139
|
+
itself through its own `revalidate()`, with the normal loader defaults.
|
|
140
|
+
|
|
141
|
+
A `revalidate()` placed directly in the intercept's `use()` (or returned from
|
|
142
|
+
its handler's `.use`) throws at definition time. Attach it to the loader:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// Throws: revalidate() is not valid inside intercept("@modal", "product") use()
|
|
146
|
+
intercept("@modal", "product", <ProductModal />, () => [
|
|
147
|
+
revalidate(() => false),
|
|
148
|
+
loader(ProductLoader),
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
// Correct: the loader owns the decision
|
|
152
|
+
intercept("@modal", "product", <ProductModal />, () => [
|
|
153
|
+
loader(ProductLoader, () => [revalidate(() => false)]),
|
|
154
|
+
]);
|
|
155
|
+
```
|
|
156
|
+
|
|
111
157
|
### Revalidation Contracts for Intercept Dependencies
|
|
112
158
|
|
|
113
|
-
|
|
114
|
-
|
|
159
|
+
When an outer layout produces `ctx.set()` data that the intercept reads, the
|
|
160
|
+
producer is the side that needs a contract: outer layouts are skipped during
|
|
161
|
+
partial action revalidation unless they opt in. The intercept side needs none
|
|
162
|
+
by default — its handler re-renders on every action while the modal is open,
|
|
163
|
+
and its loaders re-run after every action. If you narrow an intercept loader
|
|
164
|
+
with its own `revalidate()`, compose the producer's contract into it so the
|
|
165
|
+
two stay in step:
|
|
115
166
|
|
|
116
167
|
```typescript
|
|
168
|
+
import type { Revalidate } from "@rangojs/router";
|
|
117
169
|
import * as ProductActions from "./actions/product";
|
|
118
170
|
|
|
119
|
-
export const revalidateProductShell = (ctx) =>
|
|
171
|
+
export const revalidateProductShell: Revalidate = (ctx) =>
|
|
120
172
|
ctx.isAction(ProductActions) || undefined;
|
|
121
173
|
|
|
122
174
|
layout(ProductLayout, () => [
|
|
123
175
|
revalidate(revalidateProductShell), // producer reruns
|
|
124
176
|
intercept("@modal", "product", <ProductModal />, () => [
|
|
125
|
-
|
|
126
|
-
|
|
177
|
+
loader(ProductLoader, () => [
|
|
178
|
+
revalidate(revalidateProductShell), // reruns with the producer
|
|
179
|
+
revalidate(() => false), // otherwise keep the loaded data
|
|
180
|
+
]),
|
|
127
181
|
]),
|
|
128
182
|
]);
|
|
129
183
|
```
|
|
@@ -143,17 +197,16 @@ export const revalidateProduct = () => [
|
|
|
143
197
|
layout(ProductLayout, () => [
|
|
144
198
|
revalidateProduct(),
|
|
145
199
|
intercept("@modal", "product", <ProductModal />, () => [
|
|
146
|
-
revalidateProduct(),
|
|
147
|
-
loader(ProductLoader),
|
|
200
|
+
loader(ProductLoader, () => [revalidateProduct(), revalidate(() => false)]),
|
|
148
201
|
]),
|
|
149
202
|
]);
|
|
150
203
|
```
|
|
151
204
|
|
|
152
205
|
## Conditional Intercept with the `when` config
|
|
153
206
|
|
|
154
|
-
Only intercept based on navigation context. `when`
|
|
155
|
-
(
|
|
156
|
-
|
|
207
|
+
Only intercept based on navigation context. `when` goes in the config object
|
|
208
|
+
(4th argument); the use-items then move to the 5th-argument callback. The
|
|
209
|
+
config type is not exported — pass an object literal.
|
|
157
210
|
|
|
158
211
|
```typescript
|
|
159
212
|
intercept(
|
|
@@ -168,9 +221,19 @@ intercept(
|
|
|
168
221
|
)
|
|
169
222
|
```
|
|
170
223
|
|
|
171
|
-
`when` is a match-time selector
|
|
224
|
+
`when` is a synchronous match-time selector. It receives `from` / `to` (URLs),
|
|
225
|
+
`fromRouteName` / `toRouteName` (named routes only), `params` (the target's),
|
|
226
|
+
`segments` (the client's current segment path and ids), `request`, and `env`.
|
|
172
227
|
Pass an array of predicates for AND logic (all must return true). Omit `when`
|
|
173
|
-
entirely and the intercept always activates.
|
|
228
|
+
entirely and the intercept always activates. `when` is not re-evaluated during
|
|
229
|
+
action revalidation, so an open modal stays open after an action.
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// Intercept only when opened from the shop index
|
|
233
|
+
intercept("@modal", "product", <ProductModal />, {
|
|
234
|
+
when: ({ fromRouteName }) => fromRouteName === "index",
|
|
235
|
+
})
|
|
236
|
+
```
|
|
174
237
|
|
|
175
238
|
```typescript
|
|
176
239
|
intercept(
|
|
@@ -204,22 +267,84 @@ intercept(
|
|
|
204
267
|
)
|
|
205
268
|
```
|
|
206
269
|
|
|
270
|
+
## What an Intercept's `use()` Accepts
|
|
271
|
+
|
|
272
|
+
An intercept's `use()` (and its handler's `.use`) accepts:
|
|
273
|
+
|
|
274
|
+
- `loader()`, with its own `revalidate()` and `cache()` inside it
|
|
275
|
+
- `middleware()`
|
|
276
|
+
- `loading()`
|
|
277
|
+
- `layout(Chrome)`: the modal chrome, without `use()` items of its own
|
|
278
|
+
- `route`, the item type a `path()` call produces
|
|
279
|
+
- `transition()`
|
|
280
|
+
|
|
281
|
+
Everything else throws at definition time with a pointer to where it goes:
|
|
282
|
+
`revalidate()` onto the loader, `errorBoundary()` / `notFoundBoundary()` onto
|
|
283
|
+
the enclosing layout (see below), and `cache()` onto the target route.
|
|
284
|
+
|
|
285
|
+
## Caching an Intercept
|
|
286
|
+
|
|
287
|
+
To cache a modal, put `cache()` on the target route (or a layout above it).
|
|
288
|
+
An intercept navigation is cached under that scope with its own `intercept:`
|
|
289
|
+
key, so it never collides with the full-page render of the same URL. For
|
|
290
|
+
finer control, use `"use cache"` in the intercept handler, or cache the
|
|
291
|
+
intercept's loader: `loader(ProductLoader, () => [cache()])`.
|
|
292
|
+
|
|
293
|
+
## Errors in an Intercept
|
|
294
|
+
|
|
295
|
+
An intercept has no boundary of its own. When the intercept's handler or one
|
|
296
|
+
of its loaders throws, or calls `notFound()`, the modal slot renders the
|
|
297
|
+
`errorBoundary()` / `notFoundBoundary()` of the layout or path that declares the
|
|
298
|
+
intercept, or the nearest ancestor that has one. That holds when the declaring
|
|
299
|
+
layout has no routes of its own, too: the lookup continues at the layout that
|
|
300
|
+
holds it and that layout's ancestors. The intercepted route's own
|
|
301
|
+
boundaries are not consulted: the modal renders in the declaring layout's slot,
|
|
302
|
+
not in the route. On a handler error the modal layout and the background page
|
|
303
|
+
stay mounted, and the status is 500 / 404. A thrown `Response` (`redirect()`)
|
|
304
|
+
still short-circuits the navigation.
|
|
305
|
+
|
|
306
|
+
With `loading()`, an async handler streams after the response has started, so
|
|
307
|
+
its rejection is not caught on the server. Wrap the modal layout's `<Outlet />`
|
|
308
|
+
in a client error boundary.
|
|
309
|
+
|
|
310
|
+
An `errorBoundary()` or `notFoundBoundary()` placed directly in the intercept's
|
|
311
|
+
`use()` (or returned from its handler's `.use`) throws at definition time. Put
|
|
312
|
+
it on the enclosing layout:
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
// Throws: errorBoundary() is not valid inside intercept("@modal", "product") use()
|
|
316
|
+
intercept("@modal", "product", <ProductModal />, () => [
|
|
317
|
+
loader(ProductLoader),
|
|
318
|
+
errorBoundary(<ModalError />),
|
|
319
|
+
]);
|
|
320
|
+
|
|
321
|
+
// Correct: the declaring layout's boundaries handle the intercept's handler and loaders
|
|
322
|
+
layout(<ShopLayout />, () => [
|
|
323
|
+
errorBoundary(<ShopError />),
|
|
324
|
+
notFoundBoundary(<ProductNotFound />),
|
|
325
|
+
intercept("@modal", "product", <ProductModal />, () => [
|
|
326
|
+
loader(ProductLoader),
|
|
327
|
+
]),
|
|
328
|
+
]);
|
|
329
|
+
```
|
|
330
|
+
|
|
207
331
|
## Closing the Modal
|
|
208
332
|
|
|
209
333
|
Use navigation to close:
|
|
210
334
|
|
|
211
335
|
```typescript
|
|
212
336
|
"use client";
|
|
213
|
-
import { useRouter } from "@rangojs/router/client";
|
|
337
|
+
import { Outlet, useRouter } from "@rangojs/router/client";
|
|
214
338
|
|
|
215
|
-
|
|
339
|
+
// Used as the intercept's layout: layout(<ModalWrapper />)
|
|
340
|
+
export function ModalWrapper() {
|
|
216
341
|
const router = useRouter();
|
|
217
342
|
|
|
218
343
|
return (
|
|
219
344
|
<div className="modal-overlay" onClick={() => router.back()}>
|
|
220
345
|
<div className="modal" onClick={(e) => e.stopPropagation()}>
|
|
221
346
|
<button onClick={() => router.back()}>Close</button>
|
|
222
|
-
|
|
347
|
+
<Outlet />
|
|
223
348
|
</div>
|
|
224
349
|
</div>
|
|
225
350
|
);
|
|
@@ -305,10 +430,10 @@ segments.
|
|
|
305
430
|
## Complete Example
|
|
306
431
|
|
|
307
432
|
```typescript
|
|
308
|
-
// components/
|
|
433
|
+
// components/shop-layout.tsx
|
|
309
434
|
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
310
435
|
|
|
311
|
-
function ShopLayout() {
|
|
436
|
+
export function ShopLayout() {
|
|
312
437
|
return (
|
|
313
438
|
<div className="shop">
|
|
314
439
|
<ParallelOutlet name="@promoBanner" />
|
|
@@ -320,16 +445,23 @@ function ShopLayout() {
|
|
|
320
445
|
);
|
|
321
446
|
}
|
|
322
447
|
|
|
323
|
-
|
|
448
|
+
// components/modal-wrapper.tsx (see "Closing the Modal" for a closable version)
|
|
449
|
+
import { Outlet } from "@rangojs/router/client";
|
|
450
|
+
|
|
451
|
+
export function ModalWrapper() {
|
|
324
452
|
return (
|
|
325
453
|
<div className="modal-overlay">
|
|
326
|
-
<div className="modal">
|
|
454
|
+
<div className="modal">
|
|
455
|
+
<Outlet />
|
|
456
|
+
</div>
|
|
327
457
|
</div>
|
|
328
458
|
);
|
|
329
459
|
}
|
|
330
460
|
|
|
331
|
-
// urls/shop.tsx
|
|
461
|
+
// urls/shop.tsx — mounted with include("/shop", shopPatterns, { name: "shop" })
|
|
332
462
|
import { urls } from "@rangojs/router";
|
|
463
|
+
import { ShopLayout } from "../components/shop-layout";
|
|
464
|
+
import { ModalWrapper } from "../components/modal-wrapper";
|
|
333
465
|
|
|
334
466
|
export const shopPatterns = urls(({
|
|
335
467
|
path,
|
|
@@ -338,6 +470,7 @@ export const shopPatterns = urls(({
|
|
|
338
470
|
intercept,
|
|
339
471
|
loader,
|
|
340
472
|
loading,
|
|
473
|
+
cache,
|
|
341
474
|
}) => [
|
|
342
475
|
layout(<ShopLayout />, () => [
|
|
343
476
|
parallel({
|
|
@@ -347,7 +480,7 @@ export const shopPatterns = urls(({
|
|
|
347
480
|
// Intercept product detail into modal
|
|
348
481
|
intercept(
|
|
349
482
|
"@modal",
|
|
350
|
-
"product",
|
|
483
|
+
".product", // dot-local: resolves to "shop.product" in this include
|
|
351
484
|
<ProductModalContent />,
|
|
352
485
|
{ when: ({ from }) => !from.pathname.startsWith("/shop/product/") },
|
|
353
486
|
() => [
|
|
@@ -370,9 +503,11 @@ export const shopPatterns = urls(({
|
|
|
370
503
|
|
|
371
504
|
## Handler-attached `.use`
|
|
372
505
|
|
|
373
|
-
Intercept handlers can carry their own middleware, loaders, loading state,
|
|
506
|
+
Intercept handlers can carry their own middleware, loaders, loading state, 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`; boundaries go on the enclosing layout, see [Errors in an Intercept](#errors-in-an-intercept).)
|
|
374
507
|
|
|
375
508
|
```typescript
|
|
509
|
+
import { layout, loader, loading, type Handler } from "@rangojs/router";
|
|
510
|
+
|
|
376
511
|
const QuickViewModal: Handler = async (ctx) => {
|
|
377
512
|
const product = await ctx.use(ProductLoader);
|
|
378
513
|
return <QuickView product={product} />;
|
|
@@ -380,7 +515,7 @@ const QuickViewModal: Handler = async (ctx) => {
|
|
|
380
515
|
QuickViewModal.use = () => [
|
|
381
516
|
loader(ProductLoader),
|
|
382
517
|
loading(<QuickViewSkeleton />),
|
|
383
|
-
layout(<ModalChrome />),
|
|
518
|
+
layout(<ModalChrome />), // ModalChrome renders <Outlet />
|
|
384
519
|
];
|
|
385
520
|
|
|
386
521
|
intercept("@modal", "product", QuickViewModal);
|