@rangojs/router 0.0.0-experimental.31 → 0.0.0-experimental.3232cd17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -0
- package/README.md +198 -44
- package/dist/bin/rango.js +287 -105
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3248 -1117
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +73 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +107 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +245 -21
- package/skills/caching/SKILL.md +302 -6
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +270 -30
- package/skills/host-router/SKILL.md +82 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +49 -5
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +294 -30
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +203 -7
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +250 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +97 -5
- package/skills/router-setup/SKILL.md +90 -5
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +121 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +329 -27
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +67 -40
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +86 -147
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +148 -19
- package/src/browser/navigation-client.ts +187 -67
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +18 -66
- package/src/browser/partial-update.ts +123 -94
- package/src/browser/prefetch/cache.ts +214 -36
- package/src/browser/prefetch/fetch.ts +260 -38
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +126 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +93 -11
- package/src/browser/react/NavigationProvider.tsx +115 -34
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +49 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +23 -69
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +22 -5
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +52 -1
- package/src/browser/rsc-router.tsx +215 -76
- package/src/browser/scroll-restoration.ts +46 -39
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +176 -50
- package/src/browser/types.ts +95 -11
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +137 -32
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -96
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +149 -43
- package/src/cache/cache-scope.ts +148 -81
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2550 -93
- package/src/cache/cf/index.ts +11 -17
- package/src/cache/document-cache.ts +78 -27
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +108 -290
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +84 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +52 -26
- package/src/index.ts +100 -38
- package/src/internal-debug.ts +2 -4
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +437 -274
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +113 -37
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +52 -10
- package/src/route-definition/resolve-handler-use.ts +161 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -17
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +108 -9
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +45 -22
- package/src/router/handler-context.ts +83 -41
- package/src/router/intercept-resolution.ts +25 -23
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +213 -30
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +49 -45
- package/src/router/match-api.ts +121 -205
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +27 -6
- package/src/router/match-middleware/cache-lookup.ts +205 -249
- package/src/router/match-middleware/cache-store.ts +45 -32
- package/src/router/match-middleware/intercept-resolution.ts +8 -28
- package/src/router/match-middleware/segment-resolution.ts +52 -18
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +104 -40
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +173 -143
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +109 -63
- package/src/router/prerender-match.ts +192 -54
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +63 -55
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +6 -28
- package/src/router/router-interfaces.ts +100 -35
- package/src/router/router-options.ts +91 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +242 -75
- package/src/router/segment-resolution/helpers.ts +64 -25
- package/src/router/segment-resolution/loader-cache.ts +41 -37
- package/src/router/segment-resolution/revalidation.ts +456 -372
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +2 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +91 -46
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +134 -43
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +492 -383
- package/src/rsc/helpers.ts +162 -46
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +30 -3
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +90 -63
- package/src/rsc/rsc-rendering.ts +56 -54
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +74 -67
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +25 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +134 -0
- package/src/segment-system.tsx +272 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +309 -61
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +26 -24
- package/src/server/loader-registry.ts +10 -28
- package/src/server/request-context.ts +348 -128
- package/src/ssr/index.tsx +23 -15
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +618 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +233 -81
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +107 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +37 -14
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +58 -11
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/discover-routers.ts +130 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +192 -99
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +51 -6
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +8 -0
- package/src/vite/plugin-types.ts +187 -69
- package/src/vite/plugins/cjs-to-esm.ts +8 -18
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/plugins/expose-action-id.ts +49 -98
- package/src/vite/plugins/expose-id-utils.ts +11 -50
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
- package/src/vite/plugins/expose-internal-ids.ts +554 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +41 -20
- package/src/vite/plugins/virtual-entries.ts +2 -17
- package/src/vite/rango.ts +257 -289
- package/src/vite/router-discovery.ts +930 -140
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +27 -29
- package/src/vite/utils/shared-utils.ts +92 -42
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
package/skills/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,6 +81,78 @@ cache(
|
|
|
41
81
|
);
|
|
42
82
|
```
|
|
43
83
|
|
|
84
|
+
## Tag-Based Invalidation
|
|
85
|
+
|
|
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
|
+
|
|
44
156
|
## Named Profile Shorthand
|
|
45
157
|
|
|
46
158
|
Use a named cache profile string instead of an options object. The profile must be
|
|
@@ -116,13 +228,12 @@ import { MemorySegmentCacheStore } from "@rangojs/router/cache";
|
|
|
116
228
|
|
|
117
229
|
const store = new MemorySegmentCacheStore({
|
|
118
230
|
defaults: { ttl: 60, swr: 300 },
|
|
119
|
-
maxSize: 1000, // Max entries
|
|
120
231
|
});
|
|
121
232
|
```
|
|
122
233
|
|
|
123
|
-
### Cloudflare
|
|
234
|
+
### Cloudflare Edge Cache Store
|
|
124
235
|
|
|
125
|
-
For distributed caching on Cloudflare Workers:
|
|
236
|
+
For distributed caching on Cloudflare Workers using the Cache API:
|
|
126
237
|
|
|
127
238
|
```typescript
|
|
128
239
|
import { CFCacheStore } from "@rangojs/router/cache";
|
|
@@ -132,14 +243,198 @@ const router = createRouter<AppBindings>({
|
|
|
132
243
|
urls: urlpatterns,
|
|
133
244
|
cache: (env, ctx) => ({
|
|
134
245
|
store: new CFCacheStore({
|
|
135
|
-
|
|
136
|
-
|
|
246
|
+
ctx,
|
|
247
|
+
defaults: { ttl: 60, swr: 300 },
|
|
137
248
|
}),
|
|
138
249
|
enabled: true,
|
|
139
250
|
}),
|
|
140
251
|
});
|
|
141
252
|
```
|
|
142
253
|
|
|
254
|
+
### With KV L2 Persistence
|
|
255
|
+
|
|
256
|
+
Add a KV namespace for global cross-colo persistence. On Cache API miss, KV is
|
|
257
|
+
checked and hits are promoted back to L1. Writes go to both layers.
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
import { CFCacheStore } from "@rangojs/router/cache";
|
|
261
|
+
|
|
262
|
+
const router = createRouter<AppBindings>({
|
|
263
|
+
document: Document,
|
|
264
|
+
urls: urlpatterns,
|
|
265
|
+
cache: (env, ctx) => ({
|
|
266
|
+
store: new CFCacheStore({
|
|
267
|
+
ctx,
|
|
268
|
+
kv: env.CACHE_KV, // optional KV namespace binding
|
|
269
|
+
defaults: { ttl: 60, swr: 300 },
|
|
270
|
+
}),
|
|
271
|
+
enabled: true,
|
|
272
|
+
}),
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**How the two layers work:**
|
|
277
|
+
|
|
278
|
+
| Scenario | L1 (Cache API) | L2 (KV) | Result |
|
|
279
|
+
| ------------ | -------------- | ------- | ----------------------------- |
|
|
280
|
+
| Hot request | HIT | — | Serve from L1 (fast) |
|
|
281
|
+
| Cold colo | MISS | HIT | Serve from KV, promote to L1 |
|
|
282
|
+
| First render | MISS | MISS | Render, write to both L1 + KV |
|
|
283
|
+
|
|
284
|
+
KV entries require `expirationTtl >= 60s`. Short-lived entries (< 60s total TTL)
|
|
285
|
+
are only cached in L1.
|
|
286
|
+
|
|
287
|
+
### Resilience & latency budgets
|
|
288
|
+
|
|
289
|
+
Every cache read is **fail-safe**: a degraded tier never stalls or fails the
|
|
290
|
+
request — it degrades to the next tier (L1 → L2 → render). Three optional latency
|
|
291
|
+
budgets (milliseconds) bound each tier so a slow colo or KV namespace cannot pin
|
|
292
|
+
a request behind it:
|
|
293
|
+
|
|
294
|
+
| Option | Default | Bounds |
|
|
295
|
+
| --------------------- | ------- | ----------------------------------- |
|
|
296
|
+
| `edgeLookupTimeoutMs` | `10` | L1 `cache.match` (the lookup) |
|
|
297
|
+
| `edgeReadTimeoutMs` | `20` | L1 body read (CF streams it lazily) |
|
|
298
|
+
| `kvReadTimeoutMs` | `170` | L2 / KV read |
|
|
299
|
+
|
|
300
|
+
Set any to `0` (or a negative value) to disable that budget and always await the
|
|
301
|
+
read. A non-finite value (e.g. `Number(env.UNSET)`) falls back to the default.
|
|
302
|
+
The tag-invalidation marker reads inherit these same budgets and **fail open** on
|
|
303
|
+
a KV timeout — the entry is served rather than wrongly treated as invalidated.
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
new CFCacheStore({
|
|
307
|
+
ctx,
|
|
308
|
+
kv: env.CACHE_KV,
|
|
309
|
+
defaults: { ttl: 60, swr: 300 },
|
|
310
|
+
// Raise a budget only if your HEALTHY reads legitimately run slower (large
|
|
311
|
+
// Flight payloads, far-from-colo regions); measure the p99 first. These are
|
|
312
|
+
// degradation guard-rails, not tuning levers for "slow is normal here".
|
|
313
|
+
kvReadTimeoutMs: 250,
|
|
314
|
+
});
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Failure handling, by kind — none of these fail the request:
|
|
318
|
+
|
|
319
|
+
| Failure | Behavior |
|
|
320
|
+
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
321
|
+
| Transient read error (5xx/blip) | Degrade to the next tier; entry left intact |
|
|
322
|
+
| Read budget exceeded (timeout) | Abandon the read, degrade to the next tier |
|
|
323
|
+
| 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 |
|
|
324
|
+
| Corrupt / unparseable KV entry | Reported corrupt; evicted (self-heal) + render (no tier below it) |
|
|
325
|
+
| Write failure | No-op (entry simply not cached); never throws |
|
|
326
|
+
|
|
327
|
+
Each is surfaced to the router's `onError` callback (phase `"cache"`, with
|
|
328
|
+
`metadata.category` one of `cache-read`, `cache-corrupt`, `cache-write`,
|
|
329
|
+
`cache-delete`, `cache-invalidate`, `stale-revalidation`) so you can observe
|
|
330
|
+
cache health without affecting users.
|
|
331
|
+
|
|
332
|
+
### Validating cache behavior with `debug`
|
|
333
|
+
|
|
334
|
+
Pass `debug` to emit one structured event per L1 read — use it to confirm on a
|
|
335
|
+
real deployment (via `wrangler tail`) that the store behaves as expected before
|
|
336
|
+
relying on it. It is intended for validation, not steady-state production.
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
new CFCacheStore({
|
|
340
|
+
ctx,
|
|
341
|
+
kv: env.CACHE_KV,
|
|
342
|
+
debug: true, // logs each CFCacheReadDebugEvent to the console
|
|
343
|
+
// ...or capture programmatically:
|
|
344
|
+
// debug: (event) => myTelemetry.record(event),
|
|
345
|
+
});
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Each event reports which tier answered and why (`outcome`: `l1-fresh`,
|
|
349
|
+
`l1-stale-revalidate`, `l1-revalidating-guarded`, `match-timeout`, `match-error`,
|
|
350
|
+
`body-timeout`, `body-error`, `non-200`, `tag-invalidated`, `l1-miss`, `kv-fresh`,
|
|
351
|
+
`kv-stale`, `kv-stale-suppressed`, `kv-miss`, `kv-timeout`, `error`), the
|
|
352
|
+
staleness / revalidating timestamps, and the measured per-tier durations:
|
|
353
|
+
`matchMs` (the L1 `match`), `markerMs` (the tag-marker resolution tail for a
|
|
354
|
+
tagged entry, between `matchMs` and `bodyReadMs`; absent or 0 for an untagged
|
|
355
|
+
entry or a per-request memo hit), and `bodyReadMs` (the L1 body read). A
|
|
356
|
+
persistently large `markerMs` signals a degraded KV namespace; on a healthy
|
|
357
|
+
deployment KV keeps markers hot in its per-colo edge cache, so it stays a few
|
|
358
|
+
milliseconds. `match-error` (a transient `cache.match` rejection that falls
|
|
359
|
+
through to L2) is kept distinct from a plain `l1-miss`.
|
|
360
|
+
|
|
361
|
+
## Cache purity & tainted objects
|
|
362
|
+
|
|
363
|
+
A `cache()` boundary caches everything except loaders, so anything read inside a
|
|
364
|
+
cached handler is **frozen into the shared cache entry** and served to every
|
|
365
|
+
subsequent visitor. To stop one user's request-scoped data from leaking to
|
|
366
|
+
another, request-scoped APIs are guarded inside a cache scope:
|
|
367
|
+
|
|
368
|
+
| Inside a `cache()` boundary | Behavior |
|
|
369
|
+
| --------------------------------------------------------------- | --------------------------------------------------- |
|
|
370
|
+
| `cookies()` / `headers()` (read or write) | **throws** — request-scoped, would poison the entry |
|
|
371
|
+
| `ctx.header()` / `setCookie()` / `setStatus()` / `onResponse()` | **throws** — response side effects lost on a hit |
|
|
372
|
+
| `ctx.get(var)` where the var is `{ cache: false }` | **throws** on read |
|
|
373
|
+
| `ctx.set(var, value)` for a cacheable var | allowed (children are cached too) |
|
|
374
|
+
| Any of the above **inside a loader** | **allowed** — loaders always run fresh |
|
|
375
|
+
|
|
376
|
+
**Tainted objects.** Request-scoped objects (`ctx`, `env`, `request`) carry an
|
|
377
|
+
internal taint symbol so they are excluded from `"use cache"` cache keys, and
|
|
378
|
+
the cache scope is tracked via async-local state. Two flags back the guards:
|
|
379
|
+
`INSIDE_CACHE_EXEC` (set while a `"use cache"` function runs) and the `cache()`
|
|
380
|
+
DSL scope (`isInsideCacheScope()`). `isInsideCacheScope()` deliberately returns
|
|
381
|
+
`false` inside loaders — which is exactly why loaders are the dynamic holes:
|
|
382
|
+
they may read `cookies()`/`headers()` and re-run on every request.
|
|
383
|
+
|
|
384
|
+
The fix for "I need request data in a cached route": register a `loader()` and
|
|
385
|
+
**consume it with `useLoader()` in a client component**. The loader is the
|
|
386
|
+
dynamic hole — its data rides the fresh (never-cached) loader segment and is
|
|
387
|
+
rendered in the client component, so it never lands in the cached shell.
|
|
388
|
+
|
|
389
|
+
This is NOT the same as awaiting the loader in the handler. A cached handler
|
|
390
|
+
that does `await ctx.use(Loader)` and renders the result bakes that per-request
|
|
391
|
+
data straight into the shared cached segment — the loader running "fresh" does
|
|
392
|
+
not help, because its output was inlined into the cached parent, and `ctx.use()`
|
|
393
|
+
is **not** guarded. `ctx.use()` is a server-side escape hatch for non-rendered
|
|
394
|
+
uses (set a ctx var, make a routing decision); never render its result inside a
|
|
395
|
+
cached handler.
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
// WRONG — throws: cookies() read directly in a cached handler
|
|
399
|
+
cache({ ttl: 60 }, () => [
|
|
400
|
+
path("/me", () => <Profile id={cookies().get("uid")?.value} />),
|
|
401
|
+
]);
|
|
402
|
+
|
|
403
|
+
// ALSO WRONG (unguarded, but leaks) — the awaited loader data is rendered into
|
|
404
|
+
// the cached handler, so the user's data is frozen into the shared shell.
|
|
405
|
+
cache({ ttl: 60 }, () => [
|
|
406
|
+
path(
|
|
407
|
+
"/me",
|
|
408
|
+
async (ctx) => {
|
|
409
|
+
const { user } = await ctx.use(MeLoader); // runs fresh…
|
|
410
|
+
return <Profile user={user} />; // …but inlined into the CACHED segment → leak
|
|
411
|
+
},
|
|
412
|
+
{ name: "me" },
|
|
413
|
+
() => [loader(MeLoader)],
|
|
414
|
+
),
|
|
415
|
+
]);
|
|
416
|
+
|
|
417
|
+
// RIGHT — consume the loader in a CLIENT component via useLoader(). The cached
|
|
418
|
+
// route segment holds only the <Profile/> reference; the user data rides the
|
|
419
|
+
// fresh loader segment and renders client-side.
|
|
420
|
+
|
|
421
|
+
// profile.tsx (client component)
|
|
422
|
+
"use client";
|
|
423
|
+
import { useLoader } from "@rangojs/router/client";
|
|
424
|
+
|
|
425
|
+
export function Profile() {
|
|
426
|
+
const { user } = useLoader(MeLoader); // fresh per request; never cached
|
|
427
|
+
return <span>{user.name}</span>;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// urls — register the loader; MeLoader reads cookies() inside the loader (allowed)
|
|
431
|
+
cache({ ttl: 60 }, () => [
|
|
432
|
+
path("/me", () => <Profile />, { name: "me" }, () => [loader(MeLoader)]),
|
|
433
|
+
]);
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
See `/cache-guide` for the full decision guide and the `cache()` vs `"use cache"` comparison.
|
|
437
|
+
|
|
143
438
|
## Nested Cache Boundaries
|
|
144
439
|
|
|
145
440
|
Override cache settings for specific sections:
|
|
@@ -176,6 +471,7 @@ cache({ store: checkoutCache }, () => [
|
|
|
176
471
|
```typescript
|
|
177
472
|
import { urls } from "@rangojs/router";
|
|
178
473
|
import { MemorySegmentCacheStore } from "@rangojs/router/cache";
|
|
474
|
+
import * as CartActions from "./actions/cart";
|
|
179
475
|
|
|
180
476
|
// Custom store for checkout (short TTL)
|
|
181
477
|
const checkoutCache = new MemorySegmentCacheStore({
|
|
@@ -204,7 +500,7 @@ export const urlpatterns = urls(({ path, layout, cache, loader, revalidate }) =>
|
|
|
204
500
|
path("/shop/product/:slug", ProductPage, { name: "product" }, () => [
|
|
205
501
|
loader(ProductLoader, () => [cache({ ttl: 120 })]),
|
|
206
502
|
loader(CartLoader, () => [
|
|
207
|
-
revalidate((
|
|
503
|
+
revalidate((ctx) => ctx.isAction(CartActions) || undefined),
|
|
208
504
|
]),
|
|
209
505
|
]),
|
|
210
506
|
]),
|
|
@@ -55,7 +55,9 @@ import { cache, revalidate, loading, errorBoundary, middleware } from "@rangojs/
|
|
|
55
55
|
// Shared caching configuration
|
|
56
56
|
const withCaching = () => [
|
|
57
57
|
cache({ ttl: 600_000 }),
|
|
58
|
-
|
|
58
|
+
// Defer on navigation (|| undefined) so each route keeps its own param/search
|
|
59
|
+
// revalidation default; only force a re-run when an action ran.
|
|
60
|
+
revalidate(({ actionId }) => (actionId ? true : undefined)),
|
|
59
61
|
];
|
|
60
62
|
|
|
61
63
|
// Shared loading and error handling
|
|
@@ -71,6 +73,29 @@ const withAuth = () => [
|
|
|
71
73
|
];
|
|
72
74
|
```
|
|
73
75
|
|
|
76
|
+
> **Factories compose logic, not just values.** A `revalidate()` predicate in a
|
|
77
|
+
> shared factory applies its logic to _every_ route that composes it, so a
|
|
78
|
+
> footgun here is amplified across the app. Two rules:
|
|
79
|
+
>
|
|
80
|
+
> 1. Use `|| undefined` (defer), not `?? false` (hard short-circuit), in shared
|
|
81
|
+
> predicates — a hard `false` ends the chain and overrides each consuming
|
|
82
|
+
> route's own default, and a downstream revalidator never runs. See `/loader`
|
|
83
|
+
> → "`|| undefined` (defer) vs `?? false` (hard)".
|
|
84
|
+
> 2. Match actions with `ctx.isAction(Action)`, not an inline
|
|
85
|
+
> `actionId.includes("…")` buried in a factory: it resolves the action from an
|
|
86
|
+
> imported reference, so a rename is a compile error in one place instead of
|
|
87
|
+
> silent drift across every consumer.
|
|
88
|
+
>
|
|
89
|
+
> Remember the axis: a factory's `revalidate()` controls client-update
|
|
90
|
+
> selection, while its `cache()` controls stored-value freshness. They are
|
|
91
|
+
> independent even when bundled in the same factory (`/cache-guide` → "Two axes").
|
|
92
|
+
|
|
93
|
+
> **Keep factories small and intention-named.** The anti-pattern that kills
|
|
94
|
+
> readability is over-bundling — a `withDefaults()` that secretly adds five
|
|
95
|
+
> things — and factory-of-factories nesting (leaning on `.flat(3)`). Surprising
|
|
96
|
+
> config stays inline; extract only the boring, repeated parts; compose by
|
|
97
|
+
> _naming concerns_ (`withAuth()`, `withCaching()`), not by hiding them.
|
|
98
|
+
|
|
74
99
|
## Using Factories in Routes
|
|
75
100
|
|
|
76
101
|
Place factory calls inside `path()` or `layout()` use callbacks. The returned arrays are flattened automatically (up to 3 levels):
|
|
@@ -107,7 +132,7 @@ import { authMiddleware } from "./middleware/auth";
|
|
|
107
132
|
|
|
108
133
|
export const withPublicDefaults = () => [
|
|
109
134
|
cache({ ttl: 300 }),
|
|
110
|
-
revalidate(({ actionId }) =>
|
|
135
|
+
revalidate(({ actionId }) => (actionId ? true : undefined)),
|
|
111
136
|
];
|
|
112
137
|
|
|
113
138
|
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`.
|