@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/caching/SKILL.md
CHANGED
|
@@ -8,6 +8,46 @@ argument-hint: [setup]
|
|
|
8
8
|
|
|
9
9
|
@rangojs/router supports segment-level caching with stale-while-revalidate (SWR) for optimal performance.
|
|
10
10
|
|
|
11
|
+
> SWR support is store-specific. `CFCacheStore` revalidates segment, response,
|
|
12
|
+
> and `"use cache"` entries in the background. `MemorySegmentCacheStore`
|
|
13
|
+
> supports SWR for response and `"use cache"` item entries, but its
|
|
14
|
+
> route-segment entries expire at TTL with no background revalidation — use
|
|
15
|
+
> `CFCacheStore` for real segment SWR. See `/cache-guide`.
|
|
16
|
+
|
|
17
|
+
## cache() is Partial Prerendering (PPR)
|
|
18
|
+
|
|
19
|
+
`cache()` caches **everything except loaders**. On a cache hit, the cached
|
|
20
|
+
segments (layouts, route components, parallels — including any resolved
|
|
21
|
+
Suspense) are served from the store, and **loaders re-run fresh on every
|
|
22
|
+
request**, streaming their results into the same response. Loaders are the
|
|
23
|
+
dynamic "holes" of an otherwise-cached tree.
|
|
24
|
+
|
|
25
|
+
This means a `cache()` boundary at the document root **is** whole-document
|
|
26
|
+
Partial Prerendering: the static shell is cached and served instantly while
|
|
27
|
+
per-request/per-user data stays live — in one streamed response, no extra round
|
|
28
|
+
trip. The browser cannot tell the shell came from cache.
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
cache({ ttl: 60, swr: 300 }, () => [
|
|
32
|
+
layout(<RootLayout />), // cached shell
|
|
33
|
+
path("/dashboard", Dashboard, { name: "dashboard" }, () => [
|
|
34
|
+
loader(StatsLoader), // DYNAMIC HOLE — re-runs every request
|
|
35
|
+
]),
|
|
36
|
+
]);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The consumer rule: **want it cached? render it inline. want it dynamic? put it
|
|
40
|
+
in a loader and read it with `useLoader()` in a client component.** Anything
|
|
41
|
+
read with `cookies()`, `headers()`, or a non-cacheable variable belongs in a
|
|
42
|
+
loader (loaders always run fresh). Reading it directly in a cached handler
|
|
43
|
+
throws; awaiting it with `ctx.use()` and rendering the result in a cached
|
|
44
|
+
handler silently bakes per-request data into the shared shell (see "Cache purity
|
|
45
|
+
& tainted objects" below).
|
|
46
|
+
|
|
47
|
+
Pre-rendering (`/prerender`) is the build-time twin: it caches the same shell at
|
|
48
|
+
build time instead of on first request. Both feed the segment system
|
|
49
|
+
identically, and loaders always run fresh at request time.
|
|
50
|
+
|
|
11
51
|
## Route-Level Caching with cache()
|
|
12
52
|
|
|
13
53
|
Use the `cache()` DSL function to cache routes:
|
|
@@ -41,10 +81,83 @@ cache(
|
|
|
41
81
|
);
|
|
42
82
|
```
|
|
43
83
|
|
|
44
|
-
##
|
|
84
|
+
## Tag-Based Invalidation
|
|
45
85
|
|
|
46
|
-
|
|
47
|
-
|
|
86
|
+
Tag cached entries, then invalidate them on demand. Tags can be attached three ways:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// 1. Static tags in the cache() DSL
|
|
90
|
+
cache({ ttl: 300, tags: ["products"] }, () => [path("/products", List)]);
|
|
91
|
+
|
|
92
|
+
// 2. Dynamic tags (function of ctx)
|
|
93
|
+
cache(
|
|
94
|
+
{ ttl: 300, tags: (ctx) => [`product:${ctx.params.id}`, "products"] },
|
|
95
|
+
() => [path("/products/:id", Detail)],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// 3. Runtime tags inside a "use cache" function
|
|
99
|
+
async function getProduct(id: string) {
|
|
100
|
+
"use cache";
|
|
101
|
+
cacheTag(`product:${id}`, "products"); // variadic, additive
|
|
102
|
+
return db.getProduct(id);
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Invalidate with one of two server-only verbs (both variadic, imported from
|
|
107
|
+
`@rangojs/router`):
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// Server Action — read-your-own-writes. Await it so the action's own re-render
|
|
111
|
+
// (and the next navigation) sees fresh data.
|
|
112
|
+
async function updateProduct(formData: FormData) {
|
|
113
|
+
"use server";
|
|
114
|
+
await db.updateProduct(formData);
|
|
115
|
+
await updateTag("products");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Route handler / webhook — background, non-blocking (waitUntil). Hard-purge:
|
|
119
|
+
// the next read re-renders fresh (NOT stale-while-revalidate).
|
|
120
|
+
export async function POST() {
|
|
121
|
+
"use server";
|
|
122
|
+
revalidateTag("products");
|
|
123
|
+
return new Response("ok");
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
| API | Timing | Use in | Semantics |
|
|
128
|
+
| ------------------------ | --------------------------- | ------------------------- | ----------------------------------------------------- |
|
|
129
|
+
| `updateTag(...tags)` | awaitable (`Promise<void>`) | server actions | immediate; next read is fresh |
|
|
130
|
+
| `revalidateTag(...tags)` | background (`void`) | route handlers / webhooks | background (non-blocking); next read re-renders fresh |
|
|
131
|
+
|
|
132
|
+
Both built-in stores support tags. For `CFCacheStore`, distributed (cross-colo)
|
|
133
|
+
invalidation requires a `kv` namespace — the tag-invalidation markers live in
|
|
134
|
+
that same namespace; there is **no** separate tag-invalidation store to wire.
|
|
135
|
+
If no tag-capable store is configured, `updateTag`/`revalidateTag` warn and no-op.
|
|
136
|
+
|
|
137
|
+
By default `CFCacheStore` reads the KV marker on every tagged cache read
|
|
138
|
+
(strongest invalidation latency). To cut KV reads on hot tagged routes, set
|
|
139
|
+
`tagCacheTtl` (seconds) to cache each marker in the per-colo edge cache for that
|
|
140
|
+
window — the colo running `updateTag`/`revalidateTag` writes the fresh marker
|
|
141
|
+
into its own edge cache immediately (read-your-own-writes), while other colos
|
|
142
|
+
converge within `tagCacheTtl` (the **maximum extra cross-colo invalidation
|
|
143
|
+
latency** when no purge is wired). Keep it small (e.g. 30–60), or wire a purge
|
|
144
|
+
(below) and set it large. (Contrast `tagInvalidationTtl`, which must be _large_
|
|
145
|
+
— it bounds how long the KV marker itself lives and must exceed your max entry
|
|
146
|
+
TTL+SWR.)
|
|
147
|
+
|
|
148
|
+
To make other colos prompt without a short `tagCacheTtl`, pass `onRevalidateTag`:
|
|
149
|
+
each cached marker carries a namespaced Cloudflare `Cache-Tag`, and the hook is
|
|
150
|
+
handed exactly those tags (batched, once per `updateTag`/`revalidateTag` call) to
|
|
151
|
+
feed Cloudflare's purge-by-tag API — evicting the cached lookups everywhere.
|
|
152
|
+
Purge-by-tag is available on all plans (since April 2025), subject to per-plan
|
|
153
|
+
rate limits, so the batched single call matters. With a purge wired, `tagCacheTtl`
|
|
154
|
+
becomes a pure read-cost reducer + fallback window.
|
|
155
|
+
|
|
156
|
+
## Named Cache Profiles
|
|
157
|
+
|
|
158
|
+
Define named profiles in `createRouter({ cacheProfiles })` so the same TTL/SWR
|
|
159
|
+
values can be shared across the DSL and `"use cache"` functions without repetition.
|
|
160
|
+
Unknown names throw at boot time.
|
|
48
161
|
|
|
49
162
|
```typescript
|
|
50
163
|
// Define profiles in router
|
|
@@ -55,19 +168,25 @@ createRouter({
|
|
|
55
168
|
long: { ttl: 3600, swr: 7200 },
|
|
56
169
|
},
|
|
57
170
|
});
|
|
171
|
+
```
|
|
58
172
|
|
|
59
|
-
|
|
173
|
+
In the DSL, pass the profile's options directly to `cache()`:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
60
176
|
export const urlpatterns = urls(({ path, cache }) => [
|
|
61
|
-
cache(
|
|
177
|
+
cache({ ttl: 3600, swr: 7200 }, () => [
|
|
178
|
+
path("/blog", BlogIndex, { name: "blog" }),
|
|
179
|
+
]),
|
|
62
180
|
|
|
63
|
-
//
|
|
64
|
-
cache(
|
|
181
|
+
// Orphan cache boundary (covers subsequent siblings)
|
|
182
|
+
cache({ ttl: 60, swr: 120 }),
|
|
65
183
|
path("/feed", FeedPage, { name: "feed" }),
|
|
66
184
|
]);
|
|
67
185
|
```
|
|
68
186
|
|
|
69
|
-
|
|
70
|
-
|
|
187
|
+
The DSL `cache()` helper does NOT accept a string profile name — strings are only
|
|
188
|
+
valid in the `"use cache: <name>"` directive inside server functions. See
|
|
189
|
+
`/use-cache` for function-level caching with named profiles.
|
|
71
190
|
|
|
72
191
|
## Loader-Level Caching
|
|
73
192
|
|
|
@@ -116,7 +235,6 @@ import { MemorySegmentCacheStore } from "@rangojs/router/cache";
|
|
|
116
235
|
|
|
117
236
|
const store = new MemorySegmentCacheStore({
|
|
118
237
|
defaults: { ttl: 60, swr: 300 },
|
|
119
|
-
maxSize: 1000, // Max entries
|
|
120
238
|
});
|
|
121
239
|
```
|
|
122
240
|
|
|
@@ -173,13 +291,156 @@ const router = createRouter<AppBindings>({
|
|
|
173
291
|
KV entries require `expirationTtl >= 60s`. Short-lived entries (< 60s total TTL)
|
|
174
292
|
are only cached in L1.
|
|
175
293
|
|
|
176
|
-
|
|
294
|
+
### Resilience & latency budgets
|
|
295
|
+
|
|
296
|
+
Every cache read is **fail-safe**: a degraded tier never stalls or fails the
|
|
297
|
+
request — it degrades to the next tier (L1 → L2 → render). Three optional latency
|
|
298
|
+
budgets (milliseconds) bound each tier so a slow colo or KV namespace cannot pin
|
|
299
|
+
a request behind it:
|
|
300
|
+
|
|
301
|
+
| Option | Default | Bounds |
|
|
302
|
+
| --------------------- | ------- | ----------------------------------- |
|
|
303
|
+
| `edgeLookupTimeoutMs` | `10` | L1 `cache.match` (the lookup) |
|
|
304
|
+
| `edgeReadTimeoutMs` | `20` | L1 body read (CF streams it lazily) |
|
|
305
|
+
| `kvReadTimeoutMs` | `170` | L2 / KV read |
|
|
306
|
+
|
|
307
|
+
Set any to `0` (or a negative value) to disable that budget and always await the
|
|
308
|
+
read. A non-finite value (e.g. `Number(env.UNSET)`) falls back to the default.
|
|
309
|
+
The tag-invalidation marker reads inherit these same budgets and **fail open** on
|
|
310
|
+
a KV timeout — the entry is served rather than wrongly treated as invalidated.
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
new CFCacheStore({
|
|
314
|
+
ctx,
|
|
315
|
+
kv: env.CACHE_KV,
|
|
316
|
+
defaults: { ttl: 60, swr: 300 },
|
|
317
|
+
// Raise a budget only if your HEALTHY reads legitimately run slower (large
|
|
318
|
+
// Flight payloads, far-from-colo regions); measure the p99 first. These are
|
|
319
|
+
// degradation guard-rails, not tuning levers for "slow is normal here".
|
|
320
|
+
kvReadTimeoutMs: 250,
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Failure handling, by kind — none of these fail the request:
|
|
325
|
+
|
|
326
|
+
| Failure | Behavior |
|
|
327
|
+
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
328
|
+
| Transient read error (5xx/blip) | Degrade to the next tier; entry left intact |
|
|
329
|
+
| Read budget exceeded (timeout) | Abandon the read, degrade to the next tier |
|
|
330
|
+
| Corrupt / unparseable L1 entry | Reported corrupt; degrade to L2 (served if present). The L1 entry is evicted ONLY when L2 has no copy — so the evict can't race the L2→L1 promote |
|
|
331
|
+
| Corrupt / unparseable KV entry | Reported corrupt; evicted (self-heal) + render (no tier below it) |
|
|
332
|
+
| Write failure | No-op (entry simply not cached); never throws |
|
|
333
|
+
|
|
334
|
+
Each is surfaced to the router's `onError` callback (phase `"cache"`, with
|
|
335
|
+
`metadata.category` one of `cache-read`, `cache-corrupt`, `cache-write`,
|
|
336
|
+
`cache-delete`, `cache-invalidate`, `stale-revalidation`) so you can observe
|
|
337
|
+
cache health without affecting users.
|
|
338
|
+
|
|
339
|
+
### Validating cache behavior with `debug`
|
|
340
|
+
|
|
341
|
+
Pass `debug` to emit one structured event per L1 read — use it to confirm on a
|
|
342
|
+
real deployment (via `wrangler tail`) that the store behaves as expected before
|
|
343
|
+
relying on it. It is intended for validation, not steady-state production.
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
new CFCacheStore({
|
|
347
|
+
ctx,
|
|
348
|
+
kv: env.CACHE_KV,
|
|
349
|
+
debug: true, // logs each CFCacheReadDebugEvent to the console
|
|
350
|
+
// ...or capture programmatically:
|
|
351
|
+
// debug: (event) => myTelemetry.record(event),
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Each event reports which tier answered and why (`outcome`: `l1-fresh`,
|
|
356
|
+
`l1-stale-revalidate`, `l1-revalidating-guarded`, `match-timeout`, `match-error`,
|
|
357
|
+
`body-timeout`, `body-error`, `non-200`, `tag-invalidated`, `l1-miss`, `kv-fresh`,
|
|
358
|
+
`kv-stale`, `kv-stale-suppressed`, `kv-miss`, `kv-timeout`, `error`), the
|
|
359
|
+
staleness / revalidating timestamps, and the measured per-tier durations:
|
|
360
|
+
`matchMs` (the L1 `match`), `markerMs` (the tag-marker resolution tail for a
|
|
361
|
+
tagged entry, between `matchMs` and `bodyReadMs`; absent or 0 for an untagged
|
|
362
|
+
entry or a per-request memo hit), and `bodyReadMs` (the L1 body read). A
|
|
363
|
+
persistently large `markerMs` signals a degraded KV namespace; on a healthy
|
|
364
|
+
deployment KV keeps markers hot in its per-colo edge cache, so it stays a few
|
|
365
|
+
milliseconds. `match-error` (a transient `cache.match` rejection that falls
|
|
366
|
+
through to L2) is kept distinct from a plain `l1-miss`.
|
|
367
|
+
|
|
368
|
+
## Cache purity & tainted objects
|
|
369
|
+
|
|
370
|
+
A `cache()` boundary caches everything except loaders, so anything read inside a
|
|
371
|
+
cached handler is **frozen into the shared cache entry** and served to every
|
|
372
|
+
subsequent visitor. To stop one user's request-scoped data from leaking to
|
|
373
|
+
another, request-scoped APIs are guarded inside a cache scope:
|
|
374
|
+
|
|
375
|
+
| Inside a `cache()` boundary | Behavior |
|
|
376
|
+
| --------------------------------------------------------------- | --------------------------------------------------- |
|
|
377
|
+
| `cookies()` / `headers()` (read or write) | **throws** — request-scoped, would poison the entry |
|
|
378
|
+
| `ctx.header()` / `setCookie()` / `setStatus()` / `onResponse()` | **throws** — response side effects lost on a hit |
|
|
379
|
+
| `ctx.get(var)` where the var is `{ cache: false }` | **throws** on read |
|
|
380
|
+
| `ctx.set(var, value)` for a cacheable var | allowed (children are cached too) |
|
|
381
|
+
| Any of the above **inside a loader** | **allowed** — loaders always run fresh |
|
|
382
|
+
|
|
383
|
+
**Tainted objects.** Request-scoped objects (`ctx`, `env`, `request`) carry an
|
|
384
|
+
internal taint symbol so they are excluded from `"use cache"` cache keys, and
|
|
385
|
+
the cache scope is tracked via async-local state. Two flags back the guards:
|
|
386
|
+
`INSIDE_CACHE_EXEC` (set while a `"use cache"` function runs) and the `cache()`
|
|
387
|
+
DSL scope (`isInsideCacheScope()`). `isInsideCacheScope()` deliberately returns
|
|
388
|
+
`false` inside loaders — which is exactly why loaders are the dynamic holes:
|
|
389
|
+
they may read `cookies()`/`headers()` and re-run on every request.
|
|
390
|
+
|
|
391
|
+
The fix for "I need request data in a cached route": register a `loader()` and
|
|
392
|
+
**consume it with `useLoader()` in a client component**. The loader is the
|
|
393
|
+
dynamic hole — its data rides the fresh (never-cached) loader segment and is
|
|
394
|
+
rendered in the client component, so it never lands in the cached shell.
|
|
395
|
+
|
|
396
|
+
This is NOT the same as awaiting the loader in the handler. A cached handler
|
|
397
|
+
that does `await ctx.use(Loader)` and renders the result bakes that per-request
|
|
398
|
+
data straight into the shared cached segment — the loader running "fresh" does
|
|
399
|
+
not help, because its output was inlined into the cached parent, and `ctx.use()`
|
|
400
|
+
is **not** guarded. `ctx.use()` is a server-side escape hatch for non-rendered
|
|
401
|
+
uses (set a ctx var, make a routing decision); never render its result inside a
|
|
402
|
+
cached handler.
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
// WRONG — throws: cookies() read directly in a cached handler
|
|
406
|
+
cache({ ttl: 60 }, () => [
|
|
407
|
+
path("/me", () => <Profile id={cookies().get("uid")?.value} />),
|
|
408
|
+
]);
|
|
409
|
+
|
|
410
|
+
// ALSO WRONG (unguarded, but leaks) — the awaited loader data is rendered into
|
|
411
|
+
// the cached handler, so the user's data is frozen into the shared shell.
|
|
412
|
+
cache({ ttl: 60 }, () => [
|
|
413
|
+
path(
|
|
414
|
+
"/me",
|
|
415
|
+
async (ctx) => {
|
|
416
|
+
const { user } = await ctx.use(MeLoader); // runs fresh…
|
|
417
|
+
return <Profile user={user} />; // …but inlined into the CACHED segment → leak
|
|
418
|
+
},
|
|
419
|
+
{ name: "me" },
|
|
420
|
+
() => [loader(MeLoader)],
|
|
421
|
+
),
|
|
422
|
+
]);
|
|
423
|
+
|
|
424
|
+
// RIGHT — consume the loader in a CLIENT component via useLoader(). The cached
|
|
425
|
+
// route segment holds only the <Profile/> reference; the user data rides the
|
|
426
|
+
// fresh loader segment and renders client-side.
|
|
427
|
+
|
|
428
|
+
// profile.tsx (client component)
|
|
429
|
+
"use client";
|
|
430
|
+
import { useLoader } from "@rangojs/router/client";
|
|
431
|
+
|
|
432
|
+
export function Profile() {
|
|
433
|
+
const { user } = useLoader(MeLoader); // fresh per request; never cached
|
|
434
|
+
return <span>{user.name}</span>;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// urls — register the loader; MeLoader reads cookies() inside the loader (allowed)
|
|
438
|
+
cache({ ttl: 60 }, () => [
|
|
439
|
+
path("/me", () => <Profile />, { name: "me" }, () => [loader(MeLoader)]),
|
|
440
|
+
]);
|
|
441
|
+
```
|
|
177
442
|
|
|
178
|
-
|
|
179
|
-
written inside `cache()` scopes. Variables marked with `{ cache: false }` (at
|
|
180
|
-
the var level or write level) throw when read inside a cache scope. Response
|
|
181
|
-
side effects (`ctx.header()`, `ctx.cookie()`) always throw inside cache
|
|
182
|
-
boundaries. See `/cache-guide` for the full cache safety table.
|
|
443
|
+
See `/cache-guide` for the full decision guide and the `cache()` vs `"use cache"` comparison.
|
|
183
444
|
|
|
184
445
|
## Nested Cache Boundaries
|
|
185
446
|
|
|
@@ -217,6 +478,7 @@ cache({ store: checkoutCache }, () => [
|
|
|
217
478
|
```typescript
|
|
218
479
|
import { urls } from "@rangojs/router";
|
|
219
480
|
import { MemorySegmentCacheStore } from "@rangojs/router/cache";
|
|
481
|
+
import * as CartActions from "./actions/cart";
|
|
220
482
|
|
|
221
483
|
// Custom store for checkout (short TTL)
|
|
222
484
|
const checkoutCache = new MemorySegmentCacheStore({
|
|
@@ -245,7 +507,7 @@ export const urlpatterns = urls(({ path, layout, cache, loader, revalidate }) =>
|
|
|
245
507
|
path("/shop/product/:slug", ProductPage, { name: "product" }, () => [
|
|
246
508
|
loader(ProductLoader, () => [cache({ ttl: 120 })]),
|
|
247
509
|
loader(CartLoader, () => [
|
|
248
|
-
revalidate((
|
|
510
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
249
511
|
]),
|
|
250
512
|
]),
|
|
251
513
|
]),
|
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
loading,
|
|
23
23
|
parallel,
|
|
24
24
|
intercept,
|
|
25
|
-
when,
|
|
26
25
|
errorBoundary,
|
|
27
26
|
notFoundBoundary,
|
|
28
27
|
} from "@rangojs/router";
|
|
@@ -55,7 +54,9 @@ import { cache, revalidate, loading, errorBoundary, middleware } from "@rangojs/
|
|
|
55
54
|
// Shared caching configuration
|
|
56
55
|
const withCaching = () => [
|
|
57
56
|
cache({ ttl: 600_000 }),
|
|
58
|
-
|
|
57
|
+
// Defer on navigation (|| undefined) so each route keeps its own param/search
|
|
58
|
+
// revalidation default; only force a re-run when an action ran.
|
|
59
|
+
revalidate(({ actionId }) => (actionId ? true : undefined)),
|
|
59
60
|
];
|
|
60
61
|
|
|
61
62
|
// Shared loading and error handling
|
|
@@ -71,6 +72,29 @@ const withAuth = () => [
|
|
|
71
72
|
];
|
|
72
73
|
```
|
|
73
74
|
|
|
75
|
+
> **Factories compose logic, not just values.** A `revalidate()` predicate in a
|
|
76
|
+
> shared factory applies its logic to _every_ route that composes it, so a
|
|
77
|
+
> footgun here is amplified across the app. Two rules:
|
|
78
|
+
>
|
|
79
|
+
> 1. Use `|| undefined` (defer), not `?? false` (hard short-circuit), in shared
|
|
80
|
+
> predicates — a hard `false` ends the chain and overrides each consuming
|
|
81
|
+
> route's own default, and a downstream revalidator never runs. See `/loader`
|
|
82
|
+
> → "`|| undefined` (defer) vs `?? false` (hard)".
|
|
83
|
+
> 2. Match actions with `ctx.isAction(Action)`, not an inline
|
|
84
|
+
> `actionId.includes("…")` buried in a factory: it resolves the action from an
|
|
85
|
+
> imported reference, so a rename is a compile error in one place instead of
|
|
86
|
+
> silent drift across every consumer.
|
|
87
|
+
>
|
|
88
|
+
> Remember the axis: a factory's `revalidate()` controls client-update
|
|
89
|
+
> selection, while its `cache()` controls stored-value freshness. They are
|
|
90
|
+
> independent even when bundled in the same factory (`/cache-guide` → "Two axes").
|
|
91
|
+
|
|
92
|
+
> **Keep factories small and intention-named.** The anti-pattern that kills
|
|
93
|
+
> readability is over-bundling — a `withDefaults()` that secretly adds five
|
|
94
|
+
> things — and factory-of-factories nesting (leaning on `.flat(3)`). Surprising
|
|
95
|
+
> config stays inline; extract only the boring, repeated parts; compose by
|
|
96
|
+
> _naming concerns_ (`withAuth()`, `withCaching()`), not by hiding them.
|
|
97
|
+
|
|
74
98
|
## Using Factories in Routes
|
|
75
99
|
|
|
76
100
|
Place factory calls inside `path()` or `layout()` use callbacks. The returned arrays are flattened automatically (up to 3 levels):
|
|
@@ -107,7 +131,7 @@ import { authMiddleware } from "./middleware/auth";
|
|
|
107
131
|
|
|
108
132
|
export const withPublicDefaults = () => [
|
|
109
133
|
cache({ ttl: 300 }),
|
|
110
|
-
revalidate(({ actionId }) =>
|
|
134
|
+
revalidate(({ actionId }) => (actionId ? true : undefined)),
|
|
111
135
|
];
|
|
112
136
|
|
|
113
137
|
export const withProtectedDefaults = () => [
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: css
|
|
3
|
+
description: Import and apply CSS in a Rango app. Render document/app stylesheets in the Document `<head>` with Vite's `?url` import plus a `precedence`-managed `<link rel="stylesheet">` (React 19 resource model — deduped, ordered, loaded before paint). Use when wiring global/app CSS or a Document `<head>` stylesheet, or when deciding between `?url` + `<link>` and side-effect imports. Cross-app (host-router) navigation is a full document load, so each app's document CSS is always re-established by its own load.
|
|
4
|
+
argument-hint:
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# CSS imports
|
|
8
|
+
|
|
9
|
+
Document/app CSS in Rango lives in the Document `<head>`, loaded with Vite's
|
|
10
|
+
`?url` import and a `precedence`-managed `<link rel="stylesheet">`. This page is
|
|
11
|
+
the why and the one cross-app caveat; `/tailwind` is the concrete setup, `/theme`
|
|
12
|
+
is dark mode, `/fonts` is fonts.
|
|
13
|
+
|
|
14
|
+
## The pattern
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
// document.tsx
|
|
18
|
+
"use client";
|
|
19
|
+
|
|
20
|
+
import type { ReactNode } from "react";
|
|
21
|
+
import { MetaTags } from "@rangojs/router/client";
|
|
22
|
+
import styles from "./index.css?url";
|
|
23
|
+
|
|
24
|
+
export function Document({ children }: { children: ReactNode }) {
|
|
25
|
+
return (
|
|
26
|
+
<html lang="en">
|
|
27
|
+
<head>
|
|
28
|
+
<link rel="preload" href={styles} as="style" precedence="default" />
|
|
29
|
+
<link rel="stylesheet" href={styles} precedence="default" />
|
|
30
|
+
<MetaTags />
|
|
31
|
+
</head>
|
|
32
|
+
<body>{children}</body>
|
|
33
|
+
</html>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- **`?url`** returns the processed file's hashed URL instead of injecting it as a
|
|
39
|
+
side effect, giving a stable asset path that works in dev and production.
|
|
40
|
+
- **`precedence`** opts the `<link rel="stylesheet">` into React 19's managed
|
|
41
|
+
stylesheet model: React de-duplicates it by `href`, orders it by precedence
|
|
42
|
+
(any string value — it only decides cascade order relative to other managed
|
|
43
|
+
sheets), and loads it **before paint**, so there is no flash of unstyled
|
|
44
|
+
content. This is the recommended way to render a stylesheet link.
|
|
45
|
+
|
|
46
|
+
## Cross-app (host-router) navigation
|
|
47
|
+
|
|
48
|
+
You do **not** need to coordinate CSS across apps mounted under one host router.
|
|
49
|
+
A client-side navigation that crosses an app boundary is a **full document load**
|
|
50
|
+
(the server returns `X-RSC-Reload` on an app switch — see `/host-router`), so the
|
|
51
|
+
target app's entire document — its stylesheets, theme, meta — is re-established
|
|
52
|
+
by the target app's own load. Each app owns its document; how one app renders a
|
|
53
|
+
stylesheet has no effect on another.
|
|
54
|
+
|
|
55
|
+
(This replaced an earlier soft cross-app swap. Under it, a stylesheet shared
|
|
56
|
+
across apps by `href` — classically every app's `@import "tailwindcss"` compiling
|
|
57
|
+
to one hashed asset — could be silently dropped by React's by-`href` resource
|
|
58
|
+
dedup when the apps disagreed on `precedence` (one unmanaged, one managed). The
|
|
59
|
+
full reload removes that footgun entirely, which is the main reason cross-app
|
|
60
|
+
navigation is a hard boundary.)
|
|
61
|
+
|
|
62
|
+
## Side-effect imports vs `?url`
|
|
63
|
+
|
|
64
|
+
A bare `import "./index.css"` (no `?url`, no `<link>`) also produces _managed_ CSS
|
|
65
|
+
— `@vitejs/plugin-rsc` collects it via `import.meta.viteRsc.loadCss` and injects
|
|
66
|
+
it with a precedence. It is fine for **component-local** CSS that loads with its
|
|
67
|
+
client chunk. For **document-level** CSS, prefer the `?url` + `<link precedence>`
|
|
68
|
+
form above: a side-effect import is not guaranteed to be in the initial streamed
|
|
69
|
+
`<head>` (an SSR-streaming caveat), whereas the explicit `<link>` is.
|
|
70
|
+
|
|
71
|
+
## Related
|
|
72
|
+
|
|
73
|
+
- `/tailwind` — Tailwind v4 setup using this pattern.
|
|
74
|
+
- `/host-router` — multi-app routing; why cross-app navigation is a full reload.
|
|
75
|
+
- `/theme` — dark mode / theme attribute.
|
|
76
|
+
- `/fonts` — self-hosted fonts via `@fontsource`.
|
|
@@ -13,10 +13,10 @@ Inspect the route manifest to verify parent relationships, shortCodes, and route
|
|
|
13
13
|
In development, visit:
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
http://localhost:PORT
|
|
16
|
+
http://localhost:PORT/?__debug_manifest
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
Returns formatted JSON
|
|
19
|
+
Returns formatted JSON. The HTTP endpoint shape is `{ routerId, routeManifest, routeAncestry, routeTrie, precomputedEntries }` (see below for the programmatic API shape).
|
|
20
20
|
|
|
21
21
|
## Programmatic Access
|
|
22
22
|
|
|
@@ -32,6 +32,8 @@ if (process.env.NODE_ENV !== "production") {
|
|
|
32
32
|
|
|
33
33
|
## Manifest Structure
|
|
34
34
|
|
|
35
|
+
The programmatic `router.debugManifest()` call returns `{ routes, layouts, totalRoutes, totalLayouts }`:
|
|
36
|
+
|
|
35
37
|
```json
|
|
36
38
|
{
|
|
37
39
|
"routes": {
|