@rangojs/router 0.0.0-experimental.31 → 0.0.0-experimental.3232cd17
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 +4 -0
- package/README.md +198 -44
- package/dist/bin/rango.js +287 -105
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3248 -1117
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +73 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +107 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +245 -21
- package/skills/caching/SKILL.md +302 -6
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +270 -30
- package/skills/host-router/SKILL.md +82 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +49 -5
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +294 -30
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +203 -7
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +250 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +97 -5
- package/skills/router-setup/SKILL.md +90 -5
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -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 +121 -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 +120 -0
- package/skills/typesafety/SKILL.md +329 -27
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +67 -40
- 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/app-version.ts +14 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +86 -147
- 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/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +148 -19
- package/src/browser/navigation-client.ts +187 -67
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +18 -66
- package/src/browser/partial-update.ts +123 -94
- package/src/browser/prefetch/cache.ts +214 -36
- package/src/browser/prefetch/fetch.ts +260 -38
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +126 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +93 -11
- package/src/browser/react/NavigationProvider.tsx +115 -34
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +49 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -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 +23 -69
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +22 -5
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- 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 +52 -1
- package/src/browser/rsc-router.tsx +215 -76
- package/src/browser/scroll-restoration.ts +46 -39
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +176 -50
- package/src/browser/types.ts +95 -11
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +137 -32
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -96
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +149 -43
- package/src/cache/cache-scope.ts +148 -81
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2550 -93
- package/src/cache/cf/index.ts +11 -17
- package/src/cache/document-cache.ts +78 -27
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +108 -290
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +84 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- 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 +52 -26
- package/src/index.ts +100 -38
- package/src/internal-debug.ts +2 -4
- 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/network-error-thrower.tsx +1 -6
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +437 -274
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +113 -37
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +52 -10
- package/src/route-definition/resolve-handler-use.ts +161 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -17
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +108 -9
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +45 -22
- package/src/router/handler-context.ts +83 -41
- package/src/router/intercept-resolution.ts +25 -23
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +213 -30
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +49 -45
- package/src/router/match-api.ts +121 -205
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +27 -6
- package/src/router/match-middleware/cache-lookup.ts +205 -249
- package/src/router/match-middleware/cache-store.ts +45 -32
- package/src/router/match-middleware/intercept-resolution.ts +8 -28
- package/src/router/match-middleware/segment-resolution.ts +52 -18
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +104 -40
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +173 -143
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +109 -63
- package/src/router/prerender-match.ts +192 -54
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +63 -55
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +6 -28
- package/src/router/router-interfaces.ts +100 -35
- package/src/router/router-options.ts +91 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +242 -75
- package/src/router/segment-resolution/helpers.ts +64 -25
- package/src/router/segment-resolution/loader-cache.ts +41 -37
- package/src/router/segment-resolution/revalidation.ts +456 -372
- 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 +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +2 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +91 -46
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +134 -43
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +492 -383
- package/src/rsc/helpers.ts +162 -46
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +30 -3
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +90 -63
- package/src/rsc/rsc-rendering.ts +56 -54
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +74 -67
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +25 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +134 -0
- package/src/segment-system.tsx +272 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +309 -61
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +26 -24
- package/src/server/loader-registry.ts +10 -28
- package/src/server/request-context.ts +348 -128
- package/src/ssr/index.tsx +23 -15
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +618 -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 +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -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 +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -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 +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +233 -81
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +107 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +37 -14
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +58 -11
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/discover-routers.ts +130 -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 +192 -99
- 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 +51 -6
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +8 -0
- package/src/vite/plugin-types.ts +187 -69
- package/src/vite/plugins/cjs-to-esm.ts +8 -18
- 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-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/plugins/expose-action-id.ts +49 -98
- package/src/vite/plugins/expose-id-utils.ts +11 -50
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
- package/src/vite/plugins/expose-internal-ids.ts +554 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +41 -20
- package/src/vite/plugins/virtual-entries.ts +2 -17
- package/src/vite/rango.ts +257 -289
- package/src/vite/router-discovery.ts +930 -140
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -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 +27 -29
- package/src/vite/utils/shared-utils.ts +92 -42
- 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/router/middleware-cookies.ts +0 -55
package/skills/hooks/SKILL.md
CHANGED
|
@@ -58,6 +58,26 @@ function NavigationControls() {
|
|
|
58
58
|
}
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
#### Skipping revalidation
|
|
62
|
+
|
|
63
|
+
Pass `revalidate: false` to skip the RSC server fetch for same-pathname navigations (search param or hash changes). The URL updates and all hooks re-render, but server components stay as-is.
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
// Update search params without server round-trip
|
|
67
|
+
router.push("/products?color=blue", { revalidate: false });
|
|
68
|
+
router.replace("/products?page=3", { revalidate: false });
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
If the pathname changes, `revalidate: false` is silently ignored and a full navigation occurs. This also works on `<Link>`:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
<Link to="/products?color=blue" revalidate={false}>
|
|
75
|
+
Blue
|
|
76
|
+
</Link>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Plain `<a>` tags can opt in via `data-revalidate="false"`.
|
|
80
|
+
|
|
61
81
|
### useSegments()
|
|
62
82
|
|
|
63
83
|
Access current URL path and matched route segments:
|
|
@@ -69,8 +89,8 @@ import { useSegments } from "@rangojs/router/client";
|
|
|
69
89
|
function Breadcrumbs() {
|
|
70
90
|
const { path, segmentIds, location } = useSegments();
|
|
71
91
|
|
|
72
|
-
// path: ["
|
|
73
|
-
// segmentIds: ["
|
|
92
|
+
// path: ["shop", "products", "123"] (split on "/", no leading slash on any element)
|
|
93
|
+
// segmentIds: ["L0", "L0L1", "L0L1R0"] (opaque internal short-codes, not route names)
|
|
74
94
|
// location: URL object
|
|
75
95
|
|
|
76
96
|
return <nav>{path.join(" > ")}</nav>;
|
|
@@ -170,6 +190,141 @@ function SearchResults() {
|
|
|
170
190
|
}
|
|
171
191
|
```
|
|
172
192
|
|
|
193
|
+
**Shared refetch behavior**:
|
|
194
|
+
|
|
195
|
+
When the loader is registered on the route via `loader()`, a plain
|
|
196
|
+
`load()` call (no options, or a trivially-defaulted GET with no
|
|
197
|
+
`params` and no `body`) broadcasts its result to every component
|
|
198
|
+
reading the same loader id. Layout, page, and parallel-slot reads
|
|
199
|
+
all converge on the new value:
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
// Layout button calls load() — the page read below sees the update too.
|
|
203
|
+
function Layout() {
|
|
204
|
+
const { data, load } = useLoader(CartLoader);
|
|
205
|
+
return <button onClick={() => load()}>Refresh ({data.count})</button>;
|
|
206
|
+
}
|
|
207
|
+
function Page() {
|
|
208
|
+
const { data } = useLoader(CartLoader); // updates with the layout's load()
|
|
209
|
+
return <span>{data.count} items</span>;
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`isLoading` and `error` follow the same scope. `throwOnError: true`
|
|
214
|
+
render-throws are scoped to the **originating** hook — sibling readers
|
|
215
|
+
see the error in their `error` state but their boundaries are not
|
|
216
|
+
triggered by someone else's failure. A successful follow-up `load()`
|
|
217
|
+
clears the shared error.
|
|
218
|
+
|
|
219
|
+
**`load()` calls that stay local** (no broadcast, per-hook state, same
|
|
220
|
+
semantics as the old per-component `useState`):
|
|
221
|
+
|
|
222
|
+
- `load({ params: { ... } })` — explicit params.
|
|
223
|
+
- `load({ method: "POST", body })` — mutations.
|
|
224
|
+
- Any `load()` on a `useFetchLoader(loader)` whose loader is **not**
|
|
225
|
+
registered on the current route. Two unrelated components calling
|
|
226
|
+
`load()` on the same fetchable-but-unregistered loader keep
|
|
227
|
+
independent results.
|
|
228
|
+
|
|
229
|
+
So the search/list pattern still works — two components calling
|
|
230
|
+
`load({ params: { q } })` with different `q` values each keep their
|
|
231
|
+
own result; they do not collapse to last-write-wins through a shared
|
|
232
|
+
store.
|
|
233
|
+
|
|
234
|
+
**Scoping refetch with a `key`**:
|
|
235
|
+
|
|
236
|
+
Pass a `key` to partition the shared refresh store. Only hooks using the
|
|
237
|
+
**same** `key` refresh together when one of them calls `load()`. This is a
|
|
238
|
+
client-side refresh identity only — it never changes the request sent to the
|
|
239
|
+
server, and is unrelated to the server `cache({ key })` option and to
|
|
240
|
+
`revalidate()`.
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
// Two independent dashboards using the same loader. Without a key, one
|
|
244
|
+
// dashboard's load() would flip the other's spinner and value. With a key,
|
|
245
|
+
// they refresh independently.
|
|
246
|
+
function Dashboard({ id }: { id: string }) {
|
|
247
|
+
const { data, load } = useLoader(StatsLoader, { key: `dashboard:${id}` });
|
|
248
|
+
return <button onClick={() => load()}>Refresh {data.total}</button>;
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The `key` widens sharing in two ways the default cannot:
|
|
253
|
+
|
|
254
|
+
- **Parameterized GETs share.** `useFetchLoader(SearchLoader, { key: q })`
|
|
255
|
+
with the same `q` in two components share one result and refresh together —
|
|
256
|
+
a keyed `load({ params: { q } })` broadcasts to the group instead of staying
|
|
257
|
+
local. (Mutations — non-GET or `body` — stay local even with a key.)
|
|
258
|
+
- **Unregistered loaders share.** A `key` makes `useFetchLoader` of a loader
|
|
259
|
+
that is **not** registered on the route share too, letting unrelated
|
|
260
|
+
components opt into a common refresh group.
|
|
261
|
+
|
|
262
|
+
Lifecycle: a keyed read of an unregistered loader is reference-counted — its
|
|
263
|
+
shared value lives as long as at least one component using that key is mounted.
|
|
264
|
+
A persistent component (e.g. a header) keeps the value across navigations; a
|
|
265
|
+
route-scoped component's value is reclaimed when it unmounts. Registered-loader
|
|
266
|
+
reads (keyed or not) reset on navigation from fresh route data, as before.
|
|
267
|
+
|
|
268
|
+
**Refreshing multiple loaders together (`refreshGroup` + `useRefreshLoaders`)**:
|
|
269
|
+
|
|
270
|
+
`key` groups readers of one loader. To refresh **different** loaders together,
|
|
271
|
+
tag them with a shared `refreshGroup` name and trigger them with
|
|
272
|
+
`useRefreshLoaders()`. The hook takes no argument; you pass the group(s) to the
|
|
273
|
+
function it returns, so one `useRefreshLoaders()` can refresh different groups
|
|
274
|
+
depending on context. A read may carry **several** tags — pass an array — and is
|
|
275
|
+
refreshed when **any** of its groups is refreshed:
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
function Profile() {
|
|
279
|
+
const { data } = useLoader(ProfileLoader, {
|
|
280
|
+
key: userId,
|
|
281
|
+
refreshGroup: "account",
|
|
282
|
+
});
|
|
283
|
+
return <span>{data.name}</span>;
|
|
284
|
+
}
|
|
285
|
+
function Orders() {
|
|
286
|
+
// Tagged into two groups: refreshed by "account" (the whole set) or the
|
|
287
|
+
// finer "orders" tag.
|
|
288
|
+
const { data } = useLoader(OrdersLoader, {
|
|
289
|
+
key: userId,
|
|
290
|
+
refreshGroup: ["account", "orders"],
|
|
291
|
+
});
|
|
292
|
+
return <span>{data.count} orders</span>;
|
|
293
|
+
}
|
|
294
|
+
function RefreshButtons() {
|
|
295
|
+
const refresh = useRefreshLoaders();
|
|
296
|
+
return (
|
|
297
|
+
<>
|
|
298
|
+
<button onClick={() => refresh("account")}>Refresh account</button>
|
|
299
|
+
<button onClick={() => refresh("orders")}>Refresh orders only</button>
|
|
300
|
+
<button onClick={() => refresh(["account", "orders"])}>
|
|
301
|
+
Refresh both
|
|
302
|
+
</button>
|
|
303
|
+
</>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
`refresh(groups)` accepts one name or an array and re-runs every currently-mounted
|
|
309
|
+
member tagged with **any** of them, with a **plain GET** against the current route
|
|
310
|
+
URL — no params, no body, no mutation methods, because a group spans loaders with
|
|
311
|
+
different shapes. A member that sits in two of the requested groups is fetched
|
|
312
|
+
once (members are unioned and deduped by read). It returns a promise that resolves
|
|
313
|
+
when all members settle and **rejects with an `AggregateError`** if any fail;
|
|
314
|
+
group refresh never render-throws, so handle failures at the await site
|
|
315
|
+
(`await refresh("account").catch(...)`). Each failing member also exposes its
|
|
316
|
+
error via its own read's `error`.
|
|
317
|
+
|
|
318
|
+
Multiple tags give you granular vs. whole-set refresh from one place: a coarse
|
|
319
|
+
tag (`"account"`) covers everything, while a finer tag (`"orders"`) targets a
|
|
320
|
+
subset. Sharing within a group is opt-in via `key`: members that share a `key`
|
|
321
|
+
share one value (and one fetch); a grouped reader **without** a `key` gets its own
|
|
322
|
+
private bucket, so a group refresh updates only that read and never leaks into
|
|
323
|
+
unrelated unkeyed reads of the same loader. A bucket may belong to several groups
|
|
324
|
+
at once (one read tagged with multiple names, or different reads tagging the same
|
|
325
|
+
keyed bucket with different names). Keep parameterized loaders on the single-loader
|
|
326
|
+
`key` — a plain-GET group refresh sends no params.
|
|
327
|
+
|
|
173
328
|
**Load options**:
|
|
174
329
|
|
|
175
330
|
```tsx
|
|
@@ -278,9 +433,11 @@ path("/dashboard", (ctx) => {
|
|
|
278
433
|
push({ label: "Dashboard", href: "/dashboard" });
|
|
279
434
|
return <DashboardNav handle={Breadcrumbs} />;
|
|
280
435
|
});
|
|
436
|
+
```
|
|
281
437
|
|
|
438
|
+
```tsx
|
|
282
439
|
// Client component — typeof infers the full Handle<T> type
|
|
283
|
-
|
|
440
|
+
"use client";
|
|
284
441
|
import { useHandle, type Breadcrumbs } from "@rangojs/router/client";
|
|
285
442
|
|
|
286
443
|
function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
|
|
@@ -301,6 +458,11 @@ RSC serialization strips the `collect` function via `toJSON()`. On the client,
|
|
|
301
458
|
|
|
302
459
|
## Action Hooks
|
|
303
460
|
|
|
461
|
+
For the full server-action guide (defining actions, `useActionState`,
|
|
462
|
+
`useOptimistic`, validation, revalidation, error handling, file uploads),
|
|
463
|
+
see `/server-actions`. `useAction()` below is a Rango-specific hook for
|
|
464
|
+
tracking actions called outside a `<form action={...}>` flow.
|
|
465
|
+
|
|
304
466
|
### useAction()
|
|
305
467
|
|
|
306
468
|
Track state of server action invocations:
|
|
@@ -484,33 +646,81 @@ const flash = FlashMessage.read();
|
|
|
484
646
|
const product = ProductState.read();
|
|
485
647
|
```
|
|
486
648
|
|
|
487
|
-
|
|
649
|
+
> **Hydration:** `.read()` returns `undefined` on the server but may return
|
|
650
|
+
> a real value on the first client render (history state survives reload).
|
|
651
|
+
> Do not call `.read()` directly during the initial render of a component;
|
|
652
|
+
> call it from an event handler or inside a `useEffect` post-mount. For
|
|
653
|
+
> reactive hydration-safe access, use `useLocationState()` instead.
|
|
488
654
|
|
|
489
|
-
###
|
|
655
|
+
### .write() / .delete() (static, non-reactive)
|
|
490
656
|
|
|
491
|
-
|
|
657
|
+
Static counterparts to `.read()`. Both mutate the current history entry's
|
|
658
|
+
`history.state` via `replaceState`, preserving any other keys (router
|
|
659
|
+
bookkeeping, other location state slots). Both are client-only; they throw
|
|
660
|
+
when called on the server.
|
|
661
|
+
|
|
662
|
+
Neither dispatches an event, so components reading via `useLocationState`
|
|
663
|
+
will NOT re-render until the next navigation/popstate. Pair with `.read()`
|
|
664
|
+
(or a fresh mount via back/forward/reload) instead.
|
|
492
665
|
|
|
493
666
|
```tsx
|
|
494
667
|
"use client";
|
|
495
|
-
import {
|
|
668
|
+
import { ProductState } from "./state";
|
|
496
669
|
|
|
497
|
-
|
|
498
|
-
|
|
670
|
+
// Persisted across hard refresh and back/forward of this entry.
|
|
671
|
+
ProductState.write({ name: "Widget", price: 9.99 });
|
|
672
|
+
|
|
673
|
+
// Read later (or on next mount).
|
|
674
|
+
const current = ProductState.read();
|
|
675
|
+
|
|
676
|
+
// Manually clear the slot. Idempotent if it isn't set.
|
|
677
|
+
ProductState.delete();
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
| Method | Updates `history.state` | Fires `useLocationState` rerender | SSR behavior |
|
|
681
|
+
| ----------- | ----------------------- | --------------------------------- | ------------------- |
|
|
682
|
+
| `.read()` | no | n/a (returns snapshot) | returns `undefined` |
|
|
683
|
+
| `.write()` | yes (replace this slot) | no | throws |
|
|
684
|
+
| `.delete()` | yes (remove this slot) | no | throws |
|
|
685
|
+
|
|
686
|
+
## Cache Control
|
|
687
|
+
|
|
688
|
+
### invalidateClientCache()
|
|
689
|
+
|
|
690
|
+
Force the client's caches to miss after a mutation the router can't see (a REST
|
|
691
|
+
call, a WebSocket push, a login). It is a plain function, not a hook, so it works
|
|
692
|
+
from module-level callbacks too. Imported from the root entry `@rangojs/router`,
|
|
693
|
+
it is selected by export conditions: in a client component it marks the caches
|
|
694
|
+
stale immediately; from a handler/server component it writes a rotated
|
|
695
|
+
`Set-Cookie` for the responding client.
|
|
696
|
+
|
|
697
|
+
```tsx
|
|
698
|
+
"use client";
|
|
699
|
+
import { invalidateClientCache } from "@rangojs/router";
|
|
499
700
|
|
|
701
|
+
function SaveButton() {
|
|
500
702
|
const handleSave = async () => {
|
|
501
703
|
await fetch("/api/data", {
|
|
502
704
|
method: "POST",
|
|
503
705
|
body: JSON.stringify(data),
|
|
504
706
|
});
|
|
505
707
|
|
|
506
|
-
// Invalidate
|
|
507
|
-
|
|
708
|
+
// Invalidate the client's caches after the mutation
|
|
709
|
+
invalidateClientCache();
|
|
508
710
|
};
|
|
509
711
|
|
|
510
712
|
return <button onClick={handleSave}>Save</button>;
|
|
511
713
|
}
|
|
512
714
|
```
|
|
513
715
|
|
|
716
|
+
A module-level subscription works the same way (no component needed):
|
|
717
|
+
|
|
718
|
+
```ts
|
|
719
|
+
import { invalidateClientCache } from "@rangojs/router";
|
|
720
|
+
|
|
721
|
+
socket.on("catalog-updated", () => invalidateClientCache());
|
|
722
|
+
```
|
|
723
|
+
|
|
514
724
|
**Use cases**: REST API mutations, WebSocket updates, non-RSC data changes.
|
|
515
725
|
|
|
516
726
|
## Outlet Components
|
|
@@ -573,6 +783,12 @@ function ProductPage() {
|
|
|
573
783
|
return <h1>Product {params.productId}</h1>;
|
|
574
784
|
}
|
|
575
785
|
|
|
786
|
+
// Annotate the expected shape via a generic
|
|
787
|
+
function ProductPageTyped() {
|
|
788
|
+
const { productId } = useParams<{ productId: string }>();
|
|
789
|
+
return <h1>Product {productId}</h1>;
|
|
790
|
+
}
|
|
791
|
+
|
|
576
792
|
// With selector for performance (re-renders only when selected value changes)
|
|
577
793
|
function ProductId() {
|
|
578
794
|
const productId = useParams((p) => p.productId);
|
|
@@ -580,7 +796,7 @@ function ProductId() {
|
|
|
580
796
|
}
|
|
581
797
|
```
|
|
582
798
|
|
|
583
|
-
Returns merged params from all matched route segments. Updates on navigation commit (not during pending navigation).
|
|
799
|
+
Returns merged params from all matched route segments as a `Readonly<T>` map. Updates on navigation commit (not during pending navigation).
|
|
584
800
|
|
|
585
801
|
### usePathname()
|
|
586
802
|
|
|
@@ -661,24 +877,48 @@ function MountInfo() {
|
|
|
661
877
|
}
|
|
662
878
|
```
|
|
663
879
|
|
|
664
|
-
|
|
880
|
+
### useReverse(routes)
|
|
881
|
+
|
|
882
|
+
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.
|
|
883
|
+
|
|
884
|
+
> 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.
|
|
885
|
+
|
|
886
|
+
```tsx
|
|
887
|
+
"use client";
|
|
888
|
+
import { Link, useReverse } from "@rangojs/router/client";
|
|
889
|
+
import { routes as blogRoutes } from "../urls/blog.gen.js";
|
|
890
|
+
|
|
891
|
+
function BlogNav() {
|
|
892
|
+
const reverse = useReverse(blogRoutes);
|
|
893
|
+
return (
|
|
894
|
+
<nav>
|
|
895
|
+
<Link to={reverse("index")}>Blog</Link>
|
|
896
|
+
<Link to={reverse("post", { postId: "hello" })}>Post</Link>
|
|
897
|
+
</nav>
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
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.
|
|
665
903
|
|
|
666
904
|
## Hook Summary
|
|
667
905
|
|
|
668
|
-
| Hook
|
|
669
|
-
|
|
|
670
|
-
| `useParams()`
|
|
671
|
-
| `usePathname()`
|
|
672
|
-
| `useSearchParams()`
|
|
673
|
-
| `useHref()`
|
|
674
|
-
| `useMount()`
|
|
675
|
-
| `
|
|
676
|
-
| `
|
|
677
|
-
| `
|
|
678
|
-
| `
|
|
679
|
-
| `
|
|
680
|
-
| `
|
|
681
|
-
| `
|
|
682
|
-
| `
|
|
683
|
-
| `
|
|
684
|
-
| `
|
|
906
|
+
| Hook | Purpose | Returns |
|
|
907
|
+
| ------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
908
|
+
| `useParams()` | Route params | `Readonly<T>` (default `Record<string, string>`) or selected value |
|
|
909
|
+
| `usePathname()` | Current pathname | `string` |
|
|
910
|
+
| `useSearchParams()` | URL search params | `ReadonlyURLSearchParams` |
|
|
911
|
+
| `useHref()` | Mount-aware href | `(path) => string` |
|
|
912
|
+
| `useMount()` | Current include() mount path | `string` |
|
|
913
|
+
| `useReverse()` | Local reverse for imported routes | `(name, params?, search?) => string` |
|
|
914
|
+
| `useNavigation()` | Reactive navigation state | state, location, isStreaming |
|
|
915
|
+
| `useRouter()` | Stable router actions | push, replace, refresh, prefetch, back, forward |
|
|
916
|
+
| `useSegments()` | URL path & segment IDs | path, segmentIds, location |
|
|
917
|
+
| `useLinkStatus()` | Link pending state | { pending } |
|
|
918
|
+
| `useLoader()` | Loader data (strict) | data, isLoading, error |
|
|
919
|
+
| `useFetchLoader()` | Loader with on-demand fetch | data, load, isLoading |
|
|
920
|
+
| `useRefreshLoaders()` | Refresh cross-loader group(s) | `() => (groups: string \| string[]) => Promise<void>` |
|
|
921
|
+
| `useHandle()` | Accumulated handle data | T (handle type) |
|
|
922
|
+
| `useAction()` | Server action state | state, error, result |
|
|
923
|
+
| `useLocationState()` | History state (persists or flash) | T \| undefined |
|
|
924
|
+
| `invalidateClientCache()` | Force client caches to miss (function, not a hook; root entry) | `void` |
|
|
@@ -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
|
|
|
@@ -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.
|