@rangojs/router 0.0.0-experimental.132 → 0.0.0-experimental.133
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 +43 -2
- package/dist/bin/rango.js +92 -16
- package/dist/vite/index.js +166 -70
- package/package.json +19 -18
- package/skills/breadcrumbs/SKILL.md +1 -1
- package/skills/bundle-analysis/SKILL.md +2 -2
- package/skills/cache-guide/SKILL.md +2 -2
- package/skills/caching/SKILL.md +16 -9
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/hooks/SKILL.md +2 -2
- package/skills/host-router/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +1 -1
- package/skills/loader/SKILL.md +2 -0
- package/skills/migrate-react-router/SKILL.md +4 -2
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/prerender/SKILL.md +2 -0
- package/skills/rango/SKILL.md +12 -11
- package/skills/response-routes/SKILL.md +2 -2
- package/skills/route/SKILL.md +4 -0
- package/skills/router-setup/SKILL.md +3 -0
- package/skills/scripts/SKILL.md +179 -0
- package/skills/testing/SKILL.md +1 -1
- package/skills/testing/bindings.md +20 -6
- package/skills/testing/cache-prerender.md +5 -2
- package/skills/testing/client-components.md +2 -0
- package/skills/testing/e2e-parity.md +1 -1
- package/skills/testing/flight.md +8 -9
- package/skills/testing/render-handler.md +1 -1
- package/skills/testing/response-routes.md +1 -1
- package/skills/testing/server-actions.md +11 -11
- package/skills/testing/setup.md +3 -0
- package/skills/typesafety/SKILL.md +3 -2
- package/skills/use-cache/SKILL.md +10 -9
- package/src/browser/event-controller.ts +109 -2
- package/src/browser/partial-update.ts +12 -0
- package/src/browser/prefetch/cache.ts +17 -0
- package/src/browser/prefetch/fetch.ts +69 -2
- package/src/browser/react/Link.tsx +30 -5
- package/src/browser/react/NavigationProvider.tsx +12 -2
- package/src/browser/react/location-state-shared.ts +14 -2
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +23 -2
- package/src/browser/response-adapter.ts +14 -3
- package/src/browser/rsc-router.tsx +3 -0
- package/src/browser/scroll-restoration.ts +8 -3
- package/src/browser/server-action-bridge.ts +46 -11
- package/src/browser/types.ts +6 -0
- package/src/build/generate-route-types.ts +0 -1
- package/src/build/route-trie.ts +33 -9
- package/src/build/route-types/include-resolution.ts +7 -1
- package/src/build/route-types/router-processing.ts +0 -6
- package/src/build/route-types/source-scan.ts +105 -7
- package/src/cache/cache-policy.ts +42 -8
- package/src/cache/cache-runtime.ts +65 -5
- package/src/cache/cache-scope.ts +71 -11
- package/src/cache/cache-tag.ts +7 -2
- 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 +85 -613
- 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/document-cache.ts +11 -0
- package/src/cache/handle-snapshot.ts +8 -1
- package/src/cache/profile-registry.ts +25 -1
- package/src/cache/segment-codec.ts +9 -1
- package/src/cache/types.ts +4 -0
- package/src/client.rsc.tsx +38 -0
- package/src/client.tsx +11 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +1 -1
- package/src/decode-loader-results.ts +7 -1
- package/src/escape-script.ts +52 -0
- package/src/handles/MetaTags.tsx +56 -5
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +29 -11
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +46 -0
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +7 -3
- package/src/host/pattern-matcher.ts +16 -2
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +5 -0
- package/src/response-utils.ts +25 -0
- package/src/route-definition/dsl-helpers.ts +7 -0
- package/src/route-definition/redirect.ts +1 -2
- package/src/router/content-negotiation.ts +58 -10
- package/src/router/intercept-resolution.ts +9 -0
- package/src/router/match-middleware/cache-store.ts +10 -1
- package/src/router/middleware.ts +10 -3
- package/src/router/pattern-matching.ts +25 -23
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/router-interfaces.ts +7 -0
- package/src/router/router-options.ts +23 -0
- package/src/router/segment-resolution/fresh.ts +10 -0
- package/src/router/segment-resolution/helpers.ts +35 -1
- package/src/router/segment-resolution/loader-cache.ts +10 -6
- package/src/router/segment-resolution/revalidation.ts +6 -0
- package/src/router/segment-resolution.ts +1 -0
- package/src/router/trie-matching.ts +14 -9
- package/src/router.ts +18 -10
- package/src/rsc/handler.ts +52 -13
- package/src/rsc/helpers.ts +7 -1
- package/src/rsc/index.ts +1 -4
- package/src/rsc/loader-fetch.ts +107 -37
- package/src/rsc/progressive-enhancement.ts +18 -6
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-route-handler.ts +16 -133
- package/src/rsc/rsc-rendering.ts +13 -4
- package/src/rsc/server-action.ts +52 -6
- package/src/rsc/types.ts +7 -0
- package/src/search-params.ts +24 -5
- package/src/segment-loader-promise.ts +17 -2
- package/src/server/loader-registry.ts +16 -18
- package/src/server/request-context.ts +47 -20
- package/src/testing/dispatch.ts +108 -25
- package/src/testing/flight.ts +25 -0
- package/src/testing/internal/context.ts +25 -2
- package/src/testing/render-handler.ts +3 -1
- package/src/testing/render-route.tsx +15 -0
- package/src/testing/run-loader.ts +10 -3
- package/src/theme/ThemeProvider.tsx +20 -6
- package/src/theme/ThemeScript.tsx +7 -3
- package/src/theme/constants.ts +54 -3
- package/src/theme/theme-script.ts +22 -7
- package/src/types/request-scope.ts +8 -3
- package/src/vite/plugins/cjs-to-esm.ts +8 -1
- package/src/vite/plugins/expose-id-utils.ts +10 -1
- package/src/vite/plugins/expose-ids/handler-transform.ts +5 -16
- package/src/vite/plugins/expose-ids/loader-transform.ts +12 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +6 -1
- package/src/vite/plugins/expose-internal-ids.ts +0 -1
- package/src/vite/plugins/version-plugin.ts +5 -17
- package/src/vite/plugins/virtual-entries.ts +12 -2
- package/src/vite/rango.ts +15 -6
- package/src/vite/utils/ast-handler-extract.ts +11 -4
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/prerender-utils.ts +17 -2
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Types
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Shared public types for the CF cache store: the minimal KVNamespace shape, the
|
|
6
|
+
// debug-event surface, and the store options. Extracted from cf-cache-store.ts
|
|
7
|
+
// so they can be referenced without importing the class; re-exported from
|
|
8
|
+
// cf-cache-store.ts so existing import paths still resolve. The private KV
|
|
9
|
+
// envelope interfaces stay in cf-cache-store.ts with the methods that use them.
|
|
10
|
+
|
|
11
|
+
// Imported from the canonical home (also publicly exported from src/index.ts /
|
|
12
|
+
// src/index.rsc.ts) so this module shares the one interface rather than
|
|
13
|
+
// declaring a second that could drift.
|
|
14
|
+
import type { ExecutionContext } from "../../types/request-scope.js";
|
|
15
|
+
import type { CacheDefaults } from "../types.js";
|
|
16
|
+
import type { RequestContext } from "../../server/request-context.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Minimal Cloudflare KV Namespace interface.
|
|
20
|
+
* Avoids hard dependency on @cloudflare/workers-types.
|
|
21
|
+
*/
|
|
22
|
+
export interface KVNamespace {
|
|
23
|
+
get(key: string, options?: { type?: string }): Promise<any>;
|
|
24
|
+
put(
|
|
25
|
+
key: string,
|
|
26
|
+
value: string,
|
|
27
|
+
options?: { expirationTtl?: number },
|
|
28
|
+
): Promise<void>;
|
|
29
|
+
delete(key: string): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* One L1 read decision, surfaced when `debug` is enabled. Lets an operator
|
|
34
|
+
* confirm on a real deployment (e.g. via `wrangler tail`) that the store's
|
|
35
|
+
* observed inputs match its decision: which tier answered, the entry's status,
|
|
36
|
+
* the stale/revalidating timestamps, the raw CF `Age` header (so its
|
|
37
|
+
* unreliability can be seen next to the explicit revalidating-at stamp), and
|
|
38
|
+
* the measured match/body-read durations (where the latency tail shows up).
|
|
39
|
+
*/
|
|
40
|
+
export interface CFCacheReadDebugEvent {
|
|
41
|
+
/**
|
|
42
|
+
* Which read method produced this event. Only the JSON read paths (segment
|
|
43
|
+
* `get` and function `getItem`) participate in debug; the document
|
|
44
|
+
* `getResponse` path streams its body and is intentionally out of scope.
|
|
45
|
+
*/
|
|
46
|
+
op: "get" | "getItem";
|
|
47
|
+
/** Cache key (without the internal fn:/doc: prefix or version path). */
|
|
48
|
+
key: string;
|
|
49
|
+
/**
|
|
50
|
+
* What the read resolved to:
|
|
51
|
+
* - l1-fresh / l1-stale-revalidate / l1-revalidating-guarded: L1 hit outcomes
|
|
52
|
+
* - match-timeout / body-timeout: the L1 latency budgets fired
|
|
53
|
+
* - match-error: the L1 match() itself rejected (a transient Cache API infra
|
|
54
|
+
* error) -- a miss that falls through to L2/KV and is reported cache-read,
|
|
55
|
+
* distinct from a genuine l1-miss (absence) so the two are separable
|
|
56
|
+
* - body-error: the L1 body read failed fast (corrupt/non-JSON body) -- a miss
|
|
57
|
+
* that falls through to L2/KV, distinct from a body-timeout
|
|
58
|
+
* - non-200: L1 returned a non-200 (treated as a miss)
|
|
59
|
+
* - l1-miss: no L1 entry
|
|
60
|
+
* - kv-fresh / kv-stale / kv-miss: L2 fallback outcomes
|
|
61
|
+
* - kv-stale-suppressed: a stale L2 hit served WITHOUT revalidation because
|
|
62
|
+
* the L1 fall-through was degraded (body-timeout / non-200) -- the herd
|
|
63
|
+
* mitigation, distinct from kv-stale so the suppression is visible
|
|
64
|
+
* - kv-timeout: the L2/KV read budget fired (read abandoned, NOT a genuine
|
|
65
|
+
* absence -- distinct from kv-miss so a degradation signal is separable)
|
|
66
|
+
* - tag-invalidated: a live L1/KV entry whose cache tags were invalidated
|
|
67
|
+
* after it was written -- treated as a miss so the next render re-populates
|
|
68
|
+
* it (the tag-invalidation read path, distinct from a plain miss)
|
|
69
|
+
* - error: the read threw
|
|
70
|
+
*/
|
|
71
|
+
outcome:
|
|
72
|
+
| "l1-fresh"
|
|
73
|
+
| "l1-stale-revalidate"
|
|
74
|
+
| "l1-revalidating-guarded"
|
|
75
|
+
| "match-timeout"
|
|
76
|
+
| "match-error"
|
|
77
|
+
| "body-timeout"
|
|
78
|
+
| "body-error"
|
|
79
|
+
| "non-200"
|
|
80
|
+
| "tag-invalidated"
|
|
81
|
+
| "l1-miss"
|
|
82
|
+
| "kv-fresh"
|
|
83
|
+
| "kv-stale"
|
|
84
|
+
| "kv-stale-suppressed"
|
|
85
|
+
| "kv-miss"
|
|
86
|
+
| "kv-timeout"
|
|
87
|
+
| "error";
|
|
88
|
+
/** HTTP status of the matched L1 response, when one was returned. */
|
|
89
|
+
status?: number;
|
|
90
|
+
/**
|
|
91
|
+
* Stored cache status header (CACHE_STATUS_HEADER): "HIT" or "REVALIDATING".
|
|
92
|
+
* Distinct from `isRevalidating`, which also factors in stamp recency -- this
|
|
93
|
+
* is the raw stored value, so a REVALIDATING entry whose stamp aged out (so
|
|
94
|
+
* `isRevalidating` is false) is still distinguishable from a plain HIT.
|
|
95
|
+
*/
|
|
96
|
+
cacheStatus?: string | null;
|
|
97
|
+
/** Epoch-ms when the entry goes stale (from CACHE_STALE_AT_HEADER). */
|
|
98
|
+
staleAt?: number;
|
|
99
|
+
/** Epoch-ms the entry was marked REVALIDATING (from the explicit stamp). */
|
|
100
|
+
revalidatingAt?: number;
|
|
101
|
+
/** Raw CF `Age` header, for comparison against revalidatingAt (may be null). */
|
|
102
|
+
ageHeader?: string | null;
|
|
103
|
+
isStale?: boolean;
|
|
104
|
+
isRevalidating?: boolean;
|
|
105
|
+
shouldRevalidate?: boolean;
|
|
106
|
+
/** Wall-clock ms spent in cache.match (bounded by edgeLookupTimeoutMs). */
|
|
107
|
+
matchMs?: number;
|
|
108
|
+
/**
|
|
109
|
+
* Wall-clock ms spent resolving the entry's tag-invalidation markers (the
|
|
110
|
+
* per-request memo -> optional per-colo L1 marker cache -> KV cascade), for a
|
|
111
|
+
* tagged entry. 0/absent for an untagged entry or a memo hit; a non-trivial
|
|
112
|
+
* value is the serial marker-read tail that sits between matchMs and
|
|
113
|
+
* bodyReadMs. Only measured when debug is enabled.
|
|
114
|
+
*/
|
|
115
|
+
markerMs?: number;
|
|
116
|
+
/** Wall-clock ms spent reading the body (bounded by edgeReadTimeoutMs). */
|
|
117
|
+
bodyReadMs?: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Debug sink. `true` logs each {@link CFCacheReadDebugEvent} to console; a
|
|
122
|
+
* function receives the events for programmatic capture.
|
|
123
|
+
*/
|
|
124
|
+
export type CFCacheDebug = boolean | ((event: CFCacheReadDebugEvent) => void);
|
|
125
|
+
|
|
126
|
+
export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
127
|
+
/**
|
|
128
|
+
* Cache namespace. If not provided, uses caches.default (recommended).
|
|
129
|
+
* Only set this if you need isolated cache storage.
|
|
130
|
+
*/
|
|
131
|
+
namespace?: string;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Base URL for cache keys.
|
|
135
|
+
*
|
|
136
|
+
* If not provided, derives from request hostname via requestContext:
|
|
137
|
+
* - Production domains → uses `https://{hostname}/`
|
|
138
|
+
* - Dev/preview (localhost, workers.dev, pages.dev) → uses internal fallback URL
|
|
139
|
+
*/
|
|
140
|
+
baseUrl?: string;
|
|
141
|
+
|
|
142
|
+
/** Default cache options */
|
|
143
|
+
defaults?: CacheDefaults;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Cloudflare ExecutionContext for non-blocking cache writes.
|
|
147
|
+
* Pass the `ctx` from your worker's fetch handler.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* new CFCacheStore({ ctx: env.ctx })
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
ctx: ExecutionContext;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Optional KV namespace for L2 cache persistence.
|
|
158
|
+
*
|
|
159
|
+
* When provided, KV acts as a global fallback behind the per-colo Cache API.
|
|
160
|
+
* On L1 miss, KV is checked and hits are promoted back to L1.
|
|
161
|
+
* On writes, data is persisted to both L1 and KV.
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```typescript
|
|
165
|
+
* new CFCacheStore({ ctx: env.ctx, kv: env.CACHE_KV })
|
|
166
|
+
* ```
|
|
167
|
+
*
|
|
168
|
+
* Tag-based invalidation (updateTag/revalidateTag) requires KV: the
|
|
169
|
+
* tag-invalidation markers are stored in this same namespace. There is no
|
|
170
|
+
* separate tag-invalidation store to configure.
|
|
171
|
+
*/
|
|
172
|
+
kv?: KVNamespace;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Optional eager-purge hook, called ONCE per updateTag()/revalidateTag() with
|
|
176
|
+
* the namespaced Cloudflare Cache-Tags to purge (one batched call for the
|
|
177
|
+
* whole invalidation, not one per tag). These exactly match the `Cache-Tag`
|
|
178
|
+
* header this store writes on its tag-lookup marker entries
|
|
179
|
+
* (`rg:{namespace}:lk:{encodedTag}`), so forwarding them to Cloudflare's
|
|
180
|
+
* purge-by-tag API evicts the cached lookups in every colo - making
|
|
181
|
+
* cross-colo invalidation prompt instead of waiting out `tagCacheTtl`.
|
|
182
|
+
*
|
|
183
|
+
* Only meaningful with `tagCacheTtl > 0` (otherwise there are no cached
|
|
184
|
+
* lookups to purge). The values are pre-encoded, so commas in tag names are
|
|
185
|
+
* safe to pass straight to the purge API.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```ts
|
|
189
|
+
* onRevalidateTag: async (cacheTags) => {
|
|
190
|
+
* await fetch(`https://api.cloudflare.com/client/v4/zones/${ZONE}/purge_cache`, {
|
|
191
|
+
* method: "POST",
|
|
192
|
+
* headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
|
|
193
|
+
* body: JSON.stringify({ tags: cacheTags }),
|
|
194
|
+
* });
|
|
195
|
+
* }
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
onRevalidateTag?: (cacheTags: string[]) => Promise<void>;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Optional expiration (seconds) for tag-invalidation markers in KV. A marker
|
|
202
|
+
* must outlive every entry tagged before the invalidation, so this MUST
|
|
203
|
+
* exceed your largest entry TTL+SWR. Defaults to no expiration (markers
|
|
204
|
+
* persist; they are tiny - one timestamp per distinct invalidated tag).
|
|
205
|
+
*
|
|
206
|
+
* Note the opposite sizing from `tagCacheTtl` below: `tagInvalidationTtl` must
|
|
207
|
+
* be LARGE (outlive data); `tagCacheTtl` should be SMALL (a staleness ceiling).
|
|
208
|
+
*
|
|
209
|
+
* Cardinality matters: each DISTINCT invalidated tag writes one permanent KV
|
|
210
|
+
* marker (with the no-expiry default). Keep tags LOW-cardinality and never
|
|
211
|
+
* derive an invalidation tag from untrusted input (e.g.
|
|
212
|
+
* `revalidateTag(req.query.tag)`) - an attacker could otherwise grow your KV
|
|
213
|
+
* namespace without bound. Set a `tagInvalidationTtl` only if your tags are
|
|
214
|
+
* unavoidably high-cardinality AND it can still safely exceed your max entry
|
|
215
|
+
* TTL+SWR.
|
|
216
|
+
*/
|
|
217
|
+
tagInvalidationTtl?: number;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Optional TTL (seconds) for caching tag-invalidation markers in the per-colo
|
|
221
|
+
* Cache API (L1), to avoid a KV marker read on every tagged cache read.
|
|
222
|
+
*
|
|
223
|
+
* Default `0` = disabled: the marker is read from KV on every tagged read
|
|
224
|
+
* (today's behavior), giving the strongest cross-colo invalidation latency
|
|
225
|
+
* (~KV consistency). A positive value caches each marker (including the
|
|
226
|
+
* "no marker yet" state) in L1 for that many seconds, so within the window a
|
|
227
|
+
* colo answers from L1 with no KV read.
|
|
228
|
+
*
|
|
229
|
+
* The colo that runs `updateTag`/`revalidateTag` writes the fresh marker
|
|
230
|
+
* straight into its own L1 (write-through), so the invalidating request and
|
|
231
|
+
* later reads in that colo observe the invalidation immediately. One caveat: a
|
|
232
|
+
* read already in flight when the invalidation lands (one that began its KV
|
|
233
|
+
* marker fetch first) can re-cache the PRIOR marker into L1 after the
|
|
234
|
+
* write-through, so a racing concurrent reader in the same colo may miss the
|
|
235
|
+
* invalidation for up to `tagCacheTtl` -- the Cache API exposes no
|
|
236
|
+
* compare-and-set to close this fully. `tagCacheTtl` is therefore a staleness
|
|
237
|
+
* CEILING, not a promise of zero same-colo latency; keep it small (or wire
|
|
238
|
+
* `onRevalidateTag`) when that matters. By default OTHER colos only converge
|
|
239
|
+
* when their cached marker expires, so `tagCacheTtl` is the MAXIMUM extra
|
|
240
|
+
* cross-colo invalidation latency for them. Recommended 30-60 for high-read,
|
|
241
|
+
* low-mutation tags; leave at 0 when prompt global invalidation matters and
|
|
242
|
+
* you cannot wire a purge.
|
|
243
|
+
*
|
|
244
|
+
* To make other colos prompt WITHOUT a short TTL, wire `onRevalidateTag` to a
|
|
245
|
+
* Cloudflare purge-by-tag call: each marker entry carries a namespaced
|
|
246
|
+
* `Cache-Tag`, and `onRevalidateTag` is handed exactly those tags to purge, so
|
|
247
|
+
* the cached lookups are evicted everywhere on invalidation. With a purge
|
|
248
|
+
* wired, `tagCacheTtl` becomes purely a read-cost reducer + fallback window
|
|
249
|
+
* (safe to set large) rather than the invalidation-latency ceiling.
|
|
250
|
+
*/
|
|
251
|
+
tagCacheTtl?: number;
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Cache version string override. When this changes, all cached entries are
|
|
255
|
+
* effectively invalidated (new keys won't match old entries).
|
|
256
|
+
*
|
|
257
|
+
* Defaults to the auto-generated VERSION from the `@rangojs/router:version` virtual module.
|
|
258
|
+
* Only set this if you need a custom versioning strategy.
|
|
259
|
+
*/
|
|
260
|
+
version?: string;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Latency budget (ms) for an L1 edge cache (CF Cache API) read. A `match`
|
|
264
|
+
* slower than this is abandoned and treated as a miss, so a degraded colo
|
|
265
|
+
* cannot stall the request; the read then falls through to its normal miss
|
|
266
|
+
* path (L2/KV or render).
|
|
267
|
+
*
|
|
268
|
+
* Defaults to {@link EDGE_LOOKUP_TIMEOUT_MS} (10). Set to 0 (or any value
|
|
269
|
+
* <= 0) to disable the budget and always await `match`.
|
|
270
|
+
*/
|
|
271
|
+
edgeLookupTimeoutMs?: number;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Latency budget (ms) for reading the BODY of a matched L1 entry
|
|
275
|
+
* (response.json()). CF streams the cache body lazily, so the multi-second
|
|
276
|
+
* tail can appear after `match` already resolved; this bounds it. On timeout
|
|
277
|
+
* the read is treated as a miss and falls through to L2/KV or render.
|
|
278
|
+
*
|
|
279
|
+
* Separate from {@link edgeLookupTimeoutMs} because a healthy body read
|
|
280
|
+
* (fetch + JSON parse of a potentially large Flight payload) takes a little
|
|
281
|
+
* longer than a `match`. Defaults to {@link EDGE_READ_TIMEOUT_MS} (20), which
|
|
282
|
+
* clears a healthy per-colo read yet fails fast on a degraded one. Set to 0
|
|
283
|
+
* (or any value <= 0) to disable and always await the body.
|
|
284
|
+
*/
|
|
285
|
+
edgeReadTimeoutMs?: number;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Latency budget (ms) for an L2 (KV) read. KV is the last cache tier before a
|
|
289
|
+
* full render and is a global store (~50ms healthy, seconds when degraded);
|
|
290
|
+
* this bounds it so a slow namespace cannot pin the request. On timeout the
|
|
291
|
+
* read is treated as a miss (no L1 promote) and falls through to render.
|
|
292
|
+
*
|
|
293
|
+
* Defaults to {@link KV_READ_TIMEOUT_MS} (170) -- a few multiples above the
|
|
294
|
+
* ~50ms healthy read, with headroom for legitimate tails (large payloads / far
|
|
295
|
+
* regions) yet still well under a degraded namespace's multi-second tail.
|
|
296
|
+
* Lower it for a tighter SLA, raise it if your healthy KV p99 runs higher; it
|
|
297
|
+
* is a degradation guard-rail, not a tuning lever. Set to 0 (or any value
|
|
298
|
+
* <= 0) to disable and always await KV.
|
|
299
|
+
*/
|
|
300
|
+
kvReadTimeoutMs?: number;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Emit a {@link CFCacheReadDebugEvent} per L1 read. `true` logs to console
|
|
304
|
+
* (visible via `wrangler tail`); pass a function to capture events directly.
|
|
305
|
+
* Off by default. Intended for validating cache behavior on a real
|
|
306
|
+
* deployment before relying on it; not for steady-state production.
|
|
307
|
+
*/
|
|
308
|
+
debug?: CFCacheDebug;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Custom key generator applied to all cache operations.
|
|
312
|
+
* Receives the full RequestContext (including env) and the default-generated key.
|
|
313
|
+
* Return value becomes the final cache key (unless route overrides with `key` option).
|
|
314
|
+
*
|
|
315
|
+
* Reserved prefixes: tag-invalidation markers live in the SAME KV namespace as
|
|
316
|
+
* data, keyed `__tag__/<tag>` (and `__tagmarker__/<tag>` for the L1 cache). A
|
|
317
|
+
* returned key must NOT begin with `__tag__/` or `__tagmarker__/`, or it can
|
|
318
|
+
* collide with a tag marker and corrupt invalidation. The documented
|
|
319
|
+
* prepend-style generators below are safe.
|
|
320
|
+
*
|
|
321
|
+
* @example Using headers for user segmentation
|
|
322
|
+
* ```typescript
|
|
323
|
+
* keyGenerator: (ctx, defaultKey) => {
|
|
324
|
+
* const segment = ctx.request.headers.get('x-user-segment') || 'default';
|
|
325
|
+
* return `${segment}:${defaultKey}`;
|
|
326
|
+
* }
|
|
327
|
+
* ```
|
|
328
|
+
*
|
|
329
|
+
* @example Using env bindings for multi-region
|
|
330
|
+
* ```typescript
|
|
331
|
+
* keyGenerator: (ctx, defaultKey) => {
|
|
332
|
+
* const region = ctx.env.REGION || 'us';
|
|
333
|
+
* return `${region}:${defaultKey}`;
|
|
334
|
+
* }
|
|
335
|
+
* ```
|
|
336
|
+
*
|
|
337
|
+
* @example Using cookies for locale-aware caching
|
|
338
|
+
* ```typescript
|
|
339
|
+
* keyGenerator: (ctx, defaultKey) => {
|
|
340
|
+
* const locale = cookies().get('locale')?.value || 'en';
|
|
341
|
+
* return `${locale}:${defaultKey}`;
|
|
342
|
+
* }
|
|
343
|
+
* ```
|
|
344
|
+
*/
|
|
345
|
+
keyGenerator?: (
|
|
346
|
+
ctx: RequestContext<TEnv>,
|
|
347
|
+
defaultKey: string,
|
|
348
|
+
) => string | Promise<string>;
|
|
349
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// KV utilities
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Pure helpers for the CF cache store's KV (L2) tier: key byte-length limits,
|
|
6
|
+
// the expirationTtl floor, and the stale-path Cache-Control recompute. None of
|
|
7
|
+
// these reference the store instance, so they live here as standalone functions.
|
|
8
|
+
|
|
9
|
+
import { CACHE_EXPIRES_AT_HEADER } from "./cf-cache-constants.js";
|
|
10
|
+
|
|
11
|
+
/** KV key byte-length ceiling. Cloudflare KV rejects keys larger than this. */
|
|
12
|
+
export const KV_MAX_KEY_BYTES = 512;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Cloudflare KV's minimum `expirationTtl` (seconds). A `put` with a smaller
|
|
16
|
+
* expirationTtl is rejected outright. Tag-invalidation markers (the only writes
|
|
17
|
+
* that take a consumer-supplied TTL via tagInvalidationTtl) are floored to this
|
|
18
|
+
* so a too-small value cannot make EVERY updateTag/revalidateTag throw.
|
|
19
|
+
*/
|
|
20
|
+
export const KV_MIN_EXPIRATION_TTL = 60;
|
|
21
|
+
|
|
22
|
+
const kvKeyEncoder = new TextEncoder();
|
|
23
|
+
|
|
24
|
+
/** UTF-8 byte length of a KV key (multibyte tags can exceed the char count). */
|
|
25
|
+
export function kvKeyByteLength(key: string): number {
|
|
26
|
+
return kvKeyEncoder.encode(key).length;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Compute the Cache-Control directive for a stale-path REVALIDATING re-put from
|
|
31
|
+
* the entry's stored hard-expiry deadline (CACHE_EXPIRES_AT_HEADER). Returns the
|
|
32
|
+
* REMAINING ttl so the re-put preserves the original retention deadline instead
|
|
33
|
+
* of restarting it -- copying set()'s original full-window max-age would reset
|
|
34
|
+
* CF's retention clock on every re-arm and pin a perpetually-stale entry forever.
|
|
35
|
+
* An entry lacking a valid deadline (legacy/tampered) floors to max-age=1, so it
|
|
36
|
+
* hard-expires in ~1s and self-heals via KV. Mirrors promoteSegmentToL1's math.
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
export function remainingCacheControl(headers: Headers, now: number): string {
|
|
40
|
+
const expiresAt = Number(headers.get(CACHE_EXPIRES_AT_HEADER));
|
|
41
|
+
const remainingTtl =
|
|
42
|
+
Number.isFinite(expiresAt) && expiresAt > 0
|
|
43
|
+
? Math.max(1, Math.floor((expiresAt - now) / 1000))
|
|
44
|
+
: 1;
|
|
45
|
+
return `public, max-age=${remainingTtl}`;
|
|
46
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Tag-marker memo
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Per-request memoization of tag-invalidation marker reads, plus the prefix and
|
|
6
|
+
// sentinel used by the optional per-colo L1 marker cache. Extracted from
|
|
7
|
+
// cf-cache-store.ts; the WeakMaps stay MODULE SINGLETONS here (the module is
|
|
8
|
+
// evaluated once), so binding-keyed memo semantics are unchanged: the maps are
|
|
9
|
+
// keyed by the request-context object then the store INSTANCE, identical to
|
|
10
|
+
// before the move.
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Cache-API path prefix for the optional per-colo L1 cache of tag-invalidation
|
|
14
|
+
* markers (enabled by tagCacheTtl). Distinct from data keys (doc:/fn:/segment)
|
|
15
|
+
* and from the KV marker prefix so the two never collide.
|
|
16
|
+
*/
|
|
17
|
+
export const TAG_MARKER_CACHE_PREFIX = "__tagmarker__/";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Sentinel body for an L1-cached marker meaning "this tag has no invalidation
|
|
21
|
+
* marker." Distinct from any real ms-epoch timestamp (always a large positive
|
|
22
|
+
* integer). A Cache API miss (match() === undefined) always means "re-read KV",
|
|
23
|
+
* never "no marker" - absence is only ever represented by this cached sentinel.
|
|
24
|
+
*/
|
|
25
|
+
export const TAG_MARKER_ABSENT = "none";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Per-request memo of tag-invalidation markers (tag -> latest invalidatedAt, or
|
|
29
|
+
* null when no marker exists). Keyed first by the request context object (so it
|
|
30
|
+
* is naturally request-scoped and garbage-collected with the request) and then
|
|
31
|
+
* by the store INSTANCE.
|
|
32
|
+
*
|
|
33
|
+
* The per-store nesting matters because a single request can run more than one
|
|
34
|
+
* CFCacheStore - the app-level store plus a route's `cache({ store })` override,
|
|
35
|
+
* which may point at a DIFFERENT KV binding or version. A module-level map keyed
|
|
36
|
+
* by request alone (the inner map keyed by the raw tag name) would let store B's
|
|
37
|
+
* memoized marker for a tag mask store A's own KV marker, so A could serve an
|
|
38
|
+
* entry A's own KV says is invalidated. Keying by the instance isolates them;
|
|
39
|
+
* two reads through the SAME store still share the memo. A read through one
|
|
40
|
+
* store never populates another's memo, so each store always consults its own KV
|
|
41
|
+
* binding. Markers are read only through isGloballyInvalidated(), which already
|
|
42
|
+
* short-circuits when a store has no KV, so a store without KV never allocates.
|
|
43
|
+
*
|
|
44
|
+
* Without the memo, isGloballyInvalidated() issues a KV read per tag on every
|
|
45
|
+
* tagged cache read, so a page composed of many segments/items sharing a tag
|
|
46
|
+
* pays that cost N times. The memo collapses it to one KV read per distinct tag
|
|
47
|
+
* per (request, store). invalidateTags() writes through so a same-request
|
|
48
|
+
* updateTag() stays read-your-own-writes consistent (the action's own re-render
|
|
49
|
+
* sees its own invalidation from the memo, without a re-read).
|
|
50
|
+
*
|
|
51
|
+
* It does NOT span requests, so a hot single-entry route still pays one KV read
|
|
52
|
+
* per request; that read hits Cloudflare KV's own edge read cache for hot keys.
|
|
53
|
+
*/
|
|
54
|
+
const tagMarkerMemo = new WeakMap<
|
|
55
|
+
object,
|
|
56
|
+
WeakMap<object, Map<string, number | null>>
|
|
57
|
+
>();
|
|
58
|
+
|
|
59
|
+
export function getTagMarkerMemo(
|
|
60
|
+
ctx: object,
|
|
61
|
+
store: object,
|
|
62
|
+
): Map<string, number | null> {
|
|
63
|
+
let byStore = tagMarkerMemo.get(ctx);
|
|
64
|
+
if (!byStore) {
|
|
65
|
+
byStore = new WeakMap();
|
|
66
|
+
tagMarkerMemo.set(ctx, byStore);
|
|
67
|
+
}
|
|
68
|
+
let memo = byStore.get(store);
|
|
69
|
+
if (!memo) {
|
|
70
|
+
memo = new Map();
|
|
71
|
+
byStore.set(store, memo);
|
|
72
|
+
}
|
|
73
|
+
return memo;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Per-request map of IN-FLIGHT marker reads (tag -> the pending read promise).
|
|
78
|
+
* The resolved-value memo above only collapses SEQUENTIAL reads of a tag; the
|
|
79
|
+
* router resolves sibling segments in PARALLEL, so without this several
|
|
80
|
+
* concurrently-resolving segments sharing a tag would each issue their own KV
|
|
81
|
+
* read before any of them populates the memo. Sharing the in-flight promise
|
|
82
|
+
* collapses those to a single KV read. Entries are dropped once resolved (the
|
|
83
|
+
* value is then in the memo), so this only spans the concurrent read window.
|
|
84
|
+
*/
|
|
85
|
+
const tagMarkerInflight = new WeakMap<
|
|
86
|
+
object,
|
|
87
|
+
WeakMap<object, Map<string, Promise<number | null>>>
|
|
88
|
+
>();
|
|
89
|
+
|
|
90
|
+
export function getTagMarkerInflight(
|
|
91
|
+
ctx: object,
|
|
92
|
+
store: object,
|
|
93
|
+
): Map<string, Promise<number | null>> {
|
|
94
|
+
let byStore = tagMarkerInflight.get(ctx);
|
|
95
|
+
if (!byStore) {
|
|
96
|
+
byStore = new WeakMap();
|
|
97
|
+
tagMarkerInflight.set(ctx, byStore);
|
|
98
|
+
}
|
|
99
|
+
let inflight = byStore.get(store);
|
|
100
|
+
if (!inflight) {
|
|
101
|
+
inflight = new Map();
|
|
102
|
+
byStore.set(store, inflight);
|
|
103
|
+
}
|
|
104
|
+
return inflight;
|
|
105
|
+
}
|
|
@@ -65,6 +65,17 @@ function parseCacheControl(header: string | null): CacheDirectives | null {
|
|
|
65
65
|
return null;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// RFC 7234 §5.2.2.2: a shared cache MUST NOT serve a stored `no-cache`
|
|
69
|
+
// response without successful origin validation. This store's hit path has no
|
|
70
|
+
// validation step, so serving a stored no-cache response within s-maxage
|
|
71
|
+
// would hand the client content the origin marked must-revalidate. Refuse to
|
|
72
|
+
// store it. Only UNqualified `no-cache` (no `=`) vetoes — the field-name-
|
|
73
|
+
// scoped `no-cache="set-cookie"` form IS storable per the RFC, so the `=`
|
|
74
|
+
// boundary is excluded from the lookahead (unlike private/no-store above).
|
|
75
|
+
if (/(^|[\s,;])no-cache(?=$|[\s,;])/i.test(header)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
68
79
|
const directives: CacheDirectives = {};
|
|
69
80
|
|
|
70
81
|
// Parse s-maxage
|
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
import type { ResolvedSegment } from "../types.js";
|
|
10
10
|
import type { HandleStore } from "../server/handle-store.js";
|
|
11
11
|
import type { SegmentHandleData } from "./types.js";
|
|
12
|
-
|
|
12
|
+
// segment-codec eagerly pulls @vitejs/plugin-rsc (a virtual: module unresolvable
|
|
13
|
+
// in plain node/vitest). It is imported LAZILY inside the two async encode/decode
|
|
14
|
+
// helpers below so that modules which import handle-snapshot only for the
|
|
15
|
+
// plugin-rsc-free captureHandles/restoreHandles (e.g. cache-scope, on dispatch's
|
|
16
|
+
// lazy response-route cache path) do not pull plugin-rsc at module load. Behavior
|
|
17
|
+
// is unchanged: both helpers are async and already awaited the codec.
|
|
13
18
|
|
|
14
19
|
const HANDLE_ENCODE_TIMEOUT_MS = 5000;
|
|
15
20
|
|
|
@@ -52,6 +57,7 @@ export function decodeHandles(encoded: string): Promise<HandleRecord | null> {
|
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
export async function encodeHandleValue(value: unknown): Promise<string> {
|
|
60
|
+
const { serializeResult } = await import("./segment-codec.js");
|
|
55
61
|
const encoded = await withTimeout(
|
|
56
62
|
serializeResult(value),
|
|
57
63
|
HANDLE_ENCODE_TIMEOUT_MS,
|
|
@@ -67,6 +73,7 @@ export async function encodeHandleValue(value: unknown): Promise<string> {
|
|
|
67
73
|
*/
|
|
68
74
|
export async function decodeHandleValue<T>(encoded: string): Promise<T | null> {
|
|
69
75
|
try {
|
|
76
|
+
const { deserializeResult } = await import("./segment-codec.js");
|
|
70
77
|
return await deserializeResult<T>(encoded);
|
|
71
78
|
} catch {
|
|
72
79
|
return null;
|
|
@@ -42,7 +42,31 @@ export function resolveCacheProfiles(
|
|
|
42
42
|
`Profile names must match [a-zA-Z0-9_-]+.`,
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
const profile = profiles[name];
|
|
46
|
+
// Validate ttl/swr VALUES, not just the name. An unvalidated NaN/Infinity
|
|
47
|
+
// ttl flows into computeExpiration -> staleAt/expiresAt = NaN, and every
|
|
48
|
+
// expiry check (`now > NaN`) is false, so the entry never evicts and never
|
|
49
|
+
// revalidates: it is served fresh forever and accumulates unbounded. A
|
|
50
|
+
// negative ttl makes every read a guaranteed miss. This guard's policy is
|
|
51
|
+
// to FAIL FAST at config time on any non-finite/negative ttl — distinct
|
|
52
|
+
// from prefetch-cache-ttl.ts (falls back to the default) and defer.ts
|
|
53
|
+
// (treats Infinity as an intentional disable). Do not conflate them.
|
|
54
|
+
if (!Number.isFinite(profile.ttl) || profile.ttl < 0) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Invalid cache profile "${name}": ttl must be a finite non-negative ` +
|
|
57
|
+
`number (got ${profile.ttl}).`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (
|
|
61
|
+
profile.swr !== undefined &&
|
|
62
|
+
(!Number.isFinite(profile.swr) || profile.swr < 0)
|
|
63
|
+
) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Invalid cache profile "${name}": swr must be a finite non-negative ` +
|
|
66
|
+
`number (got ${profile.swr}).`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
merged[name] = profile;
|
|
46
70
|
}
|
|
47
71
|
}
|
|
48
72
|
return merged;
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { ResolvedSegment } from "../types.js";
|
|
12
12
|
import type { SerializedSegmentData } from "./types.js";
|
|
13
|
+
import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
13
14
|
import {
|
|
14
15
|
renderToReadableStream,
|
|
15
16
|
createTemporaryReferenceSet,
|
|
@@ -95,7 +96,14 @@ export async function serializeResult(value: unknown): Promise<string | null> {
|
|
|
95
96
|
const temporaryReferences = createTemporaryReferenceSet();
|
|
96
97
|
const stream = renderToReadableStream(value, { temporaryReferences });
|
|
97
98
|
return await streamToString(stream);
|
|
98
|
-
} catch {
|
|
99
|
+
} catch (error) {
|
|
100
|
+
// Returning null silently turns a non-serializable cache value into a
|
|
101
|
+
// permanent miss with no trace. Surface it on the internal debug channel so
|
|
102
|
+
// a failed serialize is diagnosable on wrangler tail, but keep returning
|
|
103
|
+
// null so the caller falls through to an uncached render rather than throws.
|
|
104
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
105
|
+
console.warn("[segment-codec] serializeResult failed:", error);
|
|
106
|
+
}
|
|
99
107
|
return null;
|
|
100
108
|
}
|
|
101
109
|
}
|
package/src/cache/types.ts
CHANGED
|
@@ -259,11 +259,15 @@ export interface CacheDefaults {
|
|
|
259
259
|
/**
|
|
260
260
|
* Default time-to-live in seconds.
|
|
261
261
|
* After TTL expires, cached entry is considered stale.
|
|
262
|
+
* Must be a finite, non-negative number; an invalid value (NaN/Infinity/
|
|
263
|
+
* negative) falls back to the default at read time.
|
|
262
264
|
*/
|
|
263
265
|
ttl?: number;
|
|
264
266
|
/**
|
|
265
267
|
* Default stale-while-revalidate window in seconds.
|
|
266
268
|
* During SWR window, stale content is served while revalidating in background.
|
|
269
|
+
* Must be a finite, non-negative number; an invalid value (NaN/Infinity/
|
|
270
|
+
* negative) falls back to the default at read time.
|
|
267
271
|
*/
|
|
268
272
|
swr?: number;
|
|
269
273
|
}
|
package/src/client.rsc.tsx
CHANGED
|
@@ -20,8 +20,40 @@ export {
|
|
|
20
20
|
type ErrorBoundaryProps,
|
|
21
21
|
} from "./client.js";
|
|
22
22
|
|
|
23
|
+
export {
|
|
24
|
+
useFetchLoader,
|
|
25
|
+
useRefreshLoaders,
|
|
26
|
+
type LoadFunction,
|
|
27
|
+
type UseLoaderResult,
|
|
28
|
+
type UseFetchLoaderResult,
|
|
29
|
+
type UseLoaderOptions,
|
|
30
|
+
} from "./use-loader.js";
|
|
31
|
+
|
|
23
32
|
export { createLoader } from "./route-definition.js";
|
|
24
33
|
|
|
34
|
+
// "use client" hooks the default ./client entry exports. They are client
|
|
35
|
+
// references in the RSC graph, identical in kind to useHref/useReverse/
|
|
36
|
+
// useHandle already forwarded below; forward them so the RSC client entry's
|
|
37
|
+
// hook surface matches the default entry. useNavigation/useAction stay omitted
|
|
38
|
+
// (they drive client-only navigation/action state — see note below).
|
|
39
|
+
export { useRouter } from "./browser/react/use-router.js";
|
|
40
|
+
export { usePathname } from "./browser/react/use-pathname.js";
|
|
41
|
+
export { useSearchParams } from "./browser/react/use-search-params.js";
|
|
42
|
+
export { useParams } from "./browser/react/use-params.js";
|
|
43
|
+
// CSP nonce for userland head-script components (analytics/GTM/inline init);
|
|
44
|
+
// forwarded so the RSC client entry's hook surface matches the default entry.
|
|
45
|
+
export { useNonce } from "./browser/react/nonce-context.js";
|
|
46
|
+
export { useMount } from "./browser/react/use-mount.js";
|
|
47
|
+
export {
|
|
48
|
+
useSegments,
|
|
49
|
+
type SegmentsState,
|
|
50
|
+
} from "./browser/react/use-segments.js";
|
|
51
|
+
export {
|
|
52
|
+
useLinkStatus,
|
|
53
|
+
type LinkStatus,
|
|
54
|
+
} from "./browser/react/use-link-status.js";
|
|
55
|
+
export { useScrollRestoration } from "./browser/react/ScrollRestoration.js";
|
|
56
|
+
|
|
25
57
|
export {
|
|
26
58
|
Link,
|
|
27
59
|
type LinkProps,
|
|
@@ -49,6 +81,12 @@ export { createHandle, isHandle, type Handle } from "./handle.js";
|
|
|
49
81
|
export { Meta } from "./handles/meta.js";
|
|
50
82
|
export { MetaTags } from "./handles/MetaTags.js";
|
|
51
83
|
export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
|
|
84
|
+
export {
|
|
85
|
+
Script,
|
|
86
|
+
type ScriptConfig,
|
|
87
|
+
type ScriptAttributes,
|
|
88
|
+
} from "./handles/script.js";
|
|
89
|
+
export { Scripts } from "./handles/Scripts.js";
|
|
52
90
|
export { Breadcrumbs, type BreadcrumbItem } from "./handles/breadcrumbs.js";
|
|
53
91
|
|
|
54
92
|
export {
|