@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c
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 +8 -0
- package/README.md +126 -16
- package/dist/bin/rango.js +319 -95
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +2724 -1053
- package/package.json +68 -14
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +64 -2
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +224 -32
- package/skills/caching/SKILL.md +279 -17
- package/skills/composability/SKILL.md +27 -3
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +11 -9
- package/skills/hooks/SKILL.md +243 -29
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +68 -19
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +190 -23
- package/skills/loader/SKILL.md +235 -9
- package/skills/middleware/SKILL.md +18 -10
- package/skills/migrate-nextjs/SKILL.md +43 -19
- package/skills/migrate-react-router/SKILL.md +8 -2
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +18 -7
- package/skills/prerender/SKILL.md +65 -60
- package/skills/rango/SKILL.md +251 -24
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +115 -48
- package/skills/route/SKILL.md +46 -5
- package/skills/router-setup/SKILL.md +30 -8
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +130 -0
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +122 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/typesafety/SKILL.md +322 -29
- package/skills/use-cache/SKILL.md +57 -14
- package/skills/view-transitions/SKILL.md +337 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +192 -150
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +94 -25
- package/src/browser/navigation-client.ts +121 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +115 -67
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +147 -128
- package/src/browser/prefetch/cache.ts +107 -56
- package/src/browser/prefetch/fetch.ts +204 -34
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +30 -7
- package/src/browser/react/NavigationProvider.tsx +283 -118
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/deferred-handle-resolution.ts +75 -0
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +17 -14
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +10 -5
- package/src/browser/react/use-params.ts +11 -11
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +25 -3
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +99 -8
- package/src/browser/rsc-router.tsx +91 -24
- package/src/browser/scroll-restoration.ts +30 -17
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +214 -55
- package/src/browser/types.ts +80 -9
- package/src/browser/validate-redirect-origin.ts +43 -16
- 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 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +117 -14
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +117 -23
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +55 -28
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +224 -41
- package/src/cache/cache-scope.ts +188 -82
- package/src/cache/cache-tag.ts +103 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +1989 -378
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +89 -21
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +10 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +44 -21
- package/src/client.tsx +36 -61
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +18 -6
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +31 -23
- package/src/handles/MetaTags.tsx +62 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +37 -8
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +51 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +96 -12
- package/src/index.ts +94 -14
- package/src/internal-debug.ts +11 -10
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +61 -6
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +34 -0
- package/src/reverse.ts +65 -40
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +304 -309
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +82 -55
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +12 -1
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +73 -25
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +27 -43
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +39 -20
- package/src/router/lazy-includes.ts +10 -47
- package/src/router/loader-resolution.ts +155 -72
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +9 -24
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +159 -285
- package/src/router/match-middleware/cache-store.ts +64 -52
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +44 -74
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -134
- package/src/router/middleware.ts +247 -166
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +85 -94
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +104 -65
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +3 -28
- package/src/router/router-interfaces.ts +83 -35
- package/src/router/router-options.ts +136 -5
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +97 -84
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +76 -39
- package/src/router/segment-resolution/revalidation.ts +272 -320
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +56 -0
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +6 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +161 -199
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +162 -64
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +110 -55
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +264 -220
- package/src/rsc/helpers.ts +100 -6
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +117 -11
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +88 -188
- package/src/rsc/rsc-rendering.ts +98 -76
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +281 -117
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +23 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-loader-promise.ts +31 -4
- package/src/segment-system.tsx +254 -143
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +163 -51
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +287 -178
- package/src/ssr/index.tsx +21 -16
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +701 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +371 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +357 -0
- package/src/testing/render-route.tsx +581 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/run-transition-when.ts +164 -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 +305 -0
- package/src/theme/ThemeProvider.tsx +20 -58
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +110 -62
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +135 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +29 -12
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +126 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +96 -68
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +44 -0
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +2 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +126 -8
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +48 -95
- package/src/vite/plugins/expose-id-utils.ts +88 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +505 -486
- package/src/vite/plugins/performance-tracks.ts +26 -25
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/version-injector.ts +40 -29
- package/src/vite/plugins/version-plugin.ts +37 -40
- package/src/vite/plugins/virtual-entries.ts +39 -25
- package/src/vite/rango.ts +109 -118
- package/src/vite/router-discovery.ts +718 -119
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +54 -39
- package/src/vite/utils/shared-utils.ts +90 -41
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -22,9 +22,9 @@ import { createHostRouter } from "@rangojs/router/host";
|
|
|
22
22
|
|
|
23
23
|
const router = createHostRouter();
|
|
24
24
|
|
|
25
|
-
router.host(["."]).
|
|
26
|
-
router.host(["admin.*"]).
|
|
27
|
-
router.host(["api.*"]).
|
|
25
|
+
router.host(["."]).lazy(() => import("./apps/main"));
|
|
26
|
+
router.host(["admin.*"]).lazy(() => import("./apps/admin"));
|
|
27
|
+
router.host(["api.*"]).lazy(() => import("./apps/api"));
|
|
28
28
|
|
|
29
29
|
export default {
|
|
30
30
|
fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
|
@@ -33,7 +33,31 @@ export default {
|
|
|
33
33
|
};
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
## Inline handlers (`.map`) vs lazy mounts (`.lazy`)
|
|
37
|
+
|
|
38
|
+
A host pattern maps to one of two things, and you pick the method by intent:
|
|
39
|
+
|
|
40
|
+
| Method | Argument | Use for |
|
|
41
|
+
| ------- | ------------------------------ | ------------------------------------------------------------ |
|
|
42
|
+
| `.map` | `(request, input) => Response` | An inline request handler that produces a response directly. |
|
|
43
|
+
| `.lazy` | `() => import("./sub-app")` | A lazily-imported handler or nested host router (a sub-app). |
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// Lazy mount: the module's default export is a handler or a HostRouter.
|
|
47
|
+
router.host(["admin.*"]).lazy(() => import("./apps/admin"));
|
|
48
|
+
|
|
49
|
+
// Inline handler: returns a Response itself (sync or async).
|
|
50
|
+
router.host(["health.*"]).map(() => new Response("ok"));
|
|
51
|
+
router
|
|
52
|
+
.host(["echo.*"])
|
|
53
|
+
.map((request) => new Response(new URL(request.url).pathname));
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Why two methods instead of one overloaded `.map()`:
|
|
57
|
+
|
|
58
|
+
- **Build-time discovery** invokes only `.lazy()` mounts (to trigger each sub-app's `createRouter()` registration). Inline `.map()` handlers are never invoked during discovery, so they can't crash it or pollute its errors.
|
|
59
|
+
- `.map(() => import("./sub-app"))` is a **type error** — a lazy import resolves to a module, not a `Response`. Use `.lazy()` for imports. (If the types are bypassed, e.g. from JS, a `.map()` handler that resolves to a module throws a clear `HostRouterError` at request time instead of returning the module.)
|
|
60
|
+
- A lazy loader may declare an ignored parameter (`.lazy((_request?) => import("./x"))`); `.lazy()` accepts it because intent is explicit, not inferred from the signature.
|
|
37
61
|
|
|
38
62
|
## Pattern Syntax
|
|
39
63
|
|
|
@@ -42,7 +66,7 @@ Each `.map()` receives either a direct handler `(request, input) => Response` or
|
|
|
42
66
|
| `.` or `*` | Any apex domain (`example.com`) |
|
|
43
67
|
| `**` | Any domain (apex + all subdomains) |
|
|
44
68
|
| `*.` | Any single-level subdomain (`www.example.com`) |
|
|
45
|
-
|
|
|
69
|
+
| `**.` | Any multi-level subdomain (`a.b.example.com`) |
|
|
46
70
|
| `example.com` | Exact domain |
|
|
47
71
|
| `*.com` | Any apex `.com` domain |
|
|
48
72
|
| `*.example.com` | Single subdomain of `example.com` |
|
|
@@ -65,8 +89,8 @@ const hosts = defineHosts({
|
|
|
65
89
|
app: [".", "www.*"],
|
|
66
90
|
});
|
|
67
91
|
|
|
68
|
-
router.host(hosts.admin).
|
|
69
|
-
router.host(hosts.app).
|
|
92
|
+
router.host(hosts.admin).lazy(() => import("./apps/admin"));
|
|
93
|
+
router.host(hosts.app).lazy(() => import("./apps/main"));
|
|
70
94
|
```
|
|
71
95
|
|
|
72
96
|
Returns a frozen object — keys are autocompleted by TypeScript.
|
|
@@ -88,7 +112,7 @@ router.use(async (request, input, next) => {
|
|
|
88
112
|
router
|
|
89
113
|
.host(["admin.*"])
|
|
90
114
|
.use(requireAuth)
|
|
91
|
-
.
|
|
115
|
+
.lazy(() => import("./apps/admin"));
|
|
92
116
|
```
|
|
93
117
|
|
|
94
118
|
Middleware signature: `(request: Request, input: RouterRequestInput, next: () => Promise<Response>) => Promise<Response>`
|
|
@@ -165,12 +189,26 @@ Logs pattern matching, route registration, and cookie override decisions to cons
|
|
|
165
189
|
## Testing
|
|
166
190
|
|
|
167
191
|
```typescript
|
|
168
|
-
import {
|
|
192
|
+
import {
|
|
193
|
+
createTestRequest,
|
|
194
|
+
testPattern,
|
|
195
|
+
matchesHost,
|
|
196
|
+
} from "@rangojs/router/host/testing";
|
|
169
197
|
|
|
170
|
-
// Test pattern matching
|
|
198
|
+
// Test pattern matching (host-only)
|
|
171
199
|
testPattern("admin.*", "admin.example.com"); // true
|
|
172
200
|
testPattern([".", "www.*"], "example.com"); // true
|
|
173
201
|
|
|
202
|
+
// Path-based patterns need the third pathname arg (defaults to "/", so a
|
|
203
|
+
// host-only pattern still works with two args):
|
|
204
|
+
testPattern("**.workers.dev/admin", "foo.workers.dev", "/admin"); // true
|
|
205
|
+
|
|
206
|
+
// Or match a pattern against a real Request (hostname + pathname from the URL):
|
|
207
|
+
matchesHost(
|
|
208
|
+
"**.workers.dev/admin",
|
|
209
|
+
new Request("https://foo.workers.dev/admin"),
|
|
210
|
+
); // true
|
|
211
|
+
|
|
174
212
|
// Create requests for integration tests
|
|
175
213
|
const request = createTestRequest({
|
|
176
214
|
host: "admin.example.com",
|
|
@@ -179,40 +217,62 @@ const request = createTestRequest({
|
|
|
179
217
|
});
|
|
180
218
|
|
|
181
219
|
// Test which route would match (without executing)
|
|
182
|
-
router.test("admin.example.com"); // { pattern, handler } | null
|
|
220
|
+
router.test("admin.example.com"); // { pattern, handler, kind } | null
|
|
183
221
|
```
|
|
184
222
|
|
|
185
223
|
## Error Types
|
|
186
224
|
|
|
187
225
|
All errors extend `HostRouterError`:
|
|
188
226
|
|
|
189
|
-
| Error | When
|
|
190
|
-
| ----------------------------- |
|
|
191
|
-
| `InvalidPatternError` | Pattern is empty, non-string, or has spaces
|
|
192
|
-
| `HostOverrideNotAllowedError` | Cookie override from disallowed host
|
|
193
|
-
| `InvalidHostnameError` | Cookie value isn't a valid hostname
|
|
194
|
-
| `HostValidationError` | Custom `validate` function threw
|
|
195
|
-
| `NoRouteMatchError` | No host pattern matched the request
|
|
196
|
-
| `InvalidHandlerError` | Handler is not a function
|
|
227
|
+
| Error | When |
|
|
228
|
+
| ----------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
229
|
+
| `InvalidPatternError` | Pattern is empty, non-string, or has spaces |
|
|
230
|
+
| `HostOverrideNotAllowedError` | Cookie override from disallowed host |
|
|
231
|
+
| `InvalidHostnameError` | Cookie value isn't a valid hostname |
|
|
232
|
+
| `HostValidationError` | Custom `validate` function threw |
|
|
233
|
+
| `NoRouteMatchError` | No host pattern matched the request |
|
|
234
|
+
| `InvalidHandlerError` | Handler is not a function, or a lazy mount resolved to a module without a usable `default` export |
|
|
235
|
+
| `HostRouterError` | A `.map()` inline handler resolved to a module namespace (a misused lazy import — use `.lazy()`) |
|
|
197
236
|
|
|
198
237
|
See the fallback section above for a `NoRouteMatchError` catch example.
|
|
199
238
|
|
|
200
239
|
## Nesting Host Routers
|
|
201
240
|
|
|
202
|
-
A lazy
|
|
241
|
+
A lazy mount can resolve to another `HostRouter`:
|
|
203
242
|
|
|
204
243
|
```typescript
|
|
205
244
|
// apps/regional.ts
|
|
206
245
|
import { createHostRouter } from "@rangojs/router/host";
|
|
207
246
|
|
|
208
247
|
const regional = createHostRouter();
|
|
209
|
-
regional.host(["us.*"]).
|
|
210
|
-
regional.host(["eu.*"]).
|
|
248
|
+
regional.host(["us.*"]).lazy(() => import("./regions/us"));
|
|
249
|
+
regional.host(["eu.*"]).lazy(() => import("./regions/eu"));
|
|
211
250
|
|
|
212
251
|
export default regional;
|
|
213
252
|
```
|
|
214
253
|
|
|
215
254
|
```typescript
|
|
216
255
|
// host-router.ts
|
|
217
|
-
router.host(["**.regional.example.com"]).
|
|
256
|
+
router.host(["**.regional.example.com"]).lazy(() => import("./apps/regional"));
|
|
218
257
|
```
|
|
258
|
+
|
|
259
|
+
## Cross-app navigation is a full document load
|
|
260
|
+
|
|
261
|
+
A client-side navigation that crosses an app boundary (e.g. a `<Link>` or
|
|
262
|
+
intercepted `<a>` from the app at `/` into an app mounted at `/shop`) is a **hard
|
|
263
|
+
document navigation**, not a soft in-tree swap. When the server sees a partial
|
|
264
|
+
(SPA) request whose router id doesn't match the matched app, it returns
|
|
265
|
+
`X-RSC-Reload` and the client does a real document navigation to the target.
|
|
266
|
+
|
|
267
|
+
Why a reload rather than a soft swap: a soft swap can't faithfully re-establish
|
|
268
|
+
the target app's **document-level** state. Stylesheets shared across apps are
|
|
269
|
+
dropped by React 19's by-`href` resource dedup; and theme, warmup, and
|
|
270
|
+
prefetch-TTL are document-lifetime (captured once at load — see
|
|
271
|
+
`browser/app-shell.ts`), so the target app's config would never take effect. A
|
|
272
|
+
full document load re-establishes the target app's entire document — CSS, theme,
|
|
273
|
+
meta, everything — by construction. So you do **not** need to coordinate
|
|
274
|
+
stylesheet `href`s, `precedence`, theme config, etc. across independently-authored
|
|
275
|
+
apps; each app owns its own document.
|
|
276
|
+
|
|
277
|
+
**Within-app** navigation is unchanged — a normal soft SPA update (the document
|
|
278
|
+
stays mounted). Only crossing an app boundary triggers the reload.
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: i18n
|
|
3
|
+
description: Locale-aware routing with `include("/:locale?", ...)`, locale resolution chains, and react-intl integration
|
|
4
|
+
argument-hint: "[topic]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Internationalization (i18n) and Locale Routing
|
|
8
|
+
|
|
9
|
+
Rango doesn't ship an i18n module. The router gives you the URL primitives
|
|
10
|
+
(optional include prefixes, constraints, typed reverse) and you compose
|
|
11
|
+
them with whatever message library you use — `react-intl`, `lingui`,
|
|
12
|
+
`@formatjs/intl`, or hand-rolled.
|
|
13
|
+
|
|
14
|
+
This skill covers:
|
|
15
|
+
|
|
16
|
+
- Mounting routes under an optional locale prefix (`/`, `/en`, `/gb`)
|
|
17
|
+
- Constraining the prefix to a known locale set
|
|
18
|
+
- Resolving the active locale (URL → cookie → `Accept-Language` → default)
|
|
19
|
+
- Generating localized URLs via `reverse()` round-trip
|
|
20
|
+
- Wiring `react-intl` into an RSC route tree
|
|
21
|
+
|
|
22
|
+
## URL Shape: Optional Locale Prefix
|
|
23
|
+
|
|
24
|
+
Mount your localized routes under an optional include prefix so the
|
|
25
|
+
default locale lives at the bare URL and other locales get a prefix:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// urls.tsx
|
|
29
|
+
import { urls } from "@rangojs/router";
|
|
30
|
+
import { menuRoutes } from "./menu";
|
|
31
|
+
|
|
32
|
+
export const urlpatterns = urls(({ include }) => [
|
|
33
|
+
include("/:locale?", menuRoutes, { name: "menu" }),
|
|
34
|
+
]);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
URLs that match:
|
|
38
|
+
|
|
39
|
+
| URL | Matched route | `ctx.params.locale` |
|
|
40
|
+
| -------------- | --------------- | ------------------- |
|
|
41
|
+
| `/` | `menu.index` | `undefined` |
|
|
42
|
+
| `/en` | `menu.index` | `"en"` |
|
|
43
|
+
| `/c/breads` | `menu.category` | `undefined` |
|
|
44
|
+
| `/en/c/breads` | `menu.category` | `"en"` |
|
|
45
|
+
|
|
46
|
+
> **Constrain to known locales** when you want unknown locales to fall
|
|
47
|
+
> through to other routes (or 404) instead of being treated as a slug:
|
|
48
|
+
>
|
|
49
|
+
> ```typescript
|
|
50
|
+
> include("/:locale(en|gb|fr)?", menuRoutes, { name: "menu" });
|
|
51
|
+
> ```
|
|
52
|
+
>
|
|
53
|
+
> `/de` now 404s (constraint rejects `de`), and `/c/breads` continues to
|
|
54
|
+
> match `menu.category` with `locale: undefined`. Without the constraint,
|
|
55
|
+
> `/de` would match `menu.index` with `locale: "de"`.
|
|
56
|
+
|
|
57
|
+
## Reading the Locale in Handlers
|
|
58
|
+
|
|
59
|
+
Absent optionals are `undefined` (not `""`), so `??` coalesces correctly:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { Handler } from "@rangojs/router";
|
|
63
|
+
|
|
64
|
+
export const MenuIndex: Handler<"menu.index"> = (ctx) => {
|
|
65
|
+
// ctx.params.locale is `string | undefined`
|
|
66
|
+
const locale = resolveLocale(ctx);
|
|
67
|
+
return <Welcome locale={locale} />;
|
|
68
|
+
};
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The `resolveLocale` helper below implements a typical fallback chain.
|
|
72
|
+
|
|
73
|
+
## Locale Resolution
|
|
74
|
+
|
|
75
|
+
URL is the strongest signal but you usually want a fallback chain:
|
|
76
|
+
|
|
77
|
+
1. **URL prefix** — if the user navigates to `/gb/...`, honor it
|
|
78
|
+
2. **Cookie** — sticky preference set by a previous language switcher
|
|
79
|
+
3. **`Accept-Language`** — browser hint
|
|
80
|
+
4. **Default** — your app default
|
|
81
|
+
|
|
82
|
+
Put it in a small helper that every locale-aware handler calls:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// lib/locale.ts
|
|
86
|
+
import { cookies, headers } from "@rangojs/router";
|
|
87
|
+
|
|
88
|
+
export const SUPPORTED_LOCALES = ["en", "gb", "fr"] as const;
|
|
89
|
+
export type Locale = (typeof SUPPORTED_LOCALES)[number];
|
|
90
|
+
const DEFAULT_LOCALE: Locale = "en";
|
|
91
|
+
|
|
92
|
+
const isSupported = (v: string): v is Locale =>
|
|
93
|
+
(SUPPORTED_LOCALES as readonly string[]).includes(v);
|
|
94
|
+
|
|
95
|
+
export function resolveLocale(ctx: {
|
|
96
|
+
params: Record<string, string | undefined>;
|
|
97
|
+
}): Locale {
|
|
98
|
+
const fromUrl = ctx.params.locale;
|
|
99
|
+
if (fromUrl && isSupported(fromUrl)) return fromUrl;
|
|
100
|
+
|
|
101
|
+
const fromCookie = cookies().get("locale")?.value;
|
|
102
|
+
if (fromCookie && isSupported(fromCookie)) return fromCookie;
|
|
103
|
+
|
|
104
|
+
const accept = headers().get("accept-language") ?? "";
|
|
105
|
+
for (const tag of accept.split(",")) {
|
|
106
|
+
const code = tag.split(";")[0].trim().split("-")[0];
|
|
107
|
+
if (isSupported(code)) return code as Locale;
|
|
108
|
+
}
|
|
109
|
+
return DEFAULT_LOCALE;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
If you want to redirect to the canonical URL when the resolved locale
|
|
114
|
+
doesn't match the URL (e.g., user has `gb` cookie but visits `/`), do
|
|
115
|
+
that in a global middleware so it covers actions too:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { redirect } from "@rangojs/router";
|
|
119
|
+
|
|
120
|
+
router.use("/*", async (ctx, next) => {
|
|
121
|
+
const fromUrl = ctx.params.locale;
|
|
122
|
+
const resolved = resolveLocale(ctx);
|
|
123
|
+
if (resolved !== DEFAULT_LOCALE && !fromUrl) {
|
|
124
|
+
return redirect(`/${resolved}${ctx.url.pathname}`);
|
|
125
|
+
}
|
|
126
|
+
await next();
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Generating Localized URLs
|
|
131
|
+
|
|
132
|
+
`reverse()` treats `undefined` and `""` for an optional param as "absent"
|
|
133
|
+
and collapses the segment cleanly. The round-trip is symmetric with the
|
|
134
|
+
matcher:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
ctx.reverse("menu.index", { locale: "" }); // → "/"
|
|
138
|
+
ctx.reverse("menu.index", { locale: undefined }); // → "/"
|
|
139
|
+
ctx.reverse("menu.index", { locale: "en" }); // → "/en"
|
|
140
|
+
ctx.reverse("menu.category", { locale: "en", slug: "breads" }); // → "/en/c/breads"
|
|
141
|
+
ctx.reverse("menu.category", { slug: "breads" }); // → "/c/breads"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
If the active locale is the app default and your URL strategy hides it
|
|
145
|
+
(`"en"` → `/`, others → `/<locale>`), normalize before calling reverse:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
const normalized = locale === DEFAULT_LOCALE ? undefined : locale;
|
|
149
|
+
const href = ctx.reverse("menu.category", { locale: normalized, slug });
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## react-intl Integration
|
|
153
|
+
|
|
154
|
+
`react-intl` needs a `<IntlProvider>` wrapping the tree, with `locale`
|
|
155
|
+
and `messages` props. The cleanest split: load messages on the server
|
|
156
|
+
(handler or layout), pass them through to a client provider component.
|
|
157
|
+
|
|
158
|
+
### Messages loader
|
|
159
|
+
|
|
160
|
+
Load message bundles per locale. Keep them server-side so they stream
|
|
161
|
+
through the RSC payload and don't bloat the client bundle:
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// lib/messages.ts
|
|
165
|
+
import type { Locale } from "./locale";
|
|
166
|
+
|
|
167
|
+
const loaders: Record<Locale, () => Promise<Record<string, string>>> = {
|
|
168
|
+
en: () => import("../messages/en.json").then((m) => m.default),
|
|
169
|
+
gb: () => import("../messages/gb.json").then((m) => m.default),
|
|
170
|
+
fr: () => import("../messages/fr.json").then((m) => m.default),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export async function loadMessages(locale: Locale) {
|
|
174
|
+
return loaders[locale]();
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Server layout: hand off to the client provider
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
// layouts/intl-layout.tsx (server component)
|
|
182
|
+
import type { ReactNode } from "react";
|
|
183
|
+
import { resolveLocale } from "../lib/locale";
|
|
184
|
+
import { loadMessages } from "../lib/messages";
|
|
185
|
+
import { IntlClientProvider } from "../components/intl-client-provider";
|
|
186
|
+
|
|
187
|
+
export async function IntlLayout({
|
|
188
|
+
ctx,
|
|
189
|
+
children,
|
|
190
|
+
}: {
|
|
191
|
+
ctx: any;
|
|
192
|
+
children: ReactNode;
|
|
193
|
+
}) {
|
|
194
|
+
const locale = resolveLocale(ctx);
|
|
195
|
+
const messages = await loadMessages(locale);
|
|
196
|
+
return (
|
|
197
|
+
<IntlClientProvider locale={locale} messages={messages}>
|
|
198
|
+
{children}
|
|
199
|
+
</IntlClientProvider>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Client provider
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
// components/intl-client-provider.tsx
|
|
208
|
+
"use client";
|
|
209
|
+
|
|
210
|
+
import { IntlProvider } from "react-intl";
|
|
211
|
+
import type { ReactNode } from "react";
|
|
212
|
+
|
|
213
|
+
export function IntlClientProvider({
|
|
214
|
+
locale,
|
|
215
|
+
messages,
|
|
216
|
+
children,
|
|
217
|
+
}: {
|
|
218
|
+
locale: string;
|
|
219
|
+
messages: Record<string, string>;
|
|
220
|
+
children: ReactNode;
|
|
221
|
+
}) {
|
|
222
|
+
return (
|
|
223
|
+
<IntlProvider
|
|
224
|
+
locale={locale}
|
|
225
|
+
defaultLocale="en"
|
|
226
|
+
messages={messages}
|
|
227
|
+
onError={(err) => {
|
|
228
|
+
if (err.code === "MISSING_TRANSLATION") return; // common, log only
|
|
229
|
+
console.error(err);
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
232
|
+
{children}
|
|
233
|
+
</IntlProvider>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Mounting
|
|
239
|
+
|
|
240
|
+
Wrap your localized routes with the layout:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import { urls } from "@rangojs/router";
|
|
244
|
+
import { IntlLayout } from "./layouts/intl-layout";
|
|
245
|
+
import { menuRoutes } from "./menu";
|
|
246
|
+
|
|
247
|
+
export const urlpatterns = urls(({ layout, include }) => [
|
|
248
|
+
layout(IntlLayout, () => [
|
|
249
|
+
include("/:locale?", menuRoutes, { name: "menu" }),
|
|
250
|
+
]),
|
|
251
|
+
]);
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`<FormattedMessage>`, `useIntl()`, etc. work in any client component
|
|
255
|
+
under the layout. Server components can use `formatjs`'s `createIntl()`
|
|
256
|
+
directly with the same `messages` map for static text.
|
|
257
|
+
|
|
258
|
+
## Common Pitfalls
|
|
259
|
+
|
|
260
|
+
| Pitfall | Fix |
|
|
261
|
+
| ------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
262
|
+
| `ctx.params.locale === ""` returns `false` | Absent optionals are `undefined`, not `""`. Use `=== undefined` or `??`. |
|
|
263
|
+
| `ctx.params.locale ?? "en"` returns `""` | Pre-fix behavior. After the include-prefix fix this works correctly. |
|
|
264
|
+
| Bare `/` 404s when mounted via `include("/:locale?", routes)` | Requires the all-optional pattern fix in `compilePattern` (shipped). |
|
|
265
|
+
| Unknown locale (e.g. `/de`) matches as `locale: "de"` | Add a constraint: `:locale(en\|gb\|fr)?`. Unknown values now 404. |
|
|
266
|
+
| Reverse produces `//c/breads` for absent locale | `reverse()` collapses `undefined`/`""` segments — should not happen. File a bug. |
|
|
267
|
+
| Locale switcher loses search params | Read `ctx.url.search` and pass to `reverse(..., undefined, parsedSearch)`. |
|
|
268
|
+
| Action middleware can't read `ctx.params.locale` | Route middleware doesn't wrap action execution. Use global `router.use()` for actions. |
|
|
269
|
+
|
|
270
|
+
## Cross-references
|
|
271
|
+
|
|
272
|
+
- `/route` — optional URL param syntax and runtime contract
|
|
273
|
+
- `/typesafety` — `RouteParams<"name">` typing for optionals
|
|
274
|
+
- `/middleware` — global vs route middleware scope (matters for actions)
|
|
275
|
+
- `/server-actions` — actions and the global-vs-route middleware boundary
|
|
276
|
+
- `/links` — `ctx.reverse()` and locale-aware URL generation
|
|
@@ -8,9 +8,6 @@ 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
|
-
Canonical semantics reference:
|
|
12
|
-
[docs/execution-model.md](../../docs/internal/execution-model.md)
|
|
13
|
-
|
|
14
11
|
## Basic Intercept
|
|
15
12
|
|
|
16
13
|
```typescript
|
|
@@ -26,7 +23,7 @@ function ShopLayout() {
|
|
|
26
23
|
);
|
|
27
24
|
}
|
|
28
25
|
|
|
29
|
-
export const urlpatterns = urls(({ path, layout, intercept, loader }) => [
|
|
26
|
+
export const urlpatterns = urls(({ path, layout, intercept, loader, loading }) => [
|
|
30
27
|
layout(<ShopLayout />, () => [
|
|
31
28
|
// Intercept product detail - shows modal during soft navigation
|
|
32
29
|
intercept(
|
|
@@ -110,8 +107,10 @@ Use named revalidation contracts on both the outer producer and the intercept
|
|
|
110
107
|
consumer when they share `ctx.set()` data:
|
|
111
108
|
|
|
112
109
|
```typescript
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
import * as ProductActions from "./actions/product";
|
|
111
|
+
|
|
112
|
+
export const revalidateProductShell = (ctx) =>
|
|
113
|
+
ctx.isAction(ProductActions) || undefined;
|
|
115
114
|
|
|
116
115
|
layout(ProductLayout, () => [
|
|
117
116
|
revalidate(revalidateProductShell), // producer reruns
|
|
@@ -143,18 +142,41 @@ layout(ProductLayout, () => [
|
|
|
143
142
|
]);
|
|
144
143
|
```
|
|
145
144
|
|
|
146
|
-
## Conditional Intercept with when
|
|
145
|
+
## Conditional Intercept with the `when` config
|
|
147
146
|
|
|
148
|
-
Only intercept based on navigation context
|
|
147
|
+
Only intercept based on navigation context. `when` is the 4th argument
|
|
148
|
+
(an `InterceptConfig` object); the other use-items go in the 5th-argument
|
|
149
|
+
callback.
|
|
149
150
|
|
|
150
151
|
```typescript
|
|
151
152
|
intercept(
|
|
152
153
|
"@modal",
|
|
153
154
|
"product",
|
|
154
155
|
<ProductModal />,
|
|
156
|
+
// Only intercept when coming from a different section
|
|
157
|
+
{ when: ({ from }) => !from.pathname.startsWith("/shop/product/") },
|
|
158
|
+
() => [
|
|
159
|
+
loader(ProductLoader),
|
|
160
|
+
]
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`when` is a match-time selector receiving `{ from, to, params, segments, ... }`.
|
|
165
|
+
Pass an array of predicates for AND logic (all must return true). Omit `when`
|
|
166
|
+
entirely and the intercept always activates.
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
intercept(
|
|
170
|
+
"@modal",
|
|
171
|
+
"product",
|
|
172
|
+
<ProductModal />,
|
|
173
|
+
{
|
|
174
|
+
when: [
|
|
175
|
+
({ from }) => from.pathname.startsWith("/shop"),
|
|
176
|
+
({ params }) => params.slug !== "featured",
|
|
177
|
+
],
|
|
178
|
+
},
|
|
155
179
|
() => [
|
|
156
|
-
// Only intercept when coming from a different section
|
|
157
|
-
when(({ from }) => !from.pathname.startsWith("/shop/product/")),
|
|
158
180
|
loader(ProductLoader),
|
|
159
181
|
]
|
|
160
182
|
)
|
|
@@ -197,6 +219,31 @@ function ModalWrapper({ children }) {
|
|
|
197
219
|
}
|
|
198
220
|
```
|
|
199
221
|
|
|
222
|
+
## Interaction with View Transitions
|
|
223
|
+
|
|
224
|
+
A layout that owns the `@modal` slot can also configure `transition()` for page
|
|
225
|
+
fades — opening a modal does **not** fire the layout's view transition. Rango
|
|
226
|
+
narrows the layout's `<ViewTransition>` wrap to the layout's default outlet
|
|
227
|
+
content, so `<ParallelOutlet />` (the slot where the modal mounts) is a sibling
|
|
228
|
+
of the wrap, not inside its subtree. Form actions submitted from inside an open
|
|
229
|
+
modal also commit without firing the underlying layout's transition, and the
|
|
230
|
+
modal subtree identity is preserved across revalidation (no remount,
|
|
231
|
+
`useActionState` survives). Closing the modal restores the page without a
|
|
232
|
+
stray transition.
|
|
233
|
+
|
|
234
|
+
For a modal-only morph (e.g. when intercepted URLs change while the modal
|
|
235
|
+
stays open), use an element-level React `<ViewTransition>` inside the modal
|
|
236
|
+
component — `transition()` accepted on `intercept()` via the DSL is not
|
|
237
|
+
applied to slot rendering today.
|
|
238
|
+
|
|
239
|
+
Caveat: route-level `transition()` wraps the route component itself, so a
|
|
240
|
+
`<ParallelOutlet />` rendered directly inside that route component would still
|
|
241
|
+
be inside the route's VT subtree. Mount the slot in a layout instead when you
|
|
242
|
+
combine intercept modals with route-level transitions.
|
|
243
|
+
|
|
244
|
+
See [skills/view-transitions](../view-transitions/SKILL.md) for the full
|
|
245
|
+
contract and direction-aware examples.
|
|
246
|
+
|
|
200
247
|
## Interaction with Prerender
|
|
201
248
|
|
|
202
249
|
When the target route of an intercept uses `Prerender`, the intercept handler is
|
|
@@ -218,10 +265,13 @@ layout(ShopLayout, () => [
|
|
|
218
265
|
]),
|
|
219
266
|
|
|
220
267
|
// This intercept is also pre-rendered at build time
|
|
221
|
-
intercept(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
268
|
+
intercept(
|
|
269
|
+
"@modal",
|
|
270
|
+
".detail",
|
|
271
|
+
<ProductModal />,
|
|
272
|
+
{ when: ({ from }) => from.pathname.startsWith("/shop") },
|
|
273
|
+
() => [loader(ProductLoader)],
|
|
274
|
+
),
|
|
225
275
|
])
|
|
226
276
|
```
|
|
227
277
|
|
|
@@ -229,8 +279,8 @@ Build-time behavior:
|
|
|
229
279
|
|
|
230
280
|
- The intercept handler (`<ProductModal />`) is resolved with BuildContext
|
|
231
281
|
- Result is stored under the key `"detail/paramHash/i"` (intercept variant)
|
|
232
|
-
- `when
|
|
233
|
-
- `when
|
|
282
|
+
- `when` config conditions are skipped at build time (all intercepts pre-rendered unconditionally)
|
|
283
|
+
- `when` is still evaluated at runtime by the intercept-resolution middleware
|
|
234
284
|
|
|
235
285
|
Runtime behavior:
|
|
236
286
|
|
|
@@ -281,7 +331,6 @@ export const shopPatterns = urls(({
|
|
|
281
331
|
intercept,
|
|
282
332
|
loader,
|
|
283
333
|
loading,
|
|
284
|
-
when,
|
|
285
334
|
}) => [
|
|
286
335
|
layout(<ShopLayout />, () => [
|
|
287
336
|
parallel({
|
|
@@ -293,8 +342,8 @@ export const shopPatterns = urls(({
|
|
|
293
342
|
"@modal",
|
|
294
343
|
"product", // Route name (without prefix)
|
|
295
344
|
<ProductModalContent />,
|
|
345
|
+
{ when: ({ from }) => !from.pathname.startsWith("/shop/product/") },
|
|
296
346
|
() => [
|
|
297
|
-
when(({ from }) => !from.pathname.startsWith("/shop/product/")),
|
|
298
347
|
layout(<ModalWrapper />),
|
|
299
348
|
loading(<ProductModalSkeleton />),
|
|
300
349
|
loader(ProductLoader, () => [cache()]),
|
|
@@ -314,7 +363,7 @@ export const shopPatterns = urls(({
|
|
|
314
363
|
|
|
315
364
|
## Handler-attached `.use`
|
|
316
365
|
|
|
317
|
-
Intercept handlers can carry their own middleware, loaders, loading state, error/notFound boundaries, and even nested `layout`/`route
|
|
366
|
+
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`.)
|
|
318
367
|
|
|
319
368
|
```typescript
|
|
320
369
|
const QuickViewModal: Handler = async (ctx) => {
|