@rangojs/router 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +255 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/history-state.d.ts +16 -0
- package/dist/types/browser/navigation-transaction.d.ts +6 -0
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
- package/dist/types/browser/rsc-router.d.ts +1 -1
- package/dist/types/browser/scroll-restoration.d.ts +7 -0
- package/dist/types/cache/cache-exec-scope.d.ts +18 -1
- package/dist/types/cache/cache-key-utils.d.ts +17 -2
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +132 -19
- package/dist/types/cache/cache-tag.d.ts +92 -7
- package/dist/types/cache/cf/cf-base64.d.ts +6 -0
- package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
- package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
- package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
- package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +54 -13
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +110 -21
- package/dist/types/cache/tag-invalidation.d.ts +30 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +220 -44
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
- package/dist/types/client.d.ts +4 -4
- package/dist/types/client.rsc.d.ts +4 -5
- package/dist/types/components/DefaultDocument.d.ts +5 -2
- package/dist/types/context-var.d.ts +6 -5
- package/dist/types/escape-script.d.ts +1 -1
- package/dist/types/handles/MetaTags.d.ts +5 -4
- package/dist/types/handles/Scripts.d.ts +8 -7
- package/dist/types/handles/meta.d.ts +2 -2
- package/dist/types/handles/script.d.ts +17 -16
- package/dist/types/href-client.d.ts +1 -1
- package/dist/types/html.d.ts +12 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +3 -2
- package/dist/types/loader-store.d.ts +7 -4
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/reverse.d.ts +36 -4
- package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
- package/dist/types/route-definition/helpers-types.d.ts +33 -9
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +1 -1
- package/dist/types/router/debug-manifest.d.ts +3 -2
- package/dist/types/router/error-handling.d.ts +20 -0
- package/dist/types/router/handler-context.d.ts +6 -0
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +9 -3
- package/dist/types/router/router-context.d.ts +3 -1
- package/dist/types/router/router-interfaces.d.ts +24 -2
- package/dist/types/router/router-options.d.ts +8 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
- package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
- package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
- package/dist/types/router/segment-wrappers.d.ts +1 -1
- package/dist/types/rsc/full-payload.d.ts +14 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/response-route-handler.d.ts +1 -1
- package/dist/types/rsc/rsc-rendering.d.ts +1 -2
- package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
- package/dist/types/rsc/shell-capture.d.ts +151 -19
- package/dist/types/rsc/shell-serve.d.ts +140 -17
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +17 -0
- package/dist/types/server/context.d.ts +139 -15
- package/dist/types/server/cookie-store.d.ts +21 -0
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +182 -53
- package/dist/types/ssr/index.d.ts +24 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- package/dist/types/testing/generated-routes.d.ts +2 -2
- package/dist/types/testing/internal/context.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +32 -6
- package/dist/types/testing/run-loader.d.ts +18 -3
- package/dist/types/testing/serve-shell-request.d.ts +132 -0
- package/dist/types/testing/shell-status.d.ts +23 -10
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
- package/dist/types/testing/vitest.d.ts +35 -1
- package/dist/types/theme/ThemeScript.d.ts +3 -3
- package/dist/types/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +28 -1
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/pattern-types.d.ts +22 -18
- package/dist/types/urls/response-types.d.ts +3 -12
- package/dist/types/use-loader.d.ts +12 -3
- package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
- package/dist/types/vite/rango.d.ts +1 -1
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1420 -1348
- package/package.json +2 -2
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +75 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +306 -124
- package/skills/caching/SKILL.md +530 -98
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +65 -38
- package/skills/cloudflare/SKILL.md +87 -20
- package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
- package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
- package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
- package/skills/comparison/SKILL.md +5 -0
- package/skills/comparison/references/framework-comparison.md +75 -57
- package/skills/composability/SKILL.md +38 -29
- package/skills/css/SKILL.md +10 -13
- package/skills/debug-manifest/SKILL.md +74 -31
- package/skills/defer-hydration/SKILL.md +52 -31
- package/skills/deployment-caching/SKILL.md +11 -3
- package/skills/document-cache/SKILL.md +98 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +55 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +23 -3
- package/skills/hooks/handle-and-actions.md +34 -17
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +39 -15
- package/skills/hooks/urls.md +7 -5
- package/skills/host-router/SKILL.md +64 -12
- package/skills/i18n/SKILL.md +60 -34
- package/skills/intercept/SKILL.md +161 -26
- package/skills/layout/SKILL.md +106 -50
- package/skills/links/SKILL.md +51 -19
- package/skills/loader/SKILL.md +386 -128
- package/skills/middleware/SKILL.md +129 -26
- package/skills/migrate-nextjs/SKILL.md +156 -70
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +51 -39
- package/skills/migrate-react-router/component-migration.md +28 -7
- package/skills/migrate-react-router/data-and-actions.md +21 -6
- package/skills/migrate-react-router/route-mapping.md +19 -10
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +92 -14
- package/skills/parallel/SKILL.md +88 -47
- package/skills/ppr/SKILL.md +866 -379
- package/skills/prerender/SKILL.md +210 -108
- package/skills/rango/SKILL.md +115 -65
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +170 -60
- package/skills/router-setup/SKILL.md +420 -240
- package/skills/scripts/SKILL.md +39 -21
- package/skills/server-actions/SKILL.md +231 -59
- package/skills/shell-manifest/SKILL.md +28 -14
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +73 -33
- package/skills/testing/cache-prerender.md +60 -33
- package/skills/testing/client-components.md +59 -6
- package/skills/testing/e2e-parity.md +12 -2
- package/skills/testing/flight.md +19 -11
- package/skills/testing/handles.md +8 -5
- package/skills/testing/loader.md +26 -6
- package/skills/testing/middleware.md +8 -3
- package/skills/testing/predicates.md +120 -0
- package/skills/testing/render-handler.md +70 -22
- package/skills/testing/response-routes.md +11 -4
- package/skills/testing/reverse-and-types.md +20 -14
- package/skills/testing/serve-shell-request.md +188 -0
- package/skills/testing/server-actions.md +12 -3
- package/skills/testing/server-tree.md +9 -0
- package/skills/testing/setup.md +9 -5
- package/skills/theme/SKILL.md +155 -30
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +137 -50
- package/skills/typesafety/generated-files-and-cli.md +26 -14
- package/skills/typesafety/params-and-search.md +44 -19
- package/skills/typesafety/route-types.md +50 -14
- package/skills/use-cache/SKILL.md +224 -83
- package/skills/vercel/SKILL.md +35 -13
- package/skills/view-transitions/SKILL.md +42 -21
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/history-state.ts +23 -0
- package/src/browser/index.ts +2 -2
- package/src/browser/navigation-bridge.ts +3 -4
- package/src/browser/navigation-transaction.ts +22 -2
- package/src/browser/partial-update.ts +35 -25
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +6 -1
- package/src/browser/scroll-restoration.ts +53 -6
- package/src/cache/cache-exec-scope.ts +37 -4
- package/src/cache/cache-key-utils.ts +23 -8
- package/src/cache/cache-policy.ts +22 -0
- package/src/cache/cache-runtime.ts +264 -68
- package/src/cache/cache-scope.ts +419 -43
- package/src/cache/cache-tag.ts +375 -11
- package/src/cache/cf/cf-base64.ts +36 -4
- package/src/cache/cf/cf-cache-constants.ts +12 -0
- package/src/cache/cf/cf-cache-store.ts +990 -348
- package/src/cache/cf/cf-cache-types.ts +32 -4
- package/src/cache/cf/cf-shell-frame.ts +258 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +133 -28
- package/src/cache/index.ts +2 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +217 -27
- package/src/cache/tag-invalidation.ts +108 -22
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +225 -44
- package/src/cache/vercel/vercel-cache-store.ts +545 -61
- package/src/client.rsc.tsx +10 -9
- package/src/client.tsx +5 -4
- package/src/components/DefaultDocument.tsx +7 -2
- package/src/context-var.ts +6 -5
- package/src/escape-script.ts +1 -1
- package/src/handles/MetaTags.tsx +13 -11
- package/src/handles/Scripts.tsx +11 -10
- package/src/handles/meta.ts +2 -2
- package/src/handles/script.ts +17 -16
- package/src/href-client.ts +1 -1
- package/src/html.ts +12 -0
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +1 -0
- package/src/loader-store.ts +7 -4
- package/src/prerender/build-shell-capture.ts +73 -22
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +121 -33
- package/src/route-definition/helpers-types.ts +33 -9
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +0 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +71 -28
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +141 -43
- package/src/router/loader-resolution.ts +115 -39
- package/src/router/match-api.ts +40 -26
- package/src/router/match-handlers.ts +5 -0
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +110 -10
- package/src/router/match-middleware/cache-store.ts +48 -101
- package/src/router/middleware-types.ts +9 -6
- package/src/router/middleware.ts +75 -13
- package/src/router/prerender-match.ts +59 -19
- package/src/router/route-snapshot.ts +11 -1
- package/src/router/router-context.ts +2 -0
- package/src/router/router-interfaces.ts +25 -2
- package/src/router/router-options.ts +8 -5
- package/src/router/segment-resolution/fresh.ts +62 -21
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +407 -76
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +208 -56
- package/src/router/segment-resolution/mask-nested.ts +138 -44
- package/src/router/segment-resolution/revalidation.ts +61 -16
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/full-payload.ts +25 -1
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-cache-serve.ts +9 -6
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +608 -225
- package/src/rsc/shell-build-manifest.ts +27 -9
- package/src/rsc/shell-capture-constants.ts +73 -6
- package/src/rsc/shell-capture.ts +1068 -296
- package/src/rsc/shell-serve.ts +316 -34
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +18 -0
- package/src/server/context.ts +300 -32
- package/src/server/cookie-store.ts +87 -61
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +297 -67
- package/src/ssr/index.tsx +136 -44
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/generated-routes.ts +2 -2
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/internal/flight-client-globals.ts +7 -5
- package/src/testing/render-route.tsx +85 -24
- package/src/testing/run-loader.ts +68 -6
- package/src/testing/serve-shell-request.ts +378 -0
- package/src/testing/shell-status.ts +34 -12
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +28 -1
- package/src/types/loader-types.ts +8 -6
- package/src/urls/pattern-types.ts +22 -18
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +12 -3
- package/src/vite/discovery/shell-prerender-phase.ts +9 -0
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
- package/src/vite/router-discovery.ts +14 -4
package/skills/hooks/SKILL.md
CHANGED
|
@@ -6,8 +6,14 @@ argument-hint: [hook-name]
|
|
|
6
6
|
|
|
7
7
|
# Client-Side React Hooks
|
|
8
8
|
|
|
9
|
+
The client-side API for reading router state in client components: navigation
|
|
10
|
+
state and actions, URL and params, loader data, handles, action status,
|
|
11
|
+
location state, outlets, and client-cache invalidation.
|
|
12
|
+
|
|
9
13
|
Import the hooks and components in this skill from `@rangojs/router/client`.
|
|
10
|
-
The root `@rangojs/router` entrypoint is for server/RSC APIs and shared types
|
|
14
|
+
The root `@rangojs/router` entrypoint is for server/RSC APIs and shared types;
|
|
15
|
+
the exceptions used here are `invalidateClientCache()` (root only) and
|
|
16
|
+
`createLocationState()` (exported from both).
|
|
11
17
|
|
|
12
18
|
## Not this skill if…
|
|
13
19
|
|
|
@@ -16,6 +22,8 @@ The root `@rangojs/router` entrypoint is for server/RSC APIs and shared types.
|
|
|
16
22
|
resolved.
|
|
17
23
|
- You want to control when a loader re-runs after an action — that is
|
|
18
24
|
`revalidate()` in the server DSL: see `/loader`.
|
|
25
|
+
- You want an action to leave the client cache alone — that is
|
|
26
|
+
`keepClientCache()` inside the action: see `/server-actions`.
|
|
19
27
|
|
|
20
28
|
Each hook's full API and recipe lives in a companion file linked below. Read
|
|
21
29
|
the one for your case.
|
|
@@ -65,22 +73,22 @@ the one for your case.
|
|
|
65
73
|
|
|
66
74
|
## Hook Summary
|
|
67
75
|
|
|
68
|
-
| Hook | Purpose | Returns
|
|
69
|
-
| ------------------------- | -------------------------------------------------------------- |
|
|
70
|
-
| `useParams()` | Route params | `Readonly<T>` (default `Record<string, string>`) or selected value |
|
|
71
|
-
| `usePathname()` | Current pathname | `string`
|
|
72
|
-
| `useSearchParams()` | URL search params (read + write) | `[ReadonlyURLSearchParams, SetSearchParams]` (RR-style tuple)
|
|
73
|
-
| `useHref()` | Mount-aware href | `(path) => string`
|
|
74
|
-
| `useMount()` | Current include() mount path | `string`
|
|
75
|
-
| `useReverse()` | Local reverse for imported routes | `(name, params?, search?) => string`
|
|
76
|
-
| `useNavigation()` | Reactive navigation state | state, location, isStreaming
|
|
77
|
-
| `useRouter()` | Stable router actions | push, replace, refresh, prefetch, back, forward
|
|
78
|
-
| `useSegments()` | URL path & segment IDs | path, segmentIds, location
|
|
79
|
-
| `useLinkStatus()` | Link pending state | { pending }
|
|
80
|
-
| `useLoader()` | Loader data (strict) | data, isLoading, error, load, refetch
|
|
81
|
-
| `useFetchLoader()` | Loader with on-demand fetch | data, load, isLoading, error, refetch
|
|
82
|
-
| `useRefreshLoaders()` | Refresh cross-loader group(s) | `() => (groups: string \| string[]) => Promise<void>`
|
|
83
|
-
| `useHandle()` | Accumulated handle data | T (handle type)
|
|
84
|
-
| `useAction()` | Server action state | state, error, result
|
|
85
|
-
| `useLocationState()` | History state (persists or flash) | T \| undefined
|
|
86
|
-
| `invalidateClientCache()` | Force client caches to miss (function, not a hook; root entry) | `void`
|
|
76
|
+
| Hook | Purpose | Returns |
|
|
77
|
+
| ------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
78
|
+
| `useParams()` | Route params | `Readonly<T>` (default `Record<string, string \| undefined>`) or selected value |
|
|
79
|
+
| `usePathname()` | Current pathname | `string` |
|
|
80
|
+
| `useSearchParams()` | URL search params (read + write) | `[ReadonlyURLSearchParams, SetSearchParams]` (RR-style tuple) |
|
|
81
|
+
| `useHref()` | Mount-aware href | `(path) => string` |
|
|
82
|
+
| `useMount()` | Current include() mount path | `string` |
|
|
83
|
+
| `useReverse()` | Local reverse for imported routes | `(name, params?, search?) => string` |
|
|
84
|
+
| `useNavigation()` | Reactive navigation state | state, location, pendingUrl, isStreaming |
|
|
85
|
+
| `useRouter()` | Stable router actions | push, replace, refresh, prefetch, back, forward |
|
|
86
|
+
| `useSegments()` | URL path & segment IDs | path, segmentIds, location |
|
|
87
|
+
| `useLinkStatus()` | Link pending state | { pending } |
|
|
88
|
+
| `useLoader()` | Loader data (strict) | data, isLoading, error, load, refetch |
|
|
89
|
+
| `useFetchLoader()` | Loader with on-demand fetch | data, load, isLoading, error, refetch |
|
|
90
|
+
| `useRefreshLoaders()` | Refresh cross-loader group(s) | `() => (groups: string \| string[]) => Promise<void>` |
|
|
91
|
+
| `useHandle()` | Accumulated handle data | T (handle type) |
|
|
92
|
+
| `useAction()` | Server action state | state, actionId, payload, error, result |
|
|
93
|
+
| `useLocationState()` | History state (persists or flash) | T \| undefined |
|
|
94
|
+
| `invalidateClientCache()` | Force client caches to miss (function, not a hook; root entry) | `void` |
|
package/skills/hooks/data.md
CHANGED
|
@@ -36,9 +36,15 @@ same-route nav, a same-structure search/filter nav), the reader that is still
|
|
|
36
36
|
showing the OLD data reports `isLoading: true` from
|
|
37
37
|
the moment the new tree is committed until the new data lands — render your
|
|
38
38
|
stale indicator from it. The flag never flashes back to `false` on the old
|
|
39
|
-
data: it flips in the same commit that swaps `data`.
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
data: it flips in the same commit that swaps `data`. The pin is per loader
|
|
40
|
+
family (`loader.$$id`), not per segment: a navigation that re-runs
|
|
41
|
+
`CartLoader` flags every `useLoader(CartLoader)`, including a persisting
|
|
42
|
+
layout reader whose own copy is not replaced. That is "this loader is in
|
|
43
|
+
flight," not "this segment's copy is being replaced." A _different_ layout
|
|
44
|
+
loader the server does not re-execute stays `isLoading: false`. A child can
|
|
45
|
+
already read a layout registration via the context walk; a second
|
|
46
|
+
`loader(Same)` on the child is a second run of the same family, not a
|
|
47
|
+
separate loading flag. It never applies to a
|
|
42
48
|
navigation that remounts the route (that reader is gone; the skeleton shows),
|
|
43
49
|
to a fully-prefetched nav (its data is already settled when it commits, so
|
|
44
50
|
nothing is pending to flag), or to an ephemeral `useFetchLoader` read outside
|
|
@@ -51,8 +57,19 @@ function ProductPrice() {
|
|
|
51
57
|
}
|
|
52
58
|
```
|
|
53
59
|
|
|
60
|
+
Unit-test the indicator with `renderRoute` (`/testing`, client-components.md
|
|
61
|
+
"Held navigation"): a `transition` spec plus a pending seed in
|
|
62
|
+
`router.navigate(url, { loaders })`.
|
|
63
|
+
|
|
54
64
|
**Precondition**: Loader must be registered on route via `loader()` helper.
|
|
55
65
|
|
|
66
|
+
**`load()` / `refetch` need a fetchable loader.** `load()`, `refetch`, and
|
|
67
|
+
`useRefreshLoaders()` call the loader through the `_rsc_loader` endpoint, which
|
|
68
|
+
only serves loaders created with `createLoader(fn, true)` or
|
|
69
|
+
`createLoader(fn, { middleware })`. A plain `createLoader(fn)` answers 403 there;
|
|
70
|
+
it re-runs only as part of a render (navigation, `router.refresh()`, or the
|
|
71
|
+
revalidation after a server action). See `/loader` → "Fetchable Loaders".
|
|
72
|
+
|
|
56
73
|
Loaders can also be passed as props from server to client components:
|
|
57
74
|
|
|
58
75
|
```tsx
|
|
@@ -108,6 +125,7 @@ reading the same loader id. Layout, page, and parallel-slot reads
|
|
|
108
125
|
all converge on the new value:
|
|
109
126
|
|
|
110
127
|
```tsx
|
|
128
|
+
// CartLoader = createLoader(fn, true), registered with loader(CartLoader).
|
|
111
129
|
// Layout button calls load() — the page read below sees the update too.
|
|
112
130
|
function Layout() {
|
|
113
131
|
const { data, load } = useLoader(CartLoader);
|
|
@@ -262,9 +280,11 @@ server, JSON bodies are available via `ctx.body` and FormData bodies via `ctx.fo
|
|
|
262
280
|
|
|
263
281
|
```tsx
|
|
264
282
|
"use client";
|
|
283
|
+
import { useRef } from "react";
|
|
265
284
|
import { useFetchLoader } from "@rangojs/router/client";
|
|
266
285
|
import { FileUploadLoader } from "../loaders/upload";
|
|
267
286
|
|
|
287
|
+
// Needs JavaScript: the form action is a client function, not a server action.
|
|
268
288
|
function FileUploader() {
|
|
269
289
|
const { data, load, isLoading } = useFetchLoader(FileUploadLoader);
|
|
270
290
|
const formRef = useRef<HTMLFormElement>(null);
|
|
@@ -34,18 +34,24 @@ Writers: handlers AND loader bodies — `ctx.use(Handle)` returns the push
|
|
|
34
34
|
function in each (middleware has no handle APIs). Loader pushes follow a delivery race (settle before
|
|
35
35
|
the handler barrier → in the SSR'd document; later → applied client-side), and
|
|
36
36
|
`loader(Def, { ssr: false })` makes them deterministic in the
|
|
37
|
-
document.
|
|
37
|
+
document. On a `ppr` shell HIT, a loader without `ssr: false` never ran during
|
|
38
|
+
the capture, so its pushes always apply client-side; with `ssr: false` they
|
|
39
|
+
bake into the shell. See `/loader` → "Writing Handles from Loaders". `useHandle`
|
|
38
40
|
re-renders on every handle update, late loader pushes included.
|
|
39
41
|
|
|
40
42
|
Handles can be passed as props from server to client components:
|
|
41
43
|
|
|
42
44
|
```tsx
|
|
43
|
-
// Server component
|
|
44
|
-
path(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
45
|
+
// Server component (route handler)
|
|
46
|
+
path(
|
|
47
|
+
"/dashboard",
|
|
48
|
+
(ctx) => {
|
|
49
|
+
const push = ctx.use(Breadcrumbs);
|
|
50
|
+
push({ label: "Dashboard", href: "/dashboard" });
|
|
51
|
+
return <DashboardNav handle={Breadcrumbs} />;
|
|
52
|
+
},
|
|
53
|
+
{ name: "dashboard" },
|
|
54
|
+
);
|
|
49
55
|
```
|
|
50
56
|
|
|
51
57
|
```tsx
|
|
@@ -58,7 +64,9 @@ function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
|
|
|
58
64
|
return (
|
|
59
65
|
<nav>
|
|
60
66
|
{crumbs.map((c) => (
|
|
61
|
-
<a
|
|
67
|
+
<a key={c.href} href={c.href}>
|
|
68
|
+
{c.label}
|
|
69
|
+
</a>
|
|
62
70
|
))}
|
|
63
71
|
</nav>
|
|
64
72
|
);
|
|
@@ -78,33 +86,42 @@ tracking actions called outside a `<form action={...}>` flow.
|
|
|
78
86
|
|
|
79
87
|
### useAction()
|
|
80
88
|
|
|
81
|
-
Track
|
|
89
|
+
Track the lifecycle of a server action, wherever on the page it was invoked.
|
|
90
|
+
If the same action fires several times, the hook reports the latest call.
|
|
82
91
|
|
|
83
92
|
```tsx
|
|
84
93
|
"use client";
|
|
85
94
|
import { useAction } from "@rangojs/router/client";
|
|
86
|
-
import { addToCart } from "../actions/cart";
|
|
95
|
+
import { addToCart } from "../actions/cart"; // addToCart(productId: string)
|
|
87
96
|
|
|
88
97
|
function AddToCartButton({ productId }: { productId: string }) {
|
|
89
98
|
const { state, error, result } = useAction(addToCart);
|
|
90
99
|
|
|
91
100
|
// state: 'idle' | 'loading' | 'streaming'
|
|
92
101
|
// actionId: string | null
|
|
93
|
-
// payload: unknown | null (
|
|
94
|
-
// error:
|
|
102
|
+
// payload: unknown[] | FormData | null (the call's arguments)
|
|
103
|
+
// error: unknown | null (whatever the action threw)
|
|
95
104
|
// result: unknown | null (return value)
|
|
96
105
|
|
|
97
106
|
return (
|
|
98
|
-
|
|
99
|
-
|
|
107
|
+
// .bind keeps the form progressively enhanced; useAction still matches it
|
|
108
|
+
<form action={addToCart.bind(null, productId)}>
|
|
100
109
|
<button disabled={state === "loading"}>
|
|
101
110
|
{state === "loading" ? "Adding..." : "Add to Cart"}
|
|
102
111
|
</button>
|
|
103
|
-
{error && <p className="error">{error.message}</p>}
|
|
112
|
+
{error instanceof Error && <p className="error">{error.message}</p>}
|
|
104
113
|
</form>
|
|
105
114
|
);
|
|
106
115
|
}
|
|
107
116
|
|
|
108
|
-
//
|
|
109
|
-
const isLoading = useAction(
|
|
117
|
+
// With a selector (re-renders only when the selected value changes)
|
|
118
|
+
const isLoading = useAction(addToCart, (s) => s.state === "loading");
|
|
119
|
+
|
|
120
|
+
// Match by export name (suffix match on "#addToCart"; ambiguous if two
|
|
121
|
+
// "use server" files export the same name)
|
|
122
|
+
const isAdding = useAction("addToCart", (s) => s.state === "loading");
|
|
110
123
|
```
|
|
124
|
+
|
|
125
|
+
Pass an action imported directly into the client component, or its export
|
|
126
|
+
name as a string. An action received as a prop from a server component has
|
|
127
|
+
lost the metadata `useAction` needs and throws; use the string form there.
|
|
@@ -13,7 +13,7 @@ function NavIndicator() {
|
|
|
13
13
|
|
|
14
14
|
// State properties
|
|
15
15
|
nav.state; // 'idle' | 'loading'
|
|
16
|
-
nav.isStreaming; // boolean
|
|
16
|
+
nav.isStreaming; // boolean — RSC data is still streaming
|
|
17
17
|
nav.location; // Current URL
|
|
18
18
|
nav.pendingUrl; // Target URL during navigation (or null)
|
|
19
19
|
|
|
@@ -29,7 +29,9 @@ function IsLoading() {
|
|
|
29
29
|
|
|
30
30
|
### useRouter()
|
|
31
31
|
|
|
32
|
-
Access stable router actions
|
|
32
|
+
Access stable router actions. The returned object never changes identity, so
|
|
33
|
+
components using it do not re-render on navigation. Call its methods from
|
|
34
|
+
event handlers or effects:
|
|
33
35
|
|
|
34
36
|
```tsx
|
|
35
37
|
"use client";
|
|
@@ -38,15 +40,41 @@ import { useRouter } from "@rangojs/router/client";
|
|
|
38
40
|
function NavigationControls() {
|
|
39
41
|
const router = useRouter();
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
<button onClick={() => router.push("/products")}>Products</button>
|
|
46
|
+
<button onClick={() => router.replace("/login", { scroll: false })}>
|
|
47
|
+
Log in
|
|
48
|
+
</button>
|
|
49
|
+
<button onClick={() => router.refresh()}>Reload data</button>
|
|
50
|
+
<button onMouseEnter={() => router.prefetch("/dashboard")}>
|
|
51
|
+
Dashboard
|
|
52
|
+
</button>
|
|
53
|
+
<button onClick={() => router.back()}>Back</button>
|
|
54
|
+
<button onClick={() => router.forward()}>Forward</button>
|
|
55
|
+
</>
|
|
56
|
+
);
|
|
47
57
|
}
|
|
48
58
|
```
|
|
49
59
|
|
|
60
|
+
| Method | Behaviour |
|
|
61
|
+
| ------------------------ | --------------------------------------------------------------------------------------------------------------- |
|
|
62
|
+
| `push(url, options?)` | Navigate and add a history entry. Returns a promise. |
|
|
63
|
+
| `replace(url, options?)` | Navigate and replace the current history entry. Returns a promise. |
|
|
64
|
+
| `refresh()` | Re-fetch the current route's server data, keeping client state. Returns a promise. |
|
|
65
|
+
| `prefetch(url, opts?)` | Prefetch a target. `{ key: ":source" }` scopes the entry to the source page (parity with `<Link prefetchKey>`). |
|
|
66
|
+
| `back()` | `history.back()`; on the first entry of the session it replaces to the app root instead of leaving the app. |
|
|
67
|
+
| `forward()` | `history.forward()`. |
|
|
68
|
+
|
|
69
|
+
`push` / `replace` options: `scroll` (`false` keeps the current scroll
|
|
70
|
+
position), `revalidate` (default `true`, see below), and `state` (location
|
|
71
|
+
state, see [`./state.md`](./state.md)).
|
|
72
|
+
|
|
73
|
+
Target resolution: a path starting with `/` is app-absolute and gets the
|
|
74
|
+
router `basename` prefixed. A relative path (`"cart"`, `"./cart"`) resolves
|
|
75
|
+
against the current `include()` mount, so `router.push("cart")` inside
|
|
76
|
+
`include("/shop", ...)` navigates to `/shop/cart`.
|
|
77
|
+
|
|
50
78
|
#### Skipping revalidation
|
|
51
79
|
|
|
52
80
|
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.
|
|
@@ -79,7 +107,7 @@ function Breadcrumbs() {
|
|
|
79
107
|
const { path, segmentIds, location } = useSegments();
|
|
80
108
|
|
|
81
109
|
// path: ["shop", "products", "123"] (split on "/", no leading slash on any element)
|
|
82
|
-
// segmentIds: ["L0", "L0L1", "L0L1R0"] (opaque internal short-codes, not route names)
|
|
110
|
+
// segmentIds: ["L0", "L0L1", "L0L1R0"] (layouts and routes only; opaque internal short-codes, not route names)
|
|
83
111
|
// location: URL object
|
|
84
112
|
|
|
85
113
|
return <nav>{path.join(" > ")}</nav>;
|
|
@@ -91,7 +119,8 @@ const isShopRoute = useSegments((s) => s.path[0] === "shop");
|
|
|
91
119
|
|
|
92
120
|
### useLinkStatus()
|
|
93
121
|
|
|
94
|
-
Track pending state inside a Link component
|
|
122
|
+
Track pending state inside a Link component. `pending` is `true` while a
|
|
123
|
+
navigation to this link's `to` is in flight:
|
|
95
124
|
|
|
96
125
|
```tsx
|
|
97
126
|
"use client";
|
package/skills/hooks/outlets.md
CHANGED
|
@@ -2,24 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
### Outlet / ParallelOutlet
|
|
4
4
|
|
|
5
|
-
Render child content in layouts
|
|
5
|
+
Render child content in layouts. `<Outlet />` renders the matched child
|
|
6
|
+
segment; `<ParallelOutlet name="@slot" />` renders a named `parallel()` slot.
|
|
7
|
+
Both apply the segment's `loading()` fallback as a Suspense boundary:
|
|
6
8
|
|
|
7
9
|
```tsx
|
|
8
10
|
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
9
11
|
|
|
10
|
-
function DashboardLayout(
|
|
12
|
+
function DashboardLayout() {
|
|
11
13
|
return (
|
|
12
14
|
<div className="dashboard">
|
|
13
15
|
<aside>
|
|
14
16
|
<ParallelOutlet name="@sidebar" />
|
|
15
17
|
</aside>
|
|
16
|
-
<main>
|
|
18
|
+
<main>
|
|
19
|
+
<Outlet />
|
|
20
|
+
</main>
|
|
17
21
|
<ParallelOutlet name="@notifications" />
|
|
18
22
|
</div>
|
|
19
23
|
);
|
|
20
24
|
}
|
|
21
25
|
```
|
|
22
26
|
|
|
27
|
+
`<Outlet name="@sidebar" />` is equivalent to `<ParallelOutlet name="@sidebar" />`;
|
|
28
|
+
`ParallelOutlet` requires the name and reads clearer at the call site. See
|
|
29
|
+
`/layout` and `/parallel` for the server side.
|
|
30
|
+
|
|
23
31
|
### useOutlet()
|
|
24
32
|
|
|
25
33
|
Access outlet content and its client-route presentation state programmatically:
|
|
@@ -48,9 +56,8 @@ function ConditionalLayout() {
|
|
|
48
56
|
}
|
|
49
57
|
```
|
|
50
58
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
`<Outlet />` itself is unchanged.
|
|
59
|
+
Migrating from the older API where `useOutlet()` returned the node directly:
|
|
60
|
+
destructure `content` and render it where you rendered the hook result.
|
|
54
61
|
|
|
55
62
|
`pending` is narrow. After hydration, a `clientUrls()` layout receives `true`
|
|
56
63
|
while a browser-local match to a different client route beneath it is presenting
|
package/skills/hooks/state.md
CHANGED
|
@@ -74,6 +74,16 @@ Plain state can also be evaluated just in time (also requires a client component
|
|
|
74
74
|
</Link>
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
Read plain (untyped) state by calling the hook with a type argument and no
|
|
78
|
+
definition:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
const state = useLocationState<{ from?: string }>(); // { from?: string } | undefined
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The same `state` option exists on `router.push()` / `router.replace()` (see
|
|
85
|
+
[`./navigation.md`](./navigation.md)).
|
|
86
|
+
|
|
77
87
|
### Flash State (read-once)
|
|
78
88
|
|
|
79
89
|
Create a location state with `{ flash: true }` for read-once state that
|
|
@@ -106,22 +116,22 @@ function FlashBanner() {
|
|
|
106
116
|
```
|
|
107
117
|
|
|
108
118
|
Flash behavior is determined by the definition (`{ flash: true }`), not by which
|
|
109
|
-
hook reads it. `useLocationState` reads the value
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
119
|
+
hook reads it. `useLocationState` reads the value during render (on the
|
|
120
|
+
hydration render it returns `undefined` and reads in a post-mount effect, so
|
|
121
|
+
SSR and hydration agree), then clears it from `history.state` via
|
|
122
|
+
`replaceState` in a `useEffect`. Multiple components reading the same flash
|
|
123
|
+
definition all see the value. Pressing back/forward will not re-show the flash
|
|
124
|
+
since it was cleared.
|
|
113
125
|
|
|
114
|
-
Set flash state from the server via `redirect()` with state
|
|
126
|
+
Set flash state from the server via `redirect()` with state (from a handler,
|
|
127
|
+
middleware, or server action). Import the definition from the shared module so
|
|
128
|
+
the client reader uses the same one:
|
|
115
129
|
|
|
116
130
|
```tsx
|
|
117
131
|
// In a route handler
|
|
118
|
-
import { redirect
|
|
119
|
-
|
|
120
|
-
export const FlashMessage = createLocationState<{ text: string }>({
|
|
121
|
-
flash: true,
|
|
122
|
-
});
|
|
132
|
+
import { redirect } from "@rangojs/router";
|
|
133
|
+
import { FlashMessage } from "../location-states";
|
|
123
134
|
|
|
124
|
-
// Handler
|
|
125
135
|
(ctx) => {
|
|
126
136
|
return redirect("/dashboard", {
|
|
127
137
|
state: [FlashMessage({ text: "Item saved!" })],
|
|
@@ -129,7 +139,7 @@ export const FlashMessage = createLocationState<{ text: string }>({
|
|
|
129
139
|
};
|
|
130
140
|
```
|
|
131
141
|
|
|
132
|
-
Or via `ctx.setLocationState()` on any response:
|
|
142
|
+
Or via `ctx.setLocationState()` on any response (handlers and middleware):
|
|
133
143
|
|
|
134
144
|
```tsx
|
|
135
145
|
(ctx) => {
|
|
@@ -194,9 +204,23 @@ ProductState.delete();
|
|
|
194
204
|
Force the client's caches to miss after a mutation the router can't see (a REST
|
|
195
205
|
call, a WebSocket push, a login). It is a plain function, not a hook, so it works
|
|
196
206
|
from module-level callbacks too. Imported from the root entry `@rangojs/router`,
|
|
197
|
-
it is selected by export conditions:
|
|
198
|
-
|
|
199
|
-
|
|
207
|
+
it is selected by export conditions:
|
|
208
|
+
|
|
209
|
+
- **Client** (client component or module): marks the history cache stale,
|
|
210
|
+
flushes the prefetch cache, rotates the router state, and notifies other tabs
|
|
211
|
+
— the same thing a completed server action does. During SSR of a client
|
|
212
|
+
component it is a no-op (dev warning).
|
|
213
|
+
- **Server** (handler, server component, middleware, loader, or action): writes
|
|
214
|
+
a rotated state `Set-Cookie` for the responding client, which marks its
|
|
215
|
+
caches stale when it next reads them. Idempotent within a request; throws
|
|
216
|
+
inside a `cache()` / `"use cache"` boundary; a no-op with a dev warning
|
|
217
|
+
outside a request. On a `ppr` route a shell HIT runs no handler, so a call
|
|
218
|
+
from handler code writes the cookie only on the renders that run it (a
|
|
219
|
+
MISS); call it from middleware, which runs on every request, HITs included.
|
|
220
|
+
|
|
221
|
+
Server actions already invalidate automatically; to suppress that for a no-op
|
|
222
|
+
action, call `keepClientCache()` inside it (see `/server-actions` → "Client
|
|
223
|
+
Cache After an Action").
|
|
200
224
|
|
|
201
225
|
```tsx
|
|
202
226
|
"use client";
|
package/skills/hooks/urls.md
CHANGED
|
@@ -29,7 +29,7 @@ function ProductId() {
|
|
|
29
29
|
}
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Returns merged params from all matched route segments as a `Readonly<T>` map. Updates on navigation commit
|
|
32
|
+
Returns merged params from all matched route segments as a `Readonly<T>` map (default `Record<string, string | undefined>` — absent optional params are omitted, so guard them). Updates on navigation commit, not during a pending navigation — except inside an optimistically rendered `clientUrls()` destination, where it reports that destination's params (see `/client-urls`).
|
|
33
33
|
|
|
34
34
|
### usePathname()
|
|
35
35
|
|
|
@@ -119,19 +119,21 @@ import { useHref, href, Link } from "@rangojs/router/client";
|
|
|
119
119
|
|
|
120
120
|
// Inside include("/shop", shopPatterns)
|
|
121
121
|
function ShopNav() {
|
|
122
|
-
const
|
|
122
|
+
const localHref = useHref();
|
|
123
123
|
|
|
124
124
|
return (
|
|
125
125
|
<>
|
|
126
126
|
{/* Local paths - auto-prefixed with /shop */}
|
|
127
|
-
<Link to={
|
|
128
|
-
<Link to={
|
|
127
|
+
<Link to={localHref("/cart")}>Cart</Link>
|
|
128
|
+
<Link to={localHref("/product/widget")}>Widget</Link>
|
|
129
|
+
{/* Absolute path - not prefixed */}
|
|
130
|
+
<Link to={href("/about")}>About</Link>
|
|
129
131
|
</>
|
|
130
132
|
);
|
|
131
133
|
}
|
|
132
134
|
```
|
|
133
135
|
|
|
134
|
-
Use `useHref()` for local navigation. Use the bare `href()` function for absolute paths.
|
|
136
|
+
Use `useHref()` for local navigation. Use the bare `href()` function for absolute paths. The function `useHref()` returns is referentially stable within a mount, so it is safe as a hook dependency or memoized prop.
|
|
135
137
|
|
|
136
138
|
### useMount()
|
|
137
139
|
|
|
@@ -8,6 +8,12 @@ argument-hint:
|
|
|
8
8
|
|
|
9
9
|
Route requests to different apps based on domain, subdomain, or path prefix patterns. Supports middleware, lazy loading, cookie-based host override for dev, and a fallback handler.
|
|
10
10
|
|
|
11
|
+
Use it when one deployment serves several independently built Rango apps (or
|
|
12
|
+
plain request handlers) and the hostname or a path prefix decides which one
|
|
13
|
+
handles the request. Each sub-app is an ordinary `createRouter()` app; the host
|
|
14
|
+
router only picks which one runs. For a single app mounted under a sub-path,
|
|
15
|
+
use `basename` instead (`/router-setup`).
|
|
16
|
+
|
|
11
17
|
## Import
|
|
12
18
|
|
|
13
19
|
```typescript
|
|
@@ -64,6 +70,10 @@ import { router } from "./router.js";
|
|
|
64
70
|
export default (request: Request, input: any) => router.fetch(request, input);
|
|
65
71
|
```
|
|
66
72
|
|
|
73
|
+
Each sub-app has its own generated route map and `Rango.Env`; typecheck each
|
|
74
|
+
in its own TypeScript program (or give route names per-app prefixes) so the
|
|
75
|
+
global augmentations don't collide — see `/typesafety` (multi-project setup).
|
|
76
|
+
|
|
67
77
|
Selecting the host entry — a host app has several `createRouter()` sub-apps, so single-router auto-discovery can't pick one. Either let rango auto-detect the lone `createHostRouter()` file, or point at it explicitly:
|
|
68
78
|
|
|
69
79
|
```typescript
|
|
@@ -71,7 +81,7 @@ Selecting the host entry — a host app has several `createRouter()` sub-apps, s
|
|
|
71
81
|
rango({ preset: "vercel", hostRouter: "./src/worker.rsc.tsx" });
|
|
72
82
|
```
|
|
73
83
|
|
|
74
|
-
On Vercel this is a single function running `hostRouter.match()` for every request (mirrors the Cloudflare single-worker model); `{ env, ctx }` (`process.env` + `{ waitUntil }`) is threaded unchanged to each matched sub-app's handler and `cache(env, ctx)` factory. See
|
|
84
|
+
On Vercel this is a single function running `hostRouter.match()` for every request (mirrors the Cloudflare single-worker model); `{ env, ctx }` (`process.env` + `{ waitUntil }`) is threaded unchanged to each matched sub-app's handler and `cache(env, ctx)` factory. See `/vercel`.
|
|
75
85
|
|
|
76
86
|
Unmatched hosts on node/vercel: because rango owns the generated entry (you have no worker `try/catch`), it catches `NoRouteMatchError` and returns **404** by default — so you do **not** need a catch-all host route. If you want different behavior (a branded 404, a redirect, a default app), register a catch-all mount as the **last** route, e.g. `host(["**"]).lazy(() => import("./apps/site/handler.js"))` — it matches any host, so the built-in 404 only fires when nothing matched at all. (Note `fallback()` is for cookie-override errors, not general unmatched hosts.)
|
|
77
87
|
|
|
@@ -120,6 +130,18 @@ Why two methods instead of one overloaded `.map()`:
|
|
|
120
130
|
|
|
121
131
|
Patterns are tested in registration order. First match wins.
|
|
122
132
|
|
|
133
|
+
Matching details:
|
|
134
|
+
|
|
135
|
+
- **Apex means exactly two labels.** There is no Public Suffix List:
|
|
136
|
+
`example.co.uk` has three labels, so `.`/`*` do not match it and `*.` does.
|
|
137
|
+
`localhost` has one label, so it matches neither `.` nor `*.`; use an exact
|
|
138
|
+
`localhost` pattern (or `**`) for local dev.
|
|
139
|
+
- **Hosts are case-insensitive; paths are not.** A path prefix matches the
|
|
140
|
+
exact path or anything below it (`example.com/api` matches `"/api"` and
|
|
141
|
+
`"/api/x"`, not `"/apix"`). A trailing `/` on the pattern is ignored.
|
|
142
|
+
- A pattern that is empty, not a string, or contains whitespace throws
|
|
143
|
+
`InvalidPatternError` at registration.
|
|
144
|
+
|
|
123
145
|
## `defineHosts` for Type Safety
|
|
124
146
|
|
|
125
147
|
```typescript
|
|
@@ -142,6 +164,8 @@ Returns a frozen object — keys are autocompleted by TypeScript.
|
|
|
142
164
|
Global middleware runs for every matched route. Per-route middleware runs only for that host pattern.
|
|
143
165
|
|
|
144
166
|
```typescript
|
|
167
|
+
import { createHostRouter, type Middleware } from "@rangojs/router/host";
|
|
168
|
+
|
|
145
169
|
const router = createHostRouter();
|
|
146
170
|
|
|
147
171
|
// Global — runs for all routes
|
|
@@ -150,7 +174,14 @@ router.use(async (request, input, next) => {
|
|
|
150
174
|
return next();
|
|
151
175
|
});
|
|
152
176
|
|
|
153
|
-
// Per-route
|
|
177
|
+
// Per-route: return a Response without calling next() to short-circuit
|
|
178
|
+
const requireAuth: Middleware = async (request, input, next) => {
|
|
179
|
+
if (!request.headers.get("authorization")) {
|
|
180
|
+
return new Response("Unauthorized", { status: 401 });
|
|
181
|
+
}
|
|
182
|
+
return next();
|
|
183
|
+
};
|
|
184
|
+
|
|
154
185
|
router
|
|
155
186
|
.host(["admin.*"])
|
|
156
187
|
.use(requireAuth)
|
|
@@ -159,23 +190,37 @@ router
|
|
|
159
190
|
|
|
160
191
|
Middleware signature: `(request: Request, input: RouterRequestInput, next: () => Promise<Response>) => Promise<Response>`
|
|
161
192
|
|
|
193
|
+
Global middleware runs first, then the matched route's middleware, then the
|
|
194
|
+
handler. `input` is typed `RouterRequestInput<any>` on purpose: one host router
|
|
195
|
+
fans out to sub-apps with different env shapes, so `input.env`/`input.vars` are
|
|
196
|
+
`any` here and each sub-app types its own env via `createRouter<TEnv>()`.
|
|
197
|
+
|
|
162
198
|
Calling `next()` more than once throws.
|
|
163
199
|
|
|
164
200
|
## Fallback Handler
|
|
165
201
|
|
|
166
|
-
Handles cookie-override errors when `hostOverride` is configured (e.g., override from a disallowed host, invalid cookie hostname). The fallback does **not** catch unmatched hosts — those throw `NoRouteMatchError`. Catch that at the worker level if you need a 404.
|
|
202
|
+
Handles cookie-override errors when `hostOverride` is configured (e.g., override from a disallowed host, invalid cookie hostname, a throwing `validate`). The fallback does **not** catch unmatched hosts — those throw `NoRouteMatchError`. Catch that at the worker level if you need a 404.
|
|
167
203
|
|
|
168
204
|
```typescript
|
|
169
205
|
const router = createHostRouter({
|
|
170
206
|
hostOverride: { cookieName: "x-dev-host", allowedHosts: ["localhost"] },
|
|
171
207
|
});
|
|
172
208
|
|
|
173
|
-
// Called when cookie override fails (not for general unmatched hosts)
|
|
174
|
-
|
|
175
|
-
|
|
209
|
+
// Called when cookie override fails (not for general unmatched hosts).
|
|
210
|
+
// Global middleware and the fallback's own middleware still run.
|
|
211
|
+
router.fallback().map((request, input) => {
|
|
212
|
+
// The override error is passed on the input (not part of RouterRequestInput's type)
|
|
213
|
+
const { error } = input as { error?: Error };
|
|
214
|
+
return new Response(`Invalid host override: ${error?.message}`, {
|
|
215
|
+
status: 400,
|
|
216
|
+
});
|
|
176
217
|
});
|
|
177
218
|
```
|
|
178
219
|
|
|
220
|
+
Without a registered fallback, a failed override returns a `400` JSON
|
|
221
|
+
response (`{ error, message }`) that also clears the override cookie
|
|
222
|
+
(`Set-Cookie: <cookieName>=; Max-Age=0`).
|
|
223
|
+
|
|
179
224
|
For unmatched hosts without `hostOverride`, catch `NoRouteMatchError` in your worker fetch. Use the `isNoRouteMatchError()` guard rather than a bare `instanceof`: a workspace with a duplicated `@rangojs/router` copy can throw the error with a different class identity, and `instanceof` would then turn the 404 into an opaque 500.
|
|
180
225
|
|
|
181
226
|
```typescript
|
|
@@ -215,10 +260,14 @@ const router = createHostRouter({
|
|
|
215
260
|
When a request arrives:
|
|
216
261
|
|
|
217
262
|
1. If no cookie → use actual hostname
|
|
218
|
-
2. If cookie present and host
|
|
219
|
-
3. If cookie present but host not allowed →
|
|
263
|
+
2. If cookie present and the actual host matches an `allowedHosts` pattern → use the cookie value as the hostname (after `validate`, when given)
|
|
264
|
+
3. If cookie present but the host is not allowed → `HostOverrideNotAllowedError`, handled by the fallback (see above)
|
|
220
265
|
|
|
221
|
-
Without a custom `validate`, the cookie value
|
|
266
|
+
Without a custom `validate`, the cookie value must parse as a bare hostname via
|
|
267
|
+
`new URL()` (no port or path) and is lowercased; otherwise
|
|
268
|
+
`InvalidHostnameError`. A custom `validate` returns the effective hostname; if
|
|
269
|
+
it throws, the error is wrapped in `HostValidationError`. The effective
|
|
270
|
+
hostname is then matched against the host patterns as usual.
|
|
222
271
|
|
|
223
272
|
## Debug Mode
|
|
224
273
|
|
|
@@ -251,12 +300,15 @@ matchesHost(
|
|
|
251
300
|
new Request("https://foo.workers.dev/admin"),
|
|
252
301
|
); // true
|
|
253
302
|
|
|
254
|
-
// Create requests for integration tests
|
|
303
|
+
// Create requests for integration tests (method defaults to GET, path to "/")
|
|
255
304
|
const request = createTestRequest({
|
|
256
305
|
host: "admin.example.com",
|
|
257
306
|
path: "/dashboard",
|
|
307
|
+
method: "POST",
|
|
308
|
+
headers: { authorization: "Bearer test" },
|
|
258
309
|
cookies: { "x-dev-host": "api.example.com" },
|
|
259
310
|
});
|
|
311
|
+
const response = await router.match(request);
|
|
260
312
|
|
|
261
313
|
// Test which route would match (without executing)
|
|
262
314
|
router.test("admin.example.com"); // { pattern, handler, kind } | null
|
|
@@ -309,8 +361,8 @@ document navigation**, not a soft in-tree swap. When the server sees a partial
|
|
|
309
361
|
Why a reload rather than a soft swap: a soft swap can't faithfully re-establish
|
|
310
362
|
the target app's **document-level** state. Stylesheets shared across apps are
|
|
311
363
|
dropped by React 19's by-`href` resource dedup; and theme, warmup, and
|
|
312
|
-
prefetch-TTL are document-lifetime (captured once at load
|
|
313
|
-
|
|
364
|
+
prefetch-TTL are document-lifetime (captured once at document load), so the
|
|
365
|
+
target app's config would never take effect. A
|
|
314
366
|
full document load re-establishes the target app's entire document — CSS, theme,
|
|
315
367
|
meta, everything — by construction. So you do **not** need to coordinate
|
|
316
368
|
stylesheet `href`s, `precedence`, theme config, etc. across independently-authored
|