@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -0
- package/README.md +126 -16
- package/dist/bin/rango.js +319 -95
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +2724 -1053
- package/package.json +68 -14
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +64 -2
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +224 -32
- package/skills/caching/SKILL.md +279 -17
- package/skills/composability/SKILL.md +27 -3
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +11 -9
- package/skills/hooks/SKILL.md +243 -29
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +68 -19
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +190 -23
- package/skills/loader/SKILL.md +235 -9
- package/skills/middleware/SKILL.md +18 -10
- package/skills/migrate-nextjs/SKILL.md +43 -19
- package/skills/migrate-react-router/SKILL.md +8 -2
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +18 -7
- package/skills/prerender/SKILL.md +65 -60
- package/skills/rango/SKILL.md +251 -24
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +115 -48
- package/skills/route/SKILL.md +46 -5
- package/skills/router-setup/SKILL.md +30 -8
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +130 -0
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +122 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/typesafety/SKILL.md +322 -29
- package/skills/use-cache/SKILL.md +57 -14
- package/skills/view-transitions/SKILL.md +337 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +192 -150
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +94 -25
- package/src/browser/navigation-client.ts +121 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +115 -67
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +147 -128
- package/src/browser/prefetch/cache.ts +107 -56
- package/src/browser/prefetch/fetch.ts +204 -34
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +30 -7
- package/src/browser/react/NavigationProvider.tsx +283 -118
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/deferred-handle-resolution.ts +75 -0
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +17 -14
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +10 -5
- package/src/browser/react/use-params.ts +11 -11
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +25 -3
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +99 -8
- package/src/browser/rsc-router.tsx +91 -24
- package/src/browser/scroll-restoration.ts +30 -17
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +214 -55
- package/src/browser/types.ts +80 -9
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +117 -14
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +117 -23
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +55 -28
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +224 -41
- package/src/cache/cache-scope.ts +188 -82
- package/src/cache/cache-tag.ts +103 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +1989 -378
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +89 -21
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +10 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +44 -21
- package/src/client.tsx +36 -61
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +18 -6
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +31 -23
- package/src/handles/MetaTags.tsx +62 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +37 -8
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +51 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +96 -12
- package/src/index.ts +94 -14
- package/src/internal-debug.ts +11 -10
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +61 -6
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +34 -0
- package/src/reverse.ts +65 -40
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +304 -309
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +82 -55
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +12 -1
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +73 -25
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +27 -43
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +39 -20
- package/src/router/lazy-includes.ts +10 -47
- package/src/router/loader-resolution.ts +155 -72
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +9 -24
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +159 -285
- package/src/router/match-middleware/cache-store.ts +64 -52
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +44 -74
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -134
- package/src/router/middleware.ts +247 -166
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +85 -94
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +104 -65
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +3 -28
- package/src/router/router-interfaces.ts +83 -35
- package/src/router/router-options.ts +136 -5
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +97 -84
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +76 -39
- package/src/router/segment-resolution/revalidation.ts +272 -320
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +56 -0
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +6 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +161 -199
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +162 -64
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +110 -55
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +264 -220
- package/src/rsc/helpers.ts +100 -6
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +117 -11
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +88 -188
- package/src/rsc/rsc-rendering.ts +98 -76
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +281 -117
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +23 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-loader-promise.ts +31 -4
- package/src/segment-system.tsx +254 -143
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +163 -51
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +287 -178
- package/src/ssr/index.tsx +21 -16
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +701 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +371 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +357 -0
- package/src/testing/render-route.tsx +581 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/run-transition-when.ts +164 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +20 -58
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +110 -62
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +135 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +29 -12
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +126 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +96 -68
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +44 -0
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +2 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +126 -8
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +48 -95
- package/src/vite/plugins/expose-id-utils.ts +88 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +505 -486
- package/src/vite/plugins/performance-tracks.ts +26 -25
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/version-injector.ts +40 -29
- package/src/vite/plugins/version-plugin.ts +37 -40
- package/src/vite/plugins/virtual-entries.ts +39 -25
- package/src/vite/rango.ts +109 -118
- package/src/vite/router-discovery.ts +718 -119
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +54 -39
- package/src/vite/utils/shared-utils.ts +90 -41
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
package/skills/loader/SKILL.md
CHANGED
|
@@ -6,7 +6,10 @@ argument-hint: [loader]
|
|
|
6
6
|
|
|
7
7
|
# Data Loaders with loader()
|
|
8
8
|
|
|
9
|
-
Loaders fetch data on the server and stream it to the client.
|
|
9
|
+
Loaders fetch data on the server and stream it to the client. For mutations
|
|
10
|
+
(writes triggered by forms or buttons), use server actions instead — see
|
|
11
|
+
`/server-actions`. Loaders re-resolve after an action runs, so the typical
|
|
12
|
+
flow is _action mutates → loader re-reads → UI updates_.
|
|
10
13
|
|
|
11
14
|
## Creating a Loader
|
|
12
15
|
|
|
@@ -88,6 +91,20 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
88
91
|
]);
|
|
89
92
|
```
|
|
90
93
|
|
|
94
|
+
> **Client refresh `key` vs. server `cache({ key })` vs. `revalidate()`.** Three
|
|
95
|
+
> different "what refreshes" knobs that are easy to confuse:
|
|
96
|
+
>
|
|
97
|
+
> - `useLoader(Loader, { key })` / `useFetchLoader(Loader, { key })` — a
|
|
98
|
+
> **client** refresh identity. It groups which mounted reads of one loader
|
|
99
|
+
> refresh together when one calls `load()`. It never touches the server
|
|
100
|
+
> request. For refreshing **different** loaders together, tag them with
|
|
101
|
+
> `{ refreshGroup }` (one name or several) and call `useRefreshLoaders()(name)`
|
|
102
|
+
> (plain GET only). See the hooks skill ("Scoping refetch with a `key`" and
|
|
103
|
+
> "Refreshing multiple loaders together").
|
|
104
|
+
> - `cache({ key })` — a **server** cache identity (storage hit/miss/ttl/swr).
|
|
105
|
+
> - `revalidate()` — which **server** segments/loaders recompute during
|
|
106
|
+
> navigation and action refreshes.
|
|
107
|
+
|
|
91
108
|
DSL loaders are the **live data layer** — they resolve fresh on every
|
|
92
109
|
request, even when the route is inside a `cache()` boundary. The router
|
|
93
110
|
excludes them from the segment cache at storage time and re-resolves them
|
|
@@ -139,7 +156,29 @@ same memoized result — loaders never run twice per request.
|
|
|
139
156
|
|
|
140
157
|
## Loader Context
|
|
141
158
|
|
|
142
|
-
Loaders receive the same context as route handlers
|
|
159
|
+
Loaders receive the same context shape as route handlers.
|
|
160
|
+
|
|
161
|
+
### Full field surface
|
|
162
|
+
|
|
163
|
+
| Field | Type | Notes |
|
|
164
|
+
| -------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
165
|
+
| `params` | `TParams` | Merged route + explicit loader params; overridable by fetchable `load({ params })`. |
|
|
166
|
+
| `routeParams` | `Record<string, string>` | Server-trusted route params from URL pattern matching; cannot be overridden. |
|
|
167
|
+
| `request` | `Request` | The incoming `Request` (headers, method, body, `signal` for abort). |
|
|
168
|
+
| `url` | `URL` | Parsed request URL. |
|
|
169
|
+
| `pathname` | `string` | URL pathname (shortcut for `ctx.url.pathname`). |
|
|
170
|
+
| `searchParams` | `URLSearchParams` | Shortcut for `ctx.url.searchParams`. |
|
|
171
|
+
| `search` | `ResolveSearchSchema<TSearch>` | Typed query params when a search schema is declared on the route; `{}` otherwise. |
|
|
172
|
+
| `env` | `TEnv` | Plain bindings from `createRouter<TEnv>()` (DB, KV, secrets, etc.). |
|
|
173
|
+
| `get` | `(key \| ContextVar) => value` | Reads variables/context-vars set by middleware. |
|
|
174
|
+
| `use` | `(loader \| handle) => T` | Access another loader's data (Promise) or a handle's collected data (after `await ctx.rendered()`). |
|
|
175
|
+
| `rendered` | `() => Promise<void>` | **Experimental.** DSL loaders only — waits for all non-loader segments (including `loading()` streaming handlers) to settle before reading handle data. |
|
|
176
|
+
| `method` | `string` | HTTP method. `"GET"` for SSR loader runs; reflects real method for fetchable loaders. |
|
|
177
|
+
| `body` | `TBody \| undefined` | Parsed request body for fetchable POST/PUT/PATCH/DELETE calls. |
|
|
178
|
+
| `formData` | `FormData \| undefined` | Present when a fetchable loader is invoked via form submission. |
|
|
179
|
+
| `reverse` | `ScopedReverseFunction` | Generate type-checked URLs from route names (same scoped semantics as route handlers). |
|
|
180
|
+
|
|
181
|
+
### Example
|
|
143
182
|
|
|
144
183
|
```typescript
|
|
145
184
|
export const ProductLoader = createLoader(async (ctx) => {
|
|
@@ -160,13 +199,24 @@ export const ProductLoader = createLoader(async (ctx) => {
|
|
|
160
199
|
// Request headers
|
|
161
200
|
const auth = ctx.request.headers.get("Authorization");
|
|
162
201
|
|
|
163
|
-
// Variables set by middleware (from
|
|
202
|
+
// Variables set by middleware (from Rango.Vars augmentation)
|
|
164
203
|
const user = ctx.get("user");
|
|
165
204
|
|
|
166
|
-
|
|
205
|
+
// Type-checked URLs for payloads. `.name` resolves within the current
|
|
206
|
+
// include() scope; a bare `name` resolves globally. See /route and
|
|
207
|
+
// /typesafety for scope rules and route-name autocomplete.
|
|
208
|
+
const detailUrl = ctx.reverse(".detail", { slug });
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
product: await fetchProduct(slug),
|
|
212
|
+
links: { self: detailUrl },
|
|
213
|
+
};
|
|
167
214
|
});
|
|
168
215
|
```
|
|
169
216
|
|
|
217
|
+
See `/route` for the full handler-context contract (shared with loaders) and
|
|
218
|
+
`/typesafety` for route-name typing that powers `ctx.reverse` autocomplete.
|
|
219
|
+
|
|
170
220
|
### params vs routeParams
|
|
171
221
|
|
|
172
222
|
- `ctx.params` — merged route params + explicit loader params. For fetchable
|
|
@@ -199,6 +249,8 @@ export const OrderLoader = createLoader(async (ctx) => {
|
|
|
199
249
|
Add caching or revalidation to specific loaders:
|
|
200
250
|
|
|
201
251
|
```typescript
|
|
252
|
+
import * as CartActions from "./actions/cart";
|
|
253
|
+
|
|
202
254
|
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
203
255
|
// Cached loader
|
|
204
256
|
loader(ProductLoader, () => [cache({ ttl: 300 })]),
|
|
@@ -208,13 +260,104 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
208
260
|
revalidate(() => false), // Never revalidate
|
|
209
261
|
]),
|
|
210
262
|
|
|
211
|
-
// Loader that revalidates after cart actions
|
|
263
|
+
// Loader that revalidates after cart actions (defer otherwise — keeps the
|
|
264
|
+
// permissive loader defaults for navigation and other actions intact)
|
|
212
265
|
loader(CartLoader, () => [
|
|
213
|
-
revalidate((
|
|
266
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
214
267
|
]),
|
|
215
268
|
]);
|
|
216
269
|
```
|
|
217
270
|
|
|
271
|
+
### `revalidate()` return shapes
|
|
272
|
+
|
|
273
|
+
> **Scope: `revalidate()` is a partial-render concern, not a cache concern.**
|
|
274
|
+
> It decides whether a segment (here, a loader) re-runs and streams to the
|
|
275
|
+
> client on a navigation or action — never whether a cached value is stale. The
|
|
276
|
+
> cache decides hit/miss/ttl/swr independently and never reads `revalidate()`.
|
|
277
|
+
> Caching a loader is a separate, opt-in step (`loader(Fn, () => [cache({...})])`).
|
|
278
|
+
> See `/cache-guide` → "Two axes" and `/rango` → "The shape of rango".
|
|
279
|
+
|
|
280
|
+
A `revalidate(fn)` callback can return one of four shapes. The chain
|
|
281
|
+
processes revalidators in order; each call's return controls how the
|
|
282
|
+
chain continues:
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
// 1) Hard decision — short-circuits the chain, used as the final answer.
|
|
286
|
+
revalidate(() => true);
|
|
287
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") ?? false);
|
|
288
|
+
|
|
289
|
+
// 2) Soft decision — updates the running suggestion for downstream
|
|
290
|
+
// revalidators on the same segment, chain continues.
|
|
291
|
+
revalidate(({ defaultShouldRevalidate }) => ({
|
|
292
|
+
defaultShouldRevalidate: !defaultShouldRevalidate,
|
|
293
|
+
}));
|
|
294
|
+
|
|
295
|
+
// 3) Defer (no opinion) — leaves the running suggestion unchanged and
|
|
296
|
+
// continues to the next revalidator. Implicit return / null /
|
|
297
|
+
// undefined are all equivalent and consumer-friendly.
|
|
298
|
+
revalidate(({ actionId }) => {
|
|
299
|
+
if (actionId?.includes("Cart")) return true; // hard for this branch only
|
|
300
|
+
// implicit return — let downstream revalidators or the segment default decide
|
|
301
|
+
});
|
|
302
|
+
revalidate(() => undefined); // explicit defer
|
|
303
|
+
revalidate(() => null); // explicit defer
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
If every revalidator on a segment defers, the segment-type default
|
|
307
|
+
(e.g. params-changed for routes, `false` for parallels) is used.
|
|
308
|
+
|
|
309
|
+
#### `|| undefined` (defer) vs `?? false` (hard) — pick deliberately
|
|
310
|
+
|
|
311
|
+
A boolean return — including `false` — is a **hard** decision: it short-circuits
|
|
312
|
+
the chain and overrides the segment default. `undefined` **defers** to the
|
|
313
|
+
running suggestion / segment default. They are not interchangeable:
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
// Defer: "revalidate on match, otherwise let the default/downstream decide."
|
|
317
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") || undefined);
|
|
318
|
+
|
|
319
|
+
// Hard: "revalidate ONLY on match, suppress everything else."
|
|
320
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") ?? false);
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
This matters most for loaders, whose defaults are permissive: a loader defaults
|
|
324
|
+
to revalidating on **any** action (`POST`) and on **param/search changes**
|
|
325
|
+
during navigation. So `?? false` on a loader silently suppresses both — the
|
|
326
|
+
loader will not refetch when you navigate to a different `:id`. Use
|
|
327
|
+
`|| undefined` when you want to _add_ a revalidation signal on top of the
|
|
328
|
+
sensible defaults, and reserve `?? false` for the rare case where you genuinely
|
|
329
|
+
want the loader to refetch on nothing but your matched action.
|
|
330
|
+
|
|
331
|
+
When **composing multiple revalidators** on one segment (see below), defer is
|
|
332
|
+
mandatory: the first hard `?? false` ends the chain and the later contracts
|
|
333
|
+
never run.
|
|
334
|
+
|
|
335
|
+
#### Matching actions: `ctx.isAction()`
|
|
336
|
+
|
|
337
|
+
To revalidate after specific server actions, match them by **reference** with
|
|
338
|
+
`ctx.isAction()` rather than hand-written `actionId` substrings. A rename or
|
|
339
|
+
moved file then becomes a type error instead of silently failing to match:
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
import { addToCart, removeFromCart } from "../actions/cart";
|
|
343
|
+
import * as CartActions from "../actions/cart";
|
|
344
|
+
|
|
345
|
+
loader(CartLoader, () => [
|
|
346
|
+
revalidate((ctx) => ctx.isAction(addToCart) || undefined), // one action
|
|
347
|
+
]);
|
|
348
|
+
revalidate((ctx) => ctx.isAction(addToCart, removeFromCart) || undefined); // several
|
|
349
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined); // any action in the module
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
`isAction()` is a method on the revalidate predicate's **context argument** —
|
|
353
|
+
there is no standalone `isAction` import; you always reach it through the callback
|
|
354
|
+
parameter (`revalidate((ctx) => ctx.isAction(...))`). It returns a raw boolean, so
|
|
355
|
+
pair it with `|| undefined` for the usual "revalidate on match, else defer"
|
|
356
|
+
intent. It returns `false` on plain navigation and on non-matches, and resolves
|
|
357
|
+
the reference the same way the router derives `actionId` (`$id` in production,
|
|
358
|
+
`$$id` in dev), so it matches in both modes. The raw `actionId` string stays
|
|
359
|
+
available on the same context as an escape hatch.
|
|
360
|
+
|
|
218
361
|
### Revalidation Contracts for Loader Dependencies
|
|
219
362
|
|
|
220
363
|
If a loader reads `ctx.get()` data produced by an outer handler/layout, share
|
|
@@ -222,8 +365,12 @@ the same named revalidation contract across producer and consumer segments.
|
|
|
222
365
|
|
|
223
366
|
```typescript
|
|
224
367
|
// revalidation-contracts.ts
|
|
225
|
-
|
|
226
|
-
|
|
368
|
+
import * as AccountActions from "./actions/account";
|
|
369
|
+
|
|
370
|
+
// Match by reference with ctx.isAction() (rename-safe), and defer (|| undefined)
|
|
371
|
+
// so these contracts compose — a hard `false` would short-circuit the rest.
|
|
372
|
+
export const revalidateAccountScope = (ctx) =>
|
|
373
|
+
ctx.isAction(AccountActions) || undefined;
|
|
227
374
|
|
|
228
375
|
layout(AccountLayout, () => [
|
|
229
376
|
revalidate(revalidateAccountScope), // producer reruns
|
|
@@ -266,6 +413,64 @@ follows the same rule: at build time, loaders are skipped entirely (there is no
|
|
|
266
413
|
real request context), and at runtime the worker resolves them fresh against
|
|
267
414
|
the live database.
|
|
268
415
|
|
|
416
|
+
### Parallel and streaming — latency overlaps first paint
|
|
417
|
+
|
|
418
|
+
Loaders do not block the page. As the render pass begins — the pass that route
|
|
419
|
+
middleware wraps, so loaders run right after middleware, not in a later
|
|
420
|
+
phase — every matched loader is kicked off **concurrently** (their promises start in the
|
|
421
|
+
same tick), and each result is **streamed** to the client as its own RSC Flight
|
|
422
|
+
chunk rather than awaited up front. Pair a loader with `loading()` (or a
|
|
423
|
+
client `<Suspense>`) and the shell paints immediately while the data streams in.
|
|
424
|
+
|
|
425
|
+
This is why **"cached UI still pays full data latency" is the wrong intuition**:
|
|
426
|
+
on a `cache()` hit the UI segments stream instantly from cache while the live
|
|
427
|
+
loaders resolve fresh **in parallel** — data latency _overlaps_ first paint
|
|
428
|
+
instead of being added on top of it. (Without a `loading()` / `<Suspense>`
|
|
429
|
+
boundary a parallel loader blocks its parent, so add one to keep the overlap.)
|
|
430
|
+
|
|
431
|
+
If you come from a framework where the loader is a blocking step that runs
|
|
432
|
+
before the response is built, this is the shift to internalize: here the
|
|
433
|
+
response starts streaming first and loader data fills in.
|
|
434
|
+
|
|
435
|
+
### See it: `debugPerformance`
|
|
436
|
+
|
|
437
|
+
Turn on the per-request performance timeline early — it is the fastest way to
|
|
438
|
+
confirm loaders overlap rather than serialize, and to find the real bottleneck
|
|
439
|
+
locally instead of guessing:
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
const router = createRouter({ document: Document, debugPerformance: true });
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Or enable it per-request from middleware (e.g. only when `?debug` is present) by
|
|
446
|
+
calling `ctx.debugPerformance()` **before** `await next()`. Each HTML request
|
|
447
|
+
then prints a shared-axis waterfall (and emits a `Server-Timing` header):
|
|
448
|
+
|
|
449
|
+
```
|
|
450
|
+
[RSC Perf] GET /product/widget (24.53ms)
|
|
451
|
+
start dur span timeline
|
|
452
|
+
0.08ms 3.20ms route-matching |#####...................................|
|
|
453
|
+
3.40ms 8.70ms ssr-render-html |.....##############.....................|
|
|
454
|
+
3.42ms 11.90ms loader:…#ProductLoader |.....###################................|
|
|
455
|
+
3.45ms 11.40ms loader:…#ReviewsLoader |.....##################.................|
|
|
456
|
+
0.00ms 24.53ms handler:total |########################################|
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
How to read it:
|
|
460
|
+
|
|
461
|
+
- **Humans:** scan the `#` bars on the shared axis. Bars that start at the same
|
|
462
|
+
offset and run side by side are executing **in parallel** — loaders should
|
|
463
|
+
overlap `ssr-render-html` / `render:total`, not sit alone to the right of
|
|
464
|
+
everything. A lone `loader:*` bar past the render bar is serialized latency to
|
|
465
|
+
chase. `handler:total` is the whole request; `render:total` is the render pass.
|
|
466
|
+
- **LLMs / programmatic:** read each row as `{ start, dur, label }`. A loader
|
|
467
|
+
overlaps paint when its `[start, start+dur]` interval intersects
|
|
468
|
+
`render:total` / `ssr-render-html`. Flag a regression when a `loader:*`
|
|
469
|
+
interval is **disjoint from and starts after** `render:total`, or when its
|
|
470
|
+
`dur` approaches `handler:total` — that loader is on the critical path instead
|
|
471
|
+
of overlapping it. Two `loader:*` rows with near-equal `start` confirm
|
|
472
|
+
parallel execution.
|
|
473
|
+
|
|
269
474
|
### Opting a Loader into Caching
|
|
270
475
|
|
|
271
476
|
To cache a specific loader's data, attach a `cache()` child:
|
|
@@ -356,6 +561,8 @@ entirely (no read, no write).
|
|
|
356
561
|
### Per-Loader Store Override
|
|
357
562
|
|
|
358
563
|
```typescript
|
|
564
|
+
import { MemorySegmentCacheStore } from "@rangojs/router/cache";
|
|
565
|
+
|
|
359
566
|
const hotStore = new MemorySegmentCacheStore({ defaults: { ttl: 10 } });
|
|
360
567
|
|
|
361
568
|
loader(PricingLoader, () => [
|
|
@@ -464,6 +671,16 @@ export const SearchLoader = createLoader(async (ctx) => {
|
|
|
464
671
|
}, true); // true = fetchable
|
|
465
672
|
```
|
|
466
673
|
|
|
674
|
+
> **No registration needed — and no worker-entry import.** A fetchable loader
|
|
675
|
+
> does not have to be registered with `loader()` in the route DSL, and it does
|
|
676
|
+
> not have to be imported by any server module. Importing it into the client
|
|
677
|
+
> component that calls `useFetchLoader()` / `load()` is enough. Rango discovers
|
|
678
|
+
> every `createLoader(fn, true)` at build time and registers it for the
|
|
679
|
+
> `_rsc_loader` endpoint, so a loader reachable only through a client component
|
|
680
|
+
> still resolves in production — on both the generated entry and a hand-written
|
|
681
|
+
> worker entry (e.g. a Cloudflare `worker.rsc.tsx`). You do **not** need to
|
|
682
|
+
> force-import the loader in your worker entry to make it resolve.
|
|
683
|
+
|
|
467
684
|
### Fetchable Loader with Middleware
|
|
468
685
|
|
|
469
686
|
Pass an options object instead of `true` to attach per-loader middleware.
|
|
@@ -539,6 +756,13 @@ export const FileUploadLoader = createLoader(async (ctx) => {
|
|
|
539
756
|
|
|
540
757
|
Client usage — see `/hooks useFetchLoader` for the full client-side pattern.
|
|
541
758
|
|
|
759
|
+
> **Refetch sharing**: when the loader is registered on the route via
|
|
760
|
+
> `loader()`, a plain `load()` call (no `params`, no `body`) broadcasts
|
|
761
|
+
> the new value to every component reading the same loader id —
|
|
762
|
+
> `useLoader` reads in layouts, pages, and parallel slots all converge.
|
|
763
|
+
> Calls with `params` or a non-GET method stay local to the call site.
|
|
764
|
+
> See `/hooks` → "Shared refetch behavior" for the full contract.
|
|
765
|
+
|
|
542
766
|
## Complete Example
|
|
543
767
|
|
|
544
768
|
```typescript
|
|
@@ -571,10 +795,12 @@ export const CartLoader = createLoader(async (ctx) => {
|
|
|
571
795
|
});
|
|
572
796
|
|
|
573
797
|
// urls.tsx — register loaders in the DSL
|
|
798
|
+
import * as CartActions from "./actions/cart";
|
|
799
|
+
|
|
574
800
|
export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalidate }) => [
|
|
575
801
|
layout(<ShopLayout />, () => [
|
|
576
802
|
loader(CartLoader, () => [
|
|
577
|
-
revalidate((
|
|
803
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
578
804
|
]),
|
|
579
805
|
|
|
580
806
|
path("/shop/product/:slug", ProductPage, { name: "product" }, () => [
|
|
@@ -10,9 +10,6 @@ Middleware runs before/after route handlers using the onion model.
|
|
|
10
10
|
|
|
11
11
|
## Execution Model
|
|
12
12
|
|
|
13
|
-
Canonical semantics reference:
|
|
14
|
-
[docs/execution-model.md](../../docs/internal/execution-model.md)
|
|
15
|
-
|
|
16
13
|
There are two levels of middleware with different execution scopes:
|
|
17
14
|
|
|
18
15
|
### Global middleware (`router.use()`)
|
|
@@ -32,17 +29,26 @@ When the router has a `basename`, pattern-scoped `.use()` patterns are automatic
|
|
|
32
29
|
|
|
33
30
|
Registered inside `urls()` callback. Wraps **rendering only** -- it does NOT wrap server action execution. Actions run before route middleware, so when route middleware executes during post-action revalidation, it can observe state that the action set (cookies, context variables, headers).
|
|
34
31
|
|
|
32
|
+
> **Implication for auth:** route middleware cannot guard server actions. Use `router.use("/admin/*", requireAuth)` (global, scoped) for action protection, or check inside the action body. See `/server-actions` for action-side auth patterns.
|
|
33
|
+
|
|
35
34
|
```
|
|
36
35
|
Request flow (with action):
|
|
37
|
-
global mw -> action executes -> route mw ->
|
|
36
|
+
global mw -> action executes -> route mw -> render pass
|
|
38
37
|
|
|
39
38
|
Request flow (no action):
|
|
40
|
-
global mw -> route mw ->
|
|
39
|
+
global mw -> route mw -> render pass
|
|
41
40
|
|
|
42
41
|
Progressive enhancement (no-JS form POST):
|
|
43
42
|
global mw -> action executes -> route mw -> full page re-render
|
|
44
43
|
```
|
|
45
44
|
|
|
45
|
+
The **render pass** resolves handler, layouts, parallels, and loaders together —
|
|
46
|
+
it is not a handler-then-loaders sequence. Handler-first ordering is guaranteed
|
|
47
|
+
only between a route handler and its child/orphan layouts and parallels (so
|
|
48
|
+
`ctx.set` is visible); loaders run **concurrently** and stream their results, so
|
|
49
|
+
their latency overlaps rendering rather than blocking it. See `/loader` →
|
|
50
|
+
"Parallel and streaming".
|
|
51
|
+
|
|
46
52
|
The contract is: **route middleware wraps rendering regardless of transport** (JS-enabled RSC stream or no-JS HTML). During PE re-render, route middleware observes action-set state (cookies, context variables) the same way it does during JS-enabled post-action revalidation.
|
|
47
53
|
|
|
48
54
|
Revalidation is still partial. Route middleware wraps the render pass that
|
|
@@ -61,8 +67,10 @@ For shared segment data, use named revalidation contracts on both the producer
|
|
|
61
67
|
and consumer segments, even when middleware is present in the chain.
|
|
62
68
|
|
|
63
69
|
```typescript
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
import * as CartActions from "./actions/cart";
|
|
71
|
+
|
|
72
|
+
export const revalidateCartData = (ctx) =>
|
|
73
|
+
ctx.isAction(CartActions) || undefined;
|
|
66
74
|
|
|
67
75
|
layout(CartLayout, () => [
|
|
68
76
|
middleware(cartRenderMiddleware),
|
|
@@ -190,7 +198,7 @@ export const myMiddleware: Middleware = async (ctx, next) => {
|
|
|
190
198
|
ctx.env.DB; // D1Database
|
|
191
199
|
ctx.env.KV; // KVNamespace
|
|
192
200
|
|
|
193
|
-
// Set variables for downstream handlers (typed via
|
|
201
|
+
// Set variables for downstream handlers (typed via Rango.Vars)
|
|
194
202
|
ctx.set("user", { id: "123", name: "John" });
|
|
195
203
|
|
|
196
204
|
// Continue to next middleware/handler
|
|
@@ -231,8 +239,8 @@ const Dashboard: Handler<"dashboard"> = (ctx) => {
|
|
|
231
239
|
```
|
|
232
240
|
|
|
233
241
|
This works alongside `ctx.get("key")` / `ctx.set("key", value)` (global typing
|
|
234
|
-
via
|
|
235
|
-
data; use
|
|
242
|
+
via Rango.Vars augmentation). Use `createVar` for route-local or feature-scoped
|
|
243
|
+
data; use Rango.Vars for app-wide middleware state.
|
|
236
244
|
|
|
237
245
|
## Redirect with State in Middleware
|
|
238
246
|
|
|
@@ -193,9 +193,9 @@ The main content always goes through `<Outlet />` via the `path()` handler.
|
|
|
193
193
|
// Rango: explicit intercept in layout
|
|
194
194
|
layout(<ShopLayout />, () => [
|
|
195
195
|
path("/product/:id", ProductPage, { name: "product" }),
|
|
196
|
-
intercept("@modal", ".product", <ProductModal />,
|
|
197
|
-
when(
|
|
198
|
-
|
|
196
|
+
intercept("@modal", ".product", <ProductModal />, {
|
|
197
|
+
when: ({ from }) => from.pathname.startsWith("/shop"),
|
|
198
|
+
}),
|
|
199
199
|
])
|
|
200
200
|
```
|
|
201
201
|
|
|
@@ -225,7 +225,7 @@ Loaders are Rango's live data layer. Use them when you need:
|
|
|
225
225
|
|
|
226
226
|
- **Client-side data refresh** — `useLoader()` in client components for reactive data
|
|
227
227
|
- **Per-loader caching** — opt in with `loader(MyLoader, () => [cache({ ttl: 60 })])`; loaders stay live by default
|
|
228
|
-
- **Revalidation control** — `revalidate()` targets specific loaders after actions
|
|
228
|
+
- **Revalidation control** — `revalidate()` targets specific segments and loaders after actions
|
|
229
229
|
- **Loading skeletons** — `loading()` shows a Suspense fallback while loaders resolve
|
|
230
230
|
|
|
231
231
|
```typescript
|
|
@@ -288,21 +288,40 @@ export const Product = Passthrough(ProductDef, async (ctx) => {
|
|
|
288
288
|
Use `Passthrough()` whenever the Next.js route has `dynamicParams: true` (the
|
|
289
289
|
default) or serves an open-ended param space. See `/prerender` for full API.
|
|
290
290
|
|
|
291
|
-
### Revalidation:
|
|
291
|
+
### Revalidation: two distinct axes
|
|
292
292
|
|
|
293
|
-
Next.js
|
|
294
|
-
|
|
293
|
+
Next.js conflates two things under "revalidation." Rango separates them — and
|
|
294
|
+
tag-based cache invalidation now maps directly.
|
|
295
295
|
|
|
296
|
-
|
|
296
|
+
**1. Cache invalidation (bust cached values) — direct equivalent.** Tag entries
|
|
297
|
+
with `cache({ tags })` or, inside a `"use cache"` function, runtime
|
|
298
|
+
`cacheTag(...tags)`. Then invalidate by tag:
|
|
297
299
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
300
|
+
```typescript
|
|
301
|
+
// Next.js Rango
|
|
302
|
+
// revalidateTag("products") → await updateTag("products") // in a server action: awaitable,
|
|
303
|
+
// // read-your-own-writes (next render is fresh)
|
|
304
|
+
// or revalidateTag("products") // in a route handler / webhook:
|
|
305
|
+
// // background, non-blocking (hard-purge)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
`updateTag` is awaitable and immediate; `revalidateTag` is fire-and-forget. Both
|
|
309
|
+
hard-purge (the next read re-renders fresh); the only difference is awaitability —
|
|
310
|
+
despite the Next.js name, `revalidateTag` here is NOT stale-while-revalidate.
|
|
311
|
+
Built-in stores (`MemorySegmentCacheStore`, `CFCacheStore`) index by tag. Next's
|
|
312
|
+
`revalidatePath` has no path-based equivalent — tag the relevant entries instead.
|
|
313
|
+
|
|
314
|
+
**2. Partial-render selection (which segments re-run after an action).** This is
|
|
315
|
+
NOT cache invalidation — it is `revalidate()`, controlling which segments
|
|
316
|
+
(layouts, paths, loaders, parallels) recompute during partial action
|
|
317
|
+
re-rendering:
|
|
301
318
|
|
|
302
319
|
```typescript
|
|
320
|
+
import { updateBlog } from "./actions/blog";
|
|
321
|
+
|
|
303
322
|
// Re-run this layout when a blog action fires
|
|
304
323
|
layout(BlogLayout, () => [
|
|
305
|
-
revalidate((
|
|
324
|
+
revalidate((ctx) => ctx.isAction(updateBlog) || undefined),
|
|
306
325
|
path("/blog/:slug", BlogPost, { name: "blogPost" }),
|
|
307
326
|
]);
|
|
308
327
|
|
|
@@ -323,15 +342,18 @@ cache({ ttl: 60, swr: 300 }, () => [
|
|
|
323
342
|
]);
|
|
324
343
|
```
|
|
325
344
|
|
|
326
|
-
The
|
|
345
|
+
The two axes compose: `updateTag()` / `revalidateTag()` bust cached values;
|
|
346
|
+
`revalidate()` selects which segments re-render and stream to the client after an
|
|
347
|
+
action.
|
|
327
348
|
|
|
328
|
-
|
|
329
|
-
- Rango asks "which segments should re-run after this action?"
|
|
349
|
+
When migrating:
|
|
330
350
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
`
|
|
351
|
+
- `revalidateTag(tag)` → `await updateTag(tag)` (in a server action) or
|
|
352
|
+
`revalidateTag(tag)` (in a route handler / webhook). Effectively 1:1.
|
|
353
|
+
- `revalidatePath(path)` → no path-based equivalent; tag the entries on that
|
|
354
|
+
route (`cache({ tags })` / `cacheTag(...)`) and invalidate by tag.
|
|
355
|
+
- To also force specific segments to re-render after the action (independent of
|
|
356
|
+
cache busting), attach a `revalidate()` rule at those segment boundaries.
|
|
335
357
|
|
|
336
358
|
## 4. Middleware
|
|
337
359
|
|
|
@@ -463,6 +485,8 @@ Server actions work the same way — `"use server"` directive, `useActionState`,
|
|
|
463
485
|
|
|
464
486
|
Key difference: in Rango, route middleware does NOT wrap action execution. Actions only see global middleware context. Use `getRequestContext()` in actions to access `ctx.set()`/`ctx.get()`.
|
|
465
487
|
|
|
488
|
+
Next.js's `revalidateTag()` maps directly: tag entries via `cache({ tags })` / `cacheTag(...)`, then invalidate. **In a server action use `await updateTag(tag)`** — it is read-your-own-writes, so the action's own re-render sees fresh data; `revalidateTag(tag)` is a background (non-blocking) hard-purge and is NOT read-your-own-writes, so reserve it for route handlers / webhooks (calling it from an action can leave that action's re-render stale). `revalidatePath()` has no path-based equivalent — tag the route's entries instead. Separately, to force specific matched segments (path/layout/parallel/intercept) and their loaders to re-render after an action, attach a `revalidate(({ actionId }) => ...)` rule to that segment or loader registration. See `/server-actions` for the full pattern (validation, error handling, file uploads), `/caching` for tag invalidation, and `/loader` for revalidation rule semantics.
|
|
489
|
+
|
|
466
490
|
## 8. Metadata / Head
|
|
467
491
|
|
|
468
492
|
Rango uses the `Meta` handle + `<MetaTags />` client component:
|
|
@@ -483,6 +483,10 @@ Since Rango uses RSC server actions, all React action patterns work:
|
|
|
483
483
|
`useActionState`, `useOptimistic`, `useTransition`, `startTransition`,
|
|
484
484
|
and plain `<form action={serverAction}>`. No framework-specific hook needed.
|
|
485
485
|
|
|
486
|
+
For the full guide — defining actions, validation with Zod, error handling,
|
|
487
|
+
revalidation rules, file uploads, and progressive enhancement — see
|
|
488
|
+
`/server-actions`.
|
|
489
|
+
|
|
486
490
|
### clientLoader / clientAction (framework mode)
|
|
487
491
|
|
|
488
492
|
RR7 framework mode's `clientLoader` and `clientAction` run in the browser.
|
|
@@ -593,11 +597,13 @@ function ErrorBoundary() {
|
|
|
593
597
|
}
|
|
594
598
|
|
|
595
599
|
// Rango: errorBoundary() wrapping a group of routes
|
|
600
|
+
// Server-side error boundaries only receive `error` (no `reset` — server render
|
|
601
|
+
// cannot be retried; users can navigate away or refresh).
|
|
596
602
|
layout(<DashboardLayout />, () => [
|
|
597
|
-
errorBoundary(({ error
|
|
603
|
+
errorBoundary(({ error }) => (
|
|
598
604
|
<div>
|
|
599
605
|
<h2>Something went wrong</h2>
|
|
600
|
-
<
|
|
606
|
+
<p>{error.message}</p>
|
|
601
607
|
</div>
|
|
602
608
|
)),
|
|
603
609
|
path("/dashboard", DashboardIndex, { name: "dashboard" }),
|
|
@@ -81,7 +81,7 @@ export const urlpatterns = urls(({ path }) => [
|
|
|
81
81
|
- `Accept: application/json` — JSON handler
|
|
82
82
|
- `Accept: text/plain` — text handler
|
|
83
83
|
- `Accept: application/xml` — XML handler
|
|
84
|
-
- `Accept: */*` —
|
|
84
|
+
- `Accept: */*` — RSC page (the primary, since it was registered first)
|
|
85
85
|
|
|
86
86
|
## Wildcard Routes
|
|
87
87
|
|
|
@@ -108,6 +108,33 @@ path.text("/api/data", () => "plain text version", { name: "dataText" }),
|
|
|
108
108
|
Without an RSC primary, there is no `text/html` candidate — the Accept header
|
|
109
109
|
picks among the response-type candidates directly.
|
|
110
110
|
|
|
111
|
+
## Type Safety For Negotiated Paths
|
|
112
|
+
|
|
113
|
+
`router.named-routes.gen.ts` validates route names, params, search, `href()`, and
|
|
114
|
+
the `Rango.Path` type, but it does not carry response payload metadata. For MIME or
|
|
115
|
+
response payload types, use one of these surfaces:
|
|
116
|
+
|
|
117
|
+
- `RouteResponse<typeof patterns, "routeName">` for a specific response variant
|
|
118
|
+
by route name. This is the clearest option when several MIME variants share
|
|
119
|
+
one URL pattern.
|
|
120
|
+
- `Rango.PathResponse<"/products/:id">` (ambient, no import) for global lookup by URL pattern or concrete path after the app
|
|
121
|
+
registers `typeof router.routeMap`:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// router.tsx
|
|
125
|
+
export const router = createRouter({ document: Document }).routes(urlpatterns);
|
|
126
|
+
|
|
127
|
+
declare global {
|
|
128
|
+
namespace Rango {
|
|
129
|
+
interface RegisteredRoutes extends typeof router.routeMap {}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`RegisteredRoutes` is what exposes the richer routeMap entries containing
|
|
135
|
+
response payload metadata. Without it, URL-pattern response lookup has paths but
|
|
136
|
+
no payloads, so response types resolve to `never`.
|
|
137
|
+
|
|
111
138
|
## How It Works
|
|
112
139
|
|
|
113
140
|
1. **Build time**: `buildRouteTrie()` calls `mergeLeaves()` when multiple routes share a pattern.
|