@rangojs/router 0.0.0-experimental.fb4fdc18 → 0.0.0-experimental.fce7fbd1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +914 -485
- package/package.json +55 -11
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +214 -18
- package/skills/host-router/SKILL.md +45 -20
- package/skills/intercept/SKILL.md +26 -4
- package/skills/layout/SKILL.md +6 -7
- package/skills/links/SKILL.md +173 -17
- package/skills/loader/SKILL.md +149 -6
- package/skills/middleware/SKILL.md +13 -9
- package/skills/migrate-nextjs/SKILL.md +1 -1
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +5 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +242 -26
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +58 -9
- package/skills/route/SKILL.md +13 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +53 -41
- package/skills/testing/SKILL.md +599 -0
- package/skills/typesafety/SKILL.md +310 -26
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/event-controller.ts +42 -66
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +6 -6
- package/src/browser/navigation-client.ts +12 -15
- package/src/browser/navigation-store.ts +7 -8
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +9 -19
- package/src/browser/react/NavigationProvider.tsx +29 -40
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-params.ts +3 -4
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +14 -1
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +30 -16
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +2 -0
- 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 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +49 -6
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +10 -8
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +26 -13
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +6 -4
- package/src/index.ts +13 -6
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +65 -41
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +238 -263
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +37 -14
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/handler-context.ts +4 -42
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +2 -2
- package/src/router/loader-resolution.ts +16 -2
- package/src/router/match-handlers.ts +62 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +32 -30
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +1 -1
- package/src/router/middleware.ts +46 -78
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +43 -1
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +19 -6
- package/src/router/segment-resolution/revalidation.ts +19 -6
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/types.ts +8 -0
- package/src/router.ts +37 -21
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +20 -65
- package/src/rsc/helpers.ts +22 -2
- package/src/rsc/index.ts +1 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/response-route-handler.ts +32 -52
- package/src/rsc/rsc-rendering.ts +27 -53
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +13 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +2 -2
- package/src/search-params.ts +4 -4
- package/src/segment-system.tsx +121 -65
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +118 -51
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +10 -0
- package/src/static-handler.ts +1 -1
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +193 -0
- package/src/testing/render-route.tsx +536 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +170 -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 +183 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +56 -11
- package/src/types/index.ts +1 -0
- package/src/types/segments.ts +18 -1
- package/src/urls/include-helper.ts +10 -53
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +11 -3
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +70 -48
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +19 -25
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +3 -7
- package/src/vite/plugins/client-ref-hashing.ts +12 -1
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
- package/src/vite/plugins/expose-action-id.ts +2 -2
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-internal-ids.ts +47 -67
- package/src/vite/plugins/performance-tracks.ts +12 -16
- package/src/vite/plugins/use-cache-transform.ts +13 -11
- package/src/vite/plugins/version-injector.ts +2 -12
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +67 -15
- package/src/vite/router-discovery.ts +208 -63
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
package/src/serialize.ts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-type serialization transforms.
|
|
3
|
+
*
|
|
4
|
+
* The type a handler or loader returns on the server is frequently NOT the type
|
|
5
|
+
* a client receives after serialization. These transforms model that boundary so
|
|
6
|
+
* consumer-facing types (e.g. `Rango.PathResponse`) describe the wire value, not
|
|
7
|
+
* the source value.
|
|
8
|
+
*
|
|
9
|
+
* Two serializers, two transforms — they are intentionally NOT interchangeable:
|
|
10
|
+
*
|
|
11
|
+
* - `JsonSerialize<T>` models plain `JSON.stringify` (`path.json()` /
|
|
12
|
+
* `fetch().then(r => r.json())`). Lossy: `Date -> string`, `undefined` /
|
|
13
|
+
* functions / symbols dropped, `Map`/`Set` -> `{}`. `bigint` *throws* (no wire
|
|
14
|
+
* value), so it collapses the whole result to `never`. Honors `toJSON()`.
|
|
15
|
+
* - `FlightSerialize<T>` models React RSC Flight (loaders, RSC props, cache).
|
|
16
|
+
* High fidelity: `Date`/`Map`/`Set`/`bigint`/typed arrays/`Promise` are
|
|
17
|
+
* preserved; ordinary functions and non-global symbols do not cross.
|
|
18
|
+
*
|
|
19
|
+
* ## Overriding (full-transform replacement)
|
|
20
|
+
*
|
|
21
|
+
* Because `Rango.JsonSerialize` / `Rango.FlightSerialize` are type *aliases*, TS
|
|
22
|
+
* cannot let you redefine them directly (aliases don't merge). Instead each alias
|
|
23
|
+
* consults a generic override slot — augment it with a single member that is your
|
|
24
|
+
* complete transform. Delegate to the built-in for the cases you don't change:
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* declare global {
|
|
28
|
+
* namespace Rango {
|
|
29
|
+
* interface FlightSerializeOverride<T> {
|
|
30
|
+
* app: T extends Money ? number : Rango.FlightSerializeBuiltin<T>;
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
* // now Rango.FlightSerialize<Money> is number; everything else is the built-in.
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* Provide exactly one member: the slot is read as `Override<T>[keyof Override<T>]`,
|
|
38
|
+
* so multiple members union (and conflict). The built-in recurses through the
|
|
39
|
+
* override-aware alias, so an override applies at every nesting level.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import type { ReactNode } from "react";
|
|
43
|
+
|
|
44
|
+
type JsonPrimitive = string | number | boolean | null;
|
|
45
|
+
|
|
46
|
+
type AnyFunction = (...args: never[]) => unknown;
|
|
47
|
+
|
|
48
|
+
// --- JSON ---------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Internal marker for a value that makes `JSON.stringify` throw (`bigint`, or a
|
|
52
|
+
* `toJSON()` returning one). Distinct from `never`, which means "omitted":
|
|
53
|
+
* `undefined`/function/symbol-valued keys are dropped, and such array slots
|
|
54
|
+
* become `null`. A throwing value has no valid JSON wire form, so it propagates
|
|
55
|
+
* up through every container and is excluded at the public boundary (`bigint`
|
|
56
|
+
* alone -> `never`; `{ id: bigint }` -> `never`).
|
|
57
|
+
*/
|
|
58
|
+
declare const JSON_THROWS: unique symbol;
|
|
59
|
+
type JsonThrows = typeof JSON_THROWS;
|
|
60
|
+
|
|
61
|
+
/** True if union `U` contains the throw marker. */
|
|
62
|
+
type HasThrow<U> = [Extract<U, JsonThrows>] extends [never] ? false : true;
|
|
63
|
+
|
|
64
|
+
/** Map a JSON array/tuple: propagate a throw; else omitted elements become null. */
|
|
65
|
+
type JsonSerializeArray<T extends readonly unknown[]> =
|
|
66
|
+
HasThrow<{ [K in keyof T]: JsonRawResolve<T[K]> }[number]> extends true
|
|
67
|
+
? JsonThrows
|
|
68
|
+
: {
|
|
69
|
+
[K in keyof T]: [JsonRawResolve<T[K]>] extends [never]
|
|
70
|
+
? null
|
|
71
|
+
: JsonRawResolve<T[K]>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** Map a JSON object: propagate a throw; else drop omitted keys. */
|
|
75
|
+
type JsonSerializeObject<T> =
|
|
76
|
+
HasThrow<{ [K in keyof T]: JsonRawResolve<T[K]> }[keyof T]> extends true
|
|
77
|
+
? JsonThrows
|
|
78
|
+
: {
|
|
79
|
+
[K in keyof T as [JsonRawResolve<T[K]>] extends [never]
|
|
80
|
+
? never
|
|
81
|
+
: K]: JsonRawResolve<T[K]>;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Built-in JSON rules, *raw* (may yield the throw marker). Honors `toJSON()` (so
|
|
86
|
+
* `Date -> string` and any class with `toJSON()` serialize correctly), preserves
|
|
87
|
+
* JSON primitives and literals, omits functions / symbols / `undefined`,
|
|
88
|
+
* collapses `Map`/`Set` to `{}`, and marks `bigint` as throwing. Recurses through
|
|
89
|
+
* the override-aware resolver, so registered overrides apply at every level.
|
|
90
|
+
*/
|
|
91
|
+
type JsonSerializeBuiltinRaw<T> = T extends {
|
|
92
|
+
toJSON(...args: never[]): infer R;
|
|
93
|
+
}
|
|
94
|
+
? JsonRawResolve<R>
|
|
95
|
+
: T extends JsonPrimitive
|
|
96
|
+
? T
|
|
97
|
+
: T extends bigint
|
|
98
|
+
? JsonThrows
|
|
99
|
+
: T extends AnyFunction
|
|
100
|
+
? never
|
|
101
|
+
: T extends symbol
|
|
102
|
+
? never
|
|
103
|
+
: T extends undefined
|
|
104
|
+
? never
|
|
105
|
+
: T extends readonly unknown[]
|
|
106
|
+
? JsonSerializeArray<T>
|
|
107
|
+
: T extends ReadonlyMap<unknown, unknown>
|
|
108
|
+
? {}
|
|
109
|
+
: T extends ReadonlySet<unknown>
|
|
110
|
+
? {}
|
|
111
|
+
: T extends object
|
|
112
|
+
? JsonSerializeObject<T>
|
|
113
|
+
: never;
|
|
114
|
+
|
|
115
|
+
/** Override-aware raw JSON resolution (the recursion entry). */
|
|
116
|
+
type JsonRawResolve<T> = [keyof Rango.JsonSerializeOverride<T>] extends [never]
|
|
117
|
+
? JsonSerializeBuiltinRaw<T>
|
|
118
|
+
: Rango.JsonSerializeOverride<T>[keyof Rango.JsonSerializeOverride<T>];
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Model the result of round-tripping a value through `JSON.stringify` /
|
|
122
|
+
* `JSON.parse`. A registered `Rango.JsonSerializeOverride` replaces the transform
|
|
123
|
+
* wholesale; otherwise the built-in rules apply. Throwing values collapse to
|
|
124
|
+
* `never`.
|
|
125
|
+
*/
|
|
126
|
+
export type JsonSerialize<T> = Exclude<JsonRawResolve<T>, JsonThrows>;
|
|
127
|
+
|
|
128
|
+
// --- Flight -------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Built-in Flight rules. Mirrors React's `ReactClientValue` contract: primitives
|
|
132
|
+
* including `bigint`, `undefined`, `null`, symbols, `Date`, `ArrayBuffer` and
|
|
133
|
+
* typed-array views, `Map`, `Set`, `FormData`, `Blob`, `Promise`,
|
|
134
|
+
* `ReadableStream`, and (async) iterables are preserved; ordinary functions
|
|
135
|
+
* resolve to `never`. JSX (`ReactNode`, and the async-node union
|
|
136
|
+
* `ReactNode | Promise<ReactNode>`) is preserved as-is via a non-distributive
|
|
137
|
+
* leaf, so handle/loader returns that carry JSX round-trip unchanged. Recurses
|
|
138
|
+
* through the override-aware `FlightSerialize`.
|
|
139
|
+
*
|
|
140
|
+
* The source of truth is React's own contract, which is intentionally NOT
|
|
141
|
+
* semver-stable across RSC framework APIs — this tracks the React version Rango
|
|
142
|
+
* pins. See:
|
|
143
|
+
* https://react.dev/reference/rsc/use-client#serializable-types-returned-by-server-components
|
|
144
|
+
*
|
|
145
|
+
* Type-level limitations (not detectable structurally, so not modeled): class
|
|
146
|
+
* instances and null-prototype objects are rejected by React at runtime but pass
|
|
147
|
+
* here as their structural shape; non-global symbols are rejected at runtime but
|
|
148
|
+
* `symbol` is preserved here; Server Functions would need an override to be
|
|
149
|
+
* distinguished from ordinary functions (which resolve to `never`).
|
|
150
|
+
*/
|
|
151
|
+
type FlightSerializeBuiltinRaw<T> = [T] extends [ReactNode | Promise<ReactNode>]
|
|
152
|
+
? T
|
|
153
|
+
: T extends string | number | boolean | bigint | symbol | null | undefined
|
|
154
|
+
? T
|
|
155
|
+
: T extends AnyFunction
|
|
156
|
+
? never
|
|
157
|
+
: T extends Date
|
|
158
|
+
? Date
|
|
159
|
+
: T extends ArrayBuffer
|
|
160
|
+
? ArrayBuffer
|
|
161
|
+
: T extends ArrayBufferView
|
|
162
|
+
? T
|
|
163
|
+
: T extends FormData
|
|
164
|
+
? FormData
|
|
165
|
+
: T extends Blob
|
|
166
|
+
? Blob
|
|
167
|
+
: T extends Map<infer K, infer V>
|
|
168
|
+
? Map<FlightSerialize<K>, FlightSerialize<V>>
|
|
169
|
+
: T extends Set<infer V>
|
|
170
|
+
? Set<FlightSerialize<V>>
|
|
171
|
+
: T extends Promise<infer V>
|
|
172
|
+
? Promise<FlightSerialize<V>>
|
|
173
|
+
: T extends ReadableStream<infer V>
|
|
174
|
+
? ReadableStream<FlightSerialize<V>>
|
|
175
|
+
: T extends readonly unknown[]
|
|
176
|
+
? { [K in keyof T]: FlightSerialize<T[K]> }
|
|
177
|
+
: T extends AsyncIterable<infer V>
|
|
178
|
+
? AsyncIterable<FlightSerialize<V>>
|
|
179
|
+
: T extends Iterable<infer V>
|
|
180
|
+
? Iterable<FlightSerialize<V>>
|
|
181
|
+
: T extends object
|
|
182
|
+
? { [K in keyof T]: FlightSerialize<T[K]> }
|
|
183
|
+
: never;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Model React RSC Flight serialization. A registered `Rango.FlightSerializeOverride`
|
|
187
|
+
* replaces the transform wholesale; otherwise the built-in rules apply.
|
|
188
|
+
*/
|
|
189
|
+
export type FlightSerialize<T> = [
|
|
190
|
+
keyof Rango.FlightSerializeOverride<T>,
|
|
191
|
+
] extends [never]
|
|
192
|
+
? FlightSerializeBuiltinRaw<T>
|
|
193
|
+
: Rango.FlightSerializeOverride<T>[keyof Rango.FlightSerializeOverride<T>];
|
|
194
|
+
|
|
195
|
+
// Module-scoped aliases so the ambient `Rango.*` members below can reference the
|
|
196
|
+
// module-level transforms without the global namespace shadowing the names.
|
|
197
|
+
type GlobalJsonSerialize<T> = JsonSerialize<T>;
|
|
198
|
+
type GlobalJsonSerializeBuiltin<T> = JsonSerializeBuiltinRaw<T>;
|
|
199
|
+
type GlobalFlightSerialize<T> = FlightSerialize<T>;
|
|
200
|
+
type GlobalFlightSerializeBuiltin<T> = FlightSerializeBuiltinRaw<T>;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Ambient serialization transforms and their override slots on the `Rango`
|
|
204
|
+
* namespace. Available with no import wherever the router's types are in scope,
|
|
205
|
+
* alongside `Rango.Path` / `Rango.PathResponse`.
|
|
206
|
+
*
|
|
207
|
+
* `Rango.JsonSerialize` is what `Rango.PathResponse` applies; `Rango.FlightSerialize`
|
|
208
|
+
* is exposed for RSC/loader/cache wire types and must NOT be used for `path.json()`.
|
|
209
|
+
* `Rango.JsonSerializeBuiltin` / `Rango.FlightSerializeBuiltin` are the defaults,
|
|
210
|
+
* exported so an override can delegate to them.
|
|
211
|
+
*/
|
|
212
|
+
declare global {
|
|
213
|
+
namespace Rango {
|
|
214
|
+
/**
|
|
215
|
+
* Full-transform override slot for `Rango.JsonSerialize`. Empty by default;
|
|
216
|
+
* augment with one member that is your complete transform (delegate to
|
|
217
|
+
* `Rango.JsonSerializeBuiltin<T>` for the cases you don't change).
|
|
218
|
+
*/
|
|
219
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
220
|
+
interface JsonSerializeOverride<T> {}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Full-transform override slot for `Rango.FlightSerialize`. Empty by default;
|
|
224
|
+
* augment with one member that is your complete transform (delegate to
|
|
225
|
+
* `Rango.FlightSerializeBuiltin<T>` for the cases you don't change).
|
|
226
|
+
*/
|
|
227
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
228
|
+
interface FlightSerializeOverride<T> {}
|
|
229
|
+
|
|
230
|
+
/** Wire type after `JSON.stringify` (`path.json()` / `fetch().json()`). */
|
|
231
|
+
type JsonSerialize<T> = GlobalJsonSerialize<T>;
|
|
232
|
+
/**
|
|
233
|
+
* Built-in `JsonSerialize` rules, for an override to delegate to. Raw: a
|
|
234
|
+
* `bigint`-bearing type yields the internal throw marker here, which
|
|
235
|
+
* `Rango.JsonSerialize` excludes to `never` at the boundary.
|
|
236
|
+
*/
|
|
237
|
+
type JsonSerializeBuiltin<T> = GlobalJsonSerializeBuiltin<T>;
|
|
238
|
+
/** Wire type after RSC Flight serialization (loaders / RSC props / cache). */
|
|
239
|
+
type FlightSerialize<T> = GlobalFlightSerialize<T>;
|
|
240
|
+
/** Built-in `FlightSerialize` rules, for an override to delegate to. */
|
|
241
|
+
type FlightSerializeBuiltin<T> = GlobalFlightSerializeBuiltin<T>;
|
|
242
|
+
}
|
|
243
|
+
}
|
package/src/server/context.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
ShouldRevalidateFn,
|
|
11
11
|
TransitionConfig,
|
|
12
12
|
} from "../types";
|
|
13
|
-
import { invariant } from "../errors";
|
|
13
|
+
import { invariant, DslContextError } from "../errors";
|
|
14
14
|
import type { DefaultRouteName } from "../types/global-namespace.js";
|
|
15
15
|
|
|
16
16
|
// ============================================================================
|
|
@@ -40,7 +40,7 @@ export interface MetricsStore {
|
|
|
40
40
|
metrics: PerformanceMetric[];
|
|
41
41
|
}
|
|
42
42
|
// ============================================================================
|
|
43
|
-
//
|
|
43
|
+
// Rango Context
|
|
44
44
|
// ============================================================================
|
|
45
45
|
|
|
46
46
|
/**
|
|
@@ -71,6 +71,10 @@ export type EntryPropCommon = {
|
|
|
71
71
|
};
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
+
* Attachments resolved by walking the parent chain, not owned by the entry:
|
|
75
|
+
* middleware composes downward; revalidate and the error/notFound boundaries are
|
|
76
|
+
* resolved by nearest-ancestor lookup. Inherited, not a single execution chain.
|
|
77
|
+
*
|
|
74
78
|
* @internal This type is an implementation detail and may change without notice.
|
|
75
79
|
*/
|
|
76
80
|
export type EntryPropDatas = {
|
|
@@ -80,6 +84,16 @@ export type EntryPropDatas = {
|
|
|
80
84
|
notFoundBoundary: (ReactNode | NotFoundBoundaryHandler)[];
|
|
81
85
|
};
|
|
82
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Render-time presentation fields shared by every entry variant.
|
|
89
|
+
*
|
|
90
|
+
* @internal This type is an implementation detail and may change without notice.
|
|
91
|
+
*/
|
|
92
|
+
export type EntryPropRender = {
|
|
93
|
+
loading?: ReactNode | false;
|
|
94
|
+
transition?: TransitionConfig;
|
|
95
|
+
};
|
|
96
|
+
|
|
83
97
|
/**
|
|
84
98
|
* Loader entry stored in EntryData
|
|
85
99
|
* Contains the loader definition and its revalidation rules
|
|
@@ -158,11 +172,9 @@ export type InterceptEntry = {
|
|
|
158
172
|
};
|
|
159
173
|
|
|
160
174
|
export interface ParallelEntryData
|
|
161
|
-
extends EntryPropCommon, EntryPropDatas, EntryPropSegments {
|
|
175
|
+
extends EntryPropCommon, EntryPropDatas, EntryPropSegments, EntryPropRender {
|
|
162
176
|
type: "parallel";
|
|
163
177
|
handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
|
|
164
|
-
loading?: ReactNode | false;
|
|
165
|
-
transition?: TransitionConfig;
|
|
166
178
|
/** Set when any parallel slot is a Static definition */
|
|
167
179
|
isStaticPrerender?: true;
|
|
168
180
|
/** Per-slot static handler $$ids for build-time store lookup */
|
|
@@ -171,6 +183,13 @@ export interface ParallelEntryData
|
|
|
171
183
|
|
|
172
184
|
export type ParallelEntries = Partial<Record<`@${string}`, ParallelEntryData>>;
|
|
173
185
|
|
|
186
|
+
/**
|
|
187
|
+
* This entry's own structural children plus its owned loaders. `loader` lives
|
|
188
|
+
* here (not in EntryPropDatas) because loaders are owned by the entry, not
|
|
189
|
+
* inherited from ancestors.
|
|
190
|
+
*
|
|
191
|
+
* @internal This type is an implementation detail and may change without notice.
|
|
192
|
+
*/
|
|
174
193
|
export type EntryPropSegments = {
|
|
175
194
|
loader: LoaderEntry[];
|
|
176
195
|
layout: EntryData[];
|
|
@@ -182,8 +201,6 @@ export type EntryData =
|
|
|
182
201
|
| ({
|
|
183
202
|
type: "route";
|
|
184
203
|
handler: Handler<any, any, any>;
|
|
185
|
-
loading?: ReactNode | false;
|
|
186
|
-
transition?: TransitionConfig;
|
|
187
204
|
/** URL pattern for this route (used by path() in urls()) */
|
|
188
205
|
pattern?: string;
|
|
189
206
|
/** Set when handler is a Prerender definition */
|
|
@@ -205,29 +222,28 @@ export type EntryData =
|
|
|
205
222
|
responseType?: string;
|
|
206
223
|
} & EntryPropCommon &
|
|
207
224
|
EntryPropDatas &
|
|
208
|
-
EntryPropSegments
|
|
225
|
+
EntryPropSegments &
|
|
226
|
+
EntryPropRender)
|
|
209
227
|
| ({
|
|
210
228
|
type: "layout";
|
|
211
229
|
handler: ReactNode | Handler<any, any, any>;
|
|
212
|
-
loading?: ReactNode | false;
|
|
213
|
-
transition?: TransitionConfig;
|
|
214
230
|
/** Set when handler is a Static definition (build-time only) */
|
|
215
231
|
isStaticPrerender?: true;
|
|
216
232
|
/** Static handler $$id for build-time store lookup */
|
|
217
233
|
staticHandlerId?: string;
|
|
218
234
|
} & EntryPropCommon &
|
|
219
235
|
EntryPropDatas &
|
|
220
|
-
EntryPropSegments
|
|
236
|
+
EntryPropSegments &
|
|
237
|
+
EntryPropRender)
|
|
221
238
|
| ParallelEntryData
|
|
222
239
|
| ({
|
|
223
240
|
type: "cache";
|
|
224
241
|
/** Cache entries create cache boundaries and render like layouts (with Outlet) */
|
|
225
242
|
handler: ReactNode | Handler<any, any, any>;
|
|
226
|
-
loading?: ReactNode | false;
|
|
227
|
-
transition?: TransitionConfig;
|
|
228
243
|
} & EntryPropCommon &
|
|
229
244
|
EntryPropDatas &
|
|
230
|
-
EntryPropSegments
|
|
245
|
+
EntryPropSegments &
|
|
246
|
+
EntryPropRender);
|
|
231
247
|
|
|
232
248
|
/**
|
|
233
249
|
* Tracked include info for build-time manifest generation
|
|
@@ -303,10 +319,28 @@ interface HelperContext {
|
|
|
303
319
|
// hold references to the old instance — causing getStore() to return
|
|
304
320
|
// undefined even inside a run() callback.
|
|
305
321
|
const RSC_CONTEXT_KEY = Symbol.for("rangojs-router:rsc-context");
|
|
306
|
-
export const
|
|
322
|
+
export const RangoContext: AsyncLocalStorage<HelperContext> = ((
|
|
307
323
|
globalThis as any
|
|
308
324
|
)[RSC_CONTEXT_KEY] ??= new AsyncLocalStorage<HelperContext>());
|
|
309
325
|
|
|
326
|
+
/** shortCode prefix letter per entry type (e.g. "L0", "R2", "M1C0"). */
|
|
327
|
+
const SHORT_CODE_PREFIX: Record<
|
|
328
|
+
"layout" | "parallel" | "route" | "loader" | "cache",
|
|
329
|
+
string
|
|
330
|
+
> = {
|
|
331
|
+
layout: "L",
|
|
332
|
+
parallel: "P",
|
|
333
|
+
route: "R",
|
|
334
|
+
loader: "D",
|
|
335
|
+
cache: "C",
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
/** Post-increment a named per-store counter, returning the prior value. */
|
|
339
|
+
function bumpCounter(store: HelperContext, key: string): number {
|
|
340
|
+
store.counters[key] ??= 0;
|
|
341
|
+
return store.counters[key]++;
|
|
342
|
+
}
|
|
343
|
+
|
|
310
344
|
export const getContext = (): {
|
|
311
345
|
context: AsyncLocalStorage<HelperContext>;
|
|
312
346
|
getStore: () => HelperContext;
|
|
@@ -330,12 +364,12 @@ export const getContext = (): {
|
|
|
330
364
|
callback: (...args: any[]) => T,
|
|
331
365
|
) => T;
|
|
332
366
|
} => {
|
|
333
|
-
const context =
|
|
367
|
+
const context = RangoContext;
|
|
334
368
|
|
|
335
369
|
return {
|
|
336
370
|
context,
|
|
337
371
|
getOrCreateStore: (forRoute?: string): HelperContext => {
|
|
338
|
-
let store =
|
|
372
|
+
let store = RangoContext.getStore();
|
|
339
373
|
if (!store) {
|
|
340
374
|
store = {
|
|
341
375
|
manifest: new Map<string, EntryData>(),
|
|
@@ -355,7 +389,7 @@ export const getContext = (): {
|
|
|
355
389
|
const store = context.getStore();
|
|
356
390
|
if (!store) {
|
|
357
391
|
throw new Error(
|
|
358
|
-
"
|
|
392
|
+
"Rango context store is not available. Make sure to run within Rango context.",
|
|
359
393
|
);
|
|
360
394
|
}
|
|
361
395
|
return store;
|
|
@@ -372,29 +406,17 @@ export const getContext = (): {
|
|
|
372
406
|
type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
|
|
373
407
|
) => {
|
|
374
408
|
const store = context.getStore();
|
|
375
|
-
invariant(store, "No context
|
|
376
|
-
store
|
|
377
|
-
const index = store.counters[type];
|
|
378
|
-
store.counters[type] = index + 1;
|
|
379
|
-
return `$${type}.${index}`;
|
|
409
|
+
invariant(store, "No context RangoContext available");
|
|
410
|
+
return `$${type}.${bumpCounter(store, type)}`;
|
|
380
411
|
},
|
|
381
412
|
getShortCode: (
|
|
382
413
|
type: "layout" | "parallel" | "route" | "loader" | "cache",
|
|
383
414
|
) => {
|
|
384
415
|
const store = context.getStore();
|
|
385
|
-
invariant(store, "No context
|
|
416
|
+
invariant(store, "No context RangoContext available");
|
|
386
417
|
|
|
387
418
|
const parent = store.parent;
|
|
388
|
-
const prefix =
|
|
389
|
-
type === "layout"
|
|
390
|
-
? "L"
|
|
391
|
-
: type === "parallel"
|
|
392
|
-
? "P"
|
|
393
|
-
: type === "loader"
|
|
394
|
-
? "D"
|
|
395
|
-
: type === "cache"
|
|
396
|
-
? "C"
|
|
397
|
-
: "R";
|
|
419
|
+
const prefix = SHORT_CODE_PREFIX[type];
|
|
398
420
|
const mountPrefix =
|
|
399
421
|
store.mountIndex !== undefined ? `M${store.mountIndex}` : "";
|
|
400
422
|
|
|
@@ -405,10 +427,7 @@ export const getContext = (): {
|
|
|
405
427
|
const counterKey = mountPrefix
|
|
406
428
|
? `${mountPrefix}_root_${type}`
|
|
407
429
|
: `root_${type}`;
|
|
408
|
-
store
|
|
409
|
-
const index = store.counters[counterKey];
|
|
410
|
-
store.counters[counterKey] = index + 1;
|
|
411
|
-
return `${mountPrefix}${prefix}${index}`;
|
|
430
|
+
return `${mountPrefix}${prefix}${bumpCounter(store, counterKey)}`;
|
|
412
431
|
} else {
|
|
413
432
|
// Child entry: use parent-scoped counter with includeScope appended.
|
|
414
433
|
// When we're evaluating a lazy include's direct children, includeScope
|
|
@@ -416,10 +435,7 @@ export const getContext = (): {
|
|
|
416
435
|
// parent's counter namespace so routes inside one include cannot
|
|
417
436
|
// collide with siblings declared outside it.
|
|
418
437
|
const counterKey = `${parent.shortCode}${includeScope}_${type}`;
|
|
419
|
-
store
|
|
420
|
-
const index = store.counters[counterKey];
|
|
421
|
-
store.counters[counterKey] = index + 1;
|
|
422
|
-
return `${parent.shortCode}${includeScope}${prefix}${index}`;
|
|
438
|
+
return `${parent.shortCode}${includeScope}${prefix}${bumpCounter(store, counterKey)}`;
|
|
423
439
|
}
|
|
424
440
|
},
|
|
425
441
|
runWithStore: <T>(
|
|
@@ -493,6 +509,31 @@ export const getContext = (): {
|
|
|
493
509
|
};
|
|
494
510
|
};
|
|
495
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Acquire the active DSL build context, throwing `message` if a helper was
|
|
514
|
+
* called outside a urls()/map() builder. Returns the store API and the live
|
|
515
|
+
* HelperContext so callers avoid a second getContext() lookup.
|
|
516
|
+
*/
|
|
517
|
+
export function requireDslContext(message: string): {
|
|
518
|
+
store: ReturnType<typeof getContext>;
|
|
519
|
+
ctx: HelperContext;
|
|
520
|
+
} {
|
|
521
|
+
const store = getContext();
|
|
522
|
+
const ctx = store.context.getStore();
|
|
523
|
+
if (!ctx) {
|
|
524
|
+
// The only reason the store is absent here is that a route-definition helper
|
|
525
|
+
// ran with no active RangoContext — i.e. outside a urls()/map() builder.
|
|
526
|
+
// Record that as the cause so the throw is self-explanatory, not a bare
|
|
527
|
+
// "must be called inside urls()" with no indication of the mechanism.
|
|
528
|
+
throw new DslContextError(message, {
|
|
529
|
+
cause:
|
|
530
|
+
"RangoContext store is undefined: a route-definition helper was called " +
|
|
531
|
+
"outside an active urls()/map() builder.",
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
return { store, ctx };
|
|
535
|
+
}
|
|
536
|
+
|
|
496
537
|
/**
|
|
497
538
|
* Run a callback with specific URL and name prefixes
|
|
498
539
|
* Used by include() to apply prefixes to nested patterns
|
|
@@ -502,7 +543,7 @@ export function runWithPrefixes<T>(
|
|
|
502
543
|
namePrefix: string | undefined,
|
|
503
544
|
callback: () => T,
|
|
504
545
|
): T {
|
|
505
|
-
const store =
|
|
546
|
+
const store = RangoContext.getStore();
|
|
506
547
|
if (!store) {
|
|
507
548
|
throw new Error("runWithPrefixes must be called within router context");
|
|
508
549
|
}
|
|
@@ -547,7 +588,7 @@ export function runWithPrefixes<T>(
|
|
|
547
588
|
? (store.rootScoped ?? false)
|
|
548
589
|
: store.rootScoped;
|
|
549
590
|
|
|
550
|
-
return
|
|
591
|
+
return RangoContext.run(
|
|
551
592
|
{
|
|
552
593
|
...store,
|
|
553
594
|
urlPrefix: combinedUrlPrefix,
|
|
@@ -562,7 +603,7 @@ export function runWithPrefixes<T>(
|
|
|
562
603
|
* Get current URL prefix from context
|
|
563
604
|
*/
|
|
564
605
|
export function getUrlPrefix(): string {
|
|
565
|
-
const store =
|
|
606
|
+
const store = RangoContext.getStore();
|
|
566
607
|
return store?.urlPrefix || "";
|
|
567
608
|
}
|
|
568
609
|
|
|
@@ -570,7 +611,7 @@ export function getUrlPrefix(): string {
|
|
|
570
611
|
* Get current name prefix from context
|
|
571
612
|
*/
|
|
572
613
|
export function getNamePrefix(): string | undefined {
|
|
573
|
-
const store =
|
|
614
|
+
const store = RangoContext.getStore();
|
|
574
615
|
return store?.namePrefix;
|
|
575
616
|
}
|
|
576
617
|
|
|
@@ -579,7 +620,7 @@ export function getNamePrefix(): string | undefined {
|
|
|
579
620
|
* Returns true at root or inside { name: "" } includes, false inside named includes.
|
|
580
621
|
*/
|
|
581
622
|
export function getRootScoped(): boolean {
|
|
582
|
-
const store =
|
|
623
|
+
const store = RangoContext.getStore();
|
|
583
624
|
return store?.rootScoped ?? true;
|
|
584
625
|
}
|
|
585
626
|
|
|
@@ -676,7 +717,7 @@ export function getParallelSlotCount(
|
|
|
676
717
|
* ```
|
|
677
718
|
*/
|
|
678
719
|
export function track(label: string, depth?: number): () => void {
|
|
679
|
-
const store =
|
|
720
|
+
const store = RangoContext.getStore();
|
|
680
721
|
|
|
681
722
|
// No-op if context unavailable or metrics not enabled
|
|
682
723
|
if (!store?.metrics?.enabled) {
|
|
@@ -699,25 +740,40 @@ export function track(label: string, depth?: number): () => void {
|
|
|
699
740
|
|
|
700
741
|
/**
|
|
701
742
|
* Separate ALS for tracking loader execution scope.
|
|
702
|
-
* Uses a dedicated ALS (not
|
|
703
|
-
* nested
|
|
743
|
+
* Uses a dedicated ALS (not RangoContext) to avoid issues with
|
|
744
|
+
* nested RangoContext.run() calls in Vite's module runner.
|
|
704
745
|
*/
|
|
705
746
|
const LOADER_SCOPE_KEY = Symbol.for("rangojs-router:loader-scope");
|
|
706
747
|
const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
|
|
707
748
|
globalThis as any
|
|
708
749
|
)[LOADER_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
|
|
709
750
|
|
|
751
|
+
// Purity-only scope: marks that a loader FUNCTION BODY is executing, regardless
|
|
752
|
+
// of how the loader was invoked (DSL via runInsideLoaderScope, or handler-
|
|
753
|
+
// invoked via ctx.use). Consulted ONLY by isInsideCacheScope() to exempt
|
|
754
|
+
// request-scoped reads. It deliberately does NOT affect isInsideLoaderScope(),
|
|
755
|
+
// so rendered()/barrier/deadlock gating (which must distinguish DSL from
|
|
756
|
+
// handler-invoked loaders) is unchanged.
|
|
757
|
+
const LOADER_BODY_SCOPE_KEY = Symbol.for("rangojs-router:loader-body-scope");
|
|
758
|
+
const loaderBodyScopeALS: AsyncLocalStorage<{ active: true }> = ((
|
|
759
|
+
globalThis as any
|
|
760
|
+
)[LOADER_BODY_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
|
|
761
|
+
|
|
710
762
|
/**
|
|
711
763
|
* Check if the current execution is inside a cache() DSL boundary.
|
|
712
764
|
* Returns false inside loader execution — loaders are always fresh
|
|
713
765
|
* (never cached), so non-cacheable reads are safe.
|
|
714
766
|
*/
|
|
715
767
|
export function isInsideCacheScope(): boolean {
|
|
716
|
-
if (
|
|
768
|
+
if (RangoContext.getStore()?.insideCacheScope !== true) return false;
|
|
717
769
|
// Loaders are always fresh — even inside a cache() boundary, the loader
|
|
718
770
|
// function re-executes on every request. Skip the guard when running
|
|
719
771
|
// inside a loader.
|
|
720
772
|
if (loaderScopeALS.getStore()?.active) return false;
|
|
773
|
+
// Also exempt handler-invoked loaders: their bodies run in a loader-body
|
|
774
|
+
// scope (not the DSL loader scope above), so request-scoped reads inside any
|
|
775
|
+
// loader — however invoked — are safe (loaders always re-run fresh).
|
|
776
|
+
if (loaderBodyScopeALS.getStore()?.active) return false;
|
|
721
777
|
return true;
|
|
722
778
|
}
|
|
723
779
|
|
|
@@ -738,3 +794,14 @@ export function isInsideLoaderScope(): boolean {
|
|
|
738
794
|
export function runInsideLoaderScope<T>(fn: () => T): T {
|
|
739
795
|
return loaderScopeALS.run({ active: true }, fn);
|
|
740
796
|
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Run `fn` inside a loader BODY scope. Marks loader-function execution for the
|
|
800
|
+
* cache-purity guard only (isInsideCacheScope), WITHOUT affecting
|
|
801
|
+
* isInsideLoaderScope()/rendered() gating. Applied to every loader body (DSL
|
|
802
|
+
* and handler-invoked via ctx.use) so request-scoped reads inside a loader
|
|
803
|
+
* never trip the cache-scope guards — loaders always run fresh.
|
|
804
|
+
*/
|
|
805
|
+
export function runInsideLoaderBodyScope<T>(fn: () => T): T {
|
|
806
|
+
return loaderBodyScopeALS.run({ active: true }, fn);
|
|
807
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { CookieOptions } from "../router/middleware-types.js";
|
|
11
11
|
import { getRequestContext } from "./request-context.js";
|
|
12
|
+
import { isInsideCacheScope } from "./context.js";
|
|
12
13
|
import { INSIDE_CACHE_EXEC } from "../cache/taint.js";
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -84,10 +85,23 @@ export interface ReadonlyHeaders {
|
|
|
84
85
|
type HeadersIterator<T> = IterableIterator<T>;
|
|
85
86
|
|
|
86
87
|
/**
|
|
87
|
-
* Throw if called inside a "use cache" function
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
88
|
+
* Throw if called inside a cache boundary — either a "use cache" function
|
|
89
|
+
* (`INSIDE_CACHE_EXEC` stamped on ctx by the cache runtime) or a `cache()`
|
|
90
|
+
* DSL boundary (`isInsideCacheScope()` — the render-store flag set while
|
|
91
|
+
* resolving a `type: "cache"` route entry).
|
|
92
|
+
*
|
|
93
|
+
* Reading request-scoped data (cookies, headers) inside a cached scope
|
|
94
|
+
* produces per-request values that are NOT reflected in the cache key, so
|
|
95
|
+
* they would be frozen into the shared cache entry and served to the wrong
|
|
96
|
+
* users. This is the same hazard for both scopes: a `cache()` boundary caches
|
|
97
|
+
* everything except loaders (it is the document-level "PPR shell"), so a read
|
|
98
|
+
* here is baked into the shell exactly like a `"use cache"` return value is
|
|
99
|
+
* baked into its cache entry.
|
|
100
|
+
*
|
|
101
|
+
* `isInsideCacheScope()` returns false inside loaders (loaders always run
|
|
102
|
+
* fresh on every request, even on a cache hit), so reading cookies()/headers()
|
|
103
|
+
* from a loader is allowed — loaders are the dynamic "holes" of a cached
|
|
104
|
+
* document.
|
|
91
105
|
*/
|
|
92
106
|
function assertNotInsideCacheContext(ctx: unknown, fnName: string): void {
|
|
93
107
|
if (
|
|
@@ -106,6 +120,16 @@ function assertNotInsideCacheContext(ctx: unknown, fnName: string): void {
|
|
|
106
120
|
` const data = await getCachedData(locale); // locale is now in the cache key`,
|
|
107
121
|
);
|
|
108
122
|
}
|
|
123
|
+
if (isInsideCacheScope()) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`${fnName}() cannot be called inside a cache() boundary. ` +
|
|
126
|
+
`A cache() scope caches everything except loaders, so request-scoped ` +
|
|
127
|
+
`data (cookies, headers) read here would be frozen into the shared ` +
|
|
128
|
+
`cached shell and served to other users. Read it inside a loader ` +
|
|
129
|
+
`instead — loaders always run fresh on every request, even on a cache hit:\n\n` +
|
|
130
|
+
` loader("user", () => getUser(cookies().get("session")?.value));`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
109
133
|
}
|
|
110
134
|
|
|
111
135
|
const HEADERS_MUTATION_METHODS = new Set(["set", "append", "delete"]);
|
|
@@ -322,6 +322,15 @@ export interface RequestContext<
|
|
|
322
322
|
* to avoid a second resolveRoute call. Cleared on HMR invalidation.
|
|
323
323
|
*/
|
|
324
324
|
_classifiedRoute?: import("../router/route-snapshot.js").RouteSnapshot;
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* @internal Coarse route-level cache signal for the X-Rango-Cache debug
|
|
328
|
+
* header. Populated by match/matchPartial only when the debug cache signal
|
|
329
|
+
* gate is enabled (debugCacheSignal option or RANGO_TEST_SIGNALS=1). Read by
|
|
330
|
+
* the response-finalization path (createResponseWithMergedHeaders). Undefined
|
|
331
|
+
* when the gate is off, so no header is emitted.
|
|
332
|
+
*/
|
|
333
|
+
_cacheSignal?: import("../router/telemetry.js").CacheSegmentSignal[];
|
|
325
334
|
}
|
|
326
335
|
|
|
327
336
|
/**
|
|
@@ -362,6 +371,7 @@ export type PublicRequestContext<
|
|
|
362
371
|
| "_setStatus"
|
|
363
372
|
| "_variables"
|
|
364
373
|
| "_classifiedRoute"
|
|
374
|
+
| "_cacheSignal"
|
|
365
375
|
| "res"
|
|
366
376
|
>;
|
|
367
377
|
|