@rangojs/router 0.0.0-experimental.107 → 0.0.0-experimental.109
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 +4 -4
- package/dist/bin/rango.js +16 -16
- package/dist/vite/index.js +146 -150
- package/package.json +6 -6
- package/skills/hooks/SKILL.md +2 -0
- package/skills/links/SKILL.md +13 -1
- package/skills/loader/SKILL.md +1 -1
- package/skills/middleware/SKILL.md +3 -3
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/prerender/SKILL.md +13 -13
- package/skills/rango/SKILL.md +9 -0
- package/skills/response-routes/SKILL.md +58 -9
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/typesafety/SKILL.md +273 -31
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/react/location-state-shared.ts +3 -3
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/rsc-router.tsx +14 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/build/generate-manifest.ts +3 -3
- 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 +3 -3
- package/src/build/route-types/router-processing.ts +4 -4
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/client.tsx +4 -7
- package/src/errors.ts +1 -1
- package/src/handle.ts +2 -2
- package/src/href-client.ts +136 -19
- package/src/index.rsc.ts +4 -4
- package/src/index.ts +2 -2
- package/src/loader.rsc.ts +1 -1
- package/src/loader.ts +1 -1
- package/src/prerender.ts +4 -4
- package/src/route-definition/dsl-helpers.ts +2 -2
- package/src/route-definition/helpers-types.ts +2 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/lazy-includes.ts +2 -2
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +1 -1
- package/src/router/prerender-match.ts +1 -1
- package/src/router/router-interfaces.ts +34 -28
- package/src/router/router-options.ts +1 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +2 -2
- package/src/router/segment-resolution/revalidation.ts +2 -2
- package/src/router.ts +13 -16
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/index.ts +1 -1
- package/src/rsc/types.ts +2 -2
- package/src/search-params.ts +4 -4
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +16 -16
- package/src/static-handler.ts +1 -1
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +3 -3
- package/src/urls/path-helper-types.ts +2 -2
- package/src/urls/pattern-types.ts +34 -0
- package/src/urls/type-extraction.ts +6 -1
- package/src/use-loader.tsx +6 -4
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +3 -3
- package/src/vite/discovery/discovery-errors.ts +1 -1
- package/src/vite/discovery/prerender-collection.ts +19 -25
- package/src/vite/discovery/route-types-writer.ts +3 -3
- package/src/vite/discovery/state.ts +4 -4
- 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 +33 -9
- package/src/vite/plugins/expose-internal-ids.ts +1 -1
- package/src/vite/plugins/performance-tracks.ts +12 -16
- package/src/vite/plugins/use-cache-transform.ts +1 -1
- package/src/vite/plugins/version-plugin.ts +2 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +11 -11
- package/src/vite/router-discovery.ts +26 -29
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/forward-user-plugins.ts +46 -17
- package/src/vite/utils/shared-utils.ts +26 -22
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
|
@@ -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
|
/**
|
|
@@ -303,7 +303,7 @@ interface HelperContext {
|
|
|
303
303
|
// hold references to the old instance — causing getStore() to return
|
|
304
304
|
// undefined even inside a run() callback.
|
|
305
305
|
const RSC_CONTEXT_KEY = Symbol.for("rangojs-router:rsc-context");
|
|
306
|
-
export const
|
|
306
|
+
export const RangoContext: AsyncLocalStorage<HelperContext> = ((
|
|
307
307
|
globalThis as any
|
|
308
308
|
)[RSC_CONTEXT_KEY] ??= new AsyncLocalStorage<HelperContext>());
|
|
309
309
|
|
|
@@ -330,12 +330,12 @@ export const getContext = (): {
|
|
|
330
330
|
callback: (...args: any[]) => T,
|
|
331
331
|
) => T;
|
|
332
332
|
} => {
|
|
333
|
-
const context =
|
|
333
|
+
const context = RangoContext;
|
|
334
334
|
|
|
335
335
|
return {
|
|
336
336
|
context,
|
|
337
337
|
getOrCreateStore: (forRoute?: string): HelperContext => {
|
|
338
|
-
let store =
|
|
338
|
+
let store = RangoContext.getStore();
|
|
339
339
|
if (!store) {
|
|
340
340
|
store = {
|
|
341
341
|
manifest: new Map<string, EntryData>(),
|
|
@@ -355,7 +355,7 @@ export const getContext = (): {
|
|
|
355
355
|
const store = context.getStore();
|
|
356
356
|
if (!store) {
|
|
357
357
|
throw new Error(
|
|
358
|
-
"
|
|
358
|
+
"Rango context store is not available. Make sure to run within Rango context.",
|
|
359
359
|
);
|
|
360
360
|
}
|
|
361
361
|
return store;
|
|
@@ -372,7 +372,7 @@ export const getContext = (): {
|
|
|
372
372
|
type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
|
|
373
373
|
) => {
|
|
374
374
|
const store = context.getStore();
|
|
375
|
-
invariant(store, "No context
|
|
375
|
+
invariant(store, "No context RangoContext available");
|
|
376
376
|
store.counters[type] ??= 0;
|
|
377
377
|
const index = store.counters[type];
|
|
378
378
|
store.counters[type] = index + 1;
|
|
@@ -382,7 +382,7 @@ export const getContext = (): {
|
|
|
382
382
|
type: "layout" | "parallel" | "route" | "loader" | "cache",
|
|
383
383
|
) => {
|
|
384
384
|
const store = context.getStore();
|
|
385
|
-
invariant(store, "No context
|
|
385
|
+
invariant(store, "No context RangoContext available");
|
|
386
386
|
|
|
387
387
|
const parent = store.parent;
|
|
388
388
|
const prefix =
|
|
@@ -502,7 +502,7 @@ export function runWithPrefixes<T>(
|
|
|
502
502
|
namePrefix: string | undefined,
|
|
503
503
|
callback: () => T,
|
|
504
504
|
): T {
|
|
505
|
-
const store =
|
|
505
|
+
const store = RangoContext.getStore();
|
|
506
506
|
if (!store) {
|
|
507
507
|
throw new Error("runWithPrefixes must be called within router context");
|
|
508
508
|
}
|
|
@@ -547,7 +547,7 @@ export function runWithPrefixes<T>(
|
|
|
547
547
|
? (store.rootScoped ?? false)
|
|
548
548
|
: store.rootScoped;
|
|
549
549
|
|
|
550
|
-
return
|
|
550
|
+
return RangoContext.run(
|
|
551
551
|
{
|
|
552
552
|
...store,
|
|
553
553
|
urlPrefix: combinedUrlPrefix,
|
|
@@ -562,7 +562,7 @@ export function runWithPrefixes<T>(
|
|
|
562
562
|
* Get current URL prefix from context
|
|
563
563
|
*/
|
|
564
564
|
export function getUrlPrefix(): string {
|
|
565
|
-
const store =
|
|
565
|
+
const store = RangoContext.getStore();
|
|
566
566
|
return store?.urlPrefix || "";
|
|
567
567
|
}
|
|
568
568
|
|
|
@@ -570,7 +570,7 @@ export function getUrlPrefix(): string {
|
|
|
570
570
|
* Get current name prefix from context
|
|
571
571
|
*/
|
|
572
572
|
export function getNamePrefix(): string | undefined {
|
|
573
|
-
const store =
|
|
573
|
+
const store = RangoContext.getStore();
|
|
574
574
|
return store?.namePrefix;
|
|
575
575
|
}
|
|
576
576
|
|
|
@@ -579,7 +579,7 @@ export function getNamePrefix(): string | undefined {
|
|
|
579
579
|
* Returns true at root or inside { name: "" } includes, false inside named includes.
|
|
580
580
|
*/
|
|
581
581
|
export function getRootScoped(): boolean {
|
|
582
|
-
const store =
|
|
582
|
+
const store = RangoContext.getStore();
|
|
583
583
|
return store?.rootScoped ?? true;
|
|
584
584
|
}
|
|
585
585
|
|
|
@@ -676,7 +676,7 @@ export function getParallelSlotCount(
|
|
|
676
676
|
* ```
|
|
677
677
|
*/
|
|
678
678
|
export function track(label: string, depth?: number): () => void {
|
|
679
|
-
const store =
|
|
679
|
+
const store = RangoContext.getStore();
|
|
680
680
|
|
|
681
681
|
// No-op if context unavailable or metrics not enabled
|
|
682
682
|
if (!store?.metrics?.enabled) {
|
|
@@ -699,8 +699,8 @@ export function track(label: string, depth?: number): () => void {
|
|
|
699
699
|
|
|
700
700
|
/**
|
|
701
701
|
* Separate ALS for tracking loader execution scope.
|
|
702
|
-
* Uses a dedicated ALS (not
|
|
703
|
-
* nested
|
|
702
|
+
* Uses a dedicated ALS (not RangoContext) to avoid issues with
|
|
703
|
+
* nested RangoContext.run() calls in Vite's module runner.
|
|
704
704
|
*/
|
|
705
705
|
const LOADER_SCOPE_KEY = Symbol.for("rangojs-router:loader-scope");
|
|
706
706
|
const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
|
|
@@ -713,7 +713,7 @@ const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
|
|
|
713
713
|
* (never cached), so non-cacheable reads are safe.
|
|
714
714
|
*/
|
|
715
715
|
export function isInsideCacheScope(): boolean {
|
|
716
|
-
if (
|
|
716
|
+
if (RangoContext.getStore()?.insideCacheScope !== true) return false;
|
|
717
717
|
// Loaders are always fresh — even inside a cache() boundary, the loader
|
|
718
718
|
// function re-executes on every request. Skip the guard when running
|
|
719
719
|
// inside a loader.
|
package/src/static-handler.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* ```typescript
|
|
8
8
|
* // In env.ts or env.d.ts
|
|
9
9
|
* declare global {
|
|
10
|
-
* namespace
|
|
10
|
+
* namespace Rango {
|
|
11
11
|
* interface Env extends AppBindings {}
|
|
12
12
|
* interface Vars extends AppVariables {}
|
|
13
13
|
* }
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
20
|
declare global {
|
|
21
|
-
namespace
|
|
21
|
+
namespace Rango {
|
|
22
22
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
23
23
|
interface Env {
|
|
24
24
|
// Empty by default - users augment with their bindings (e.g., { DB: D1Database })
|
|
@@ -44,13 +44,25 @@ declare global {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* Route map for path-validation surfaces (`href`, `ValidPaths`, `PathResponse`).
|
|
48
|
+
*
|
|
49
|
+
* Resolution order:
|
|
50
|
+
* 1. `RegisteredRoutes` — manual `extends typeof router.routeMap`; richest map,
|
|
51
|
+
* the only one carrying response-route payload metadata.
|
|
52
|
+
* 2. `GeneratedRouteMap` — auto-wired by `router.named-routes.gen.ts`; path +
|
|
53
|
+
* search only. Lets `rango generate` alone enable `href()`/`ValidPaths` path
|
|
54
|
+
* checking without a manual `RegisteredRoutes` declaration.
|
|
55
|
+
* 3. `Record<string, string>` — permissive fallback when nothing is registered.
|
|
56
|
+
*
|
|
57
|
+
* Referencing `GeneratedRouteMap` here is cycle-safe: it is declared in the
|
|
58
|
+
* standalone gen file with no imports, unlike `RegisteredRoutes extends typeof
|
|
59
|
+
* router.routeMap`.
|
|
50
60
|
*/
|
|
51
|
-
export type GetRegisteredRoutes = keyof
|
|
52
|
-
?
|
|
53
|
-
|
|
61
|
+
export type GetRegisteredRoutes = keyof Rango.RegisteredRoutes extends never
|
|
62
|
+
? keyof Rango.GeneratedRouteMap extends never
|
|
63
|
+
? Record<string, string>
|
|
64
|
+
: Rango.GeneratedRouteMap
|
|
65
|
+
: Rango.RegisteredRoutes;
|
|
54
66
|
|
|
55
67
|
/**
|
|
56
68
|
* Default route map for reverse() surfaces.
|
|
@@ -58,12 +70,11 @@ export type GetRegisteredRoutes = keyof RSCRouter.RegisteredRoutes extends never
|
|
|
58
70
|
* cycles, but falls back to RegisteredRoutes for manual augmentation and then to
|
|
59
71
|
* a permissive record when no route types are available.
|
|
60
72
|
*/
|
|
61
|
-
export type DefaultReverseRouteMap =
|
|
62
|
-
keyof
|
|
63
|
-
?
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
: RSCRouter.GeneratedRouteMap;
|
|
73
|
+
export type DefaultReverseRouteMap = keyof Rango.GeneratedRouteMap extends never
|
|
74
|
+
? keyof Rango.RegisteredRoutes extends never
|
|
75
|
+
? Record<string, string>
|
|
76
|
+
: Rango.RegisteredRoutes
|
|
77
|
+
: Rango.GeneratedRouteMap;
|
|
67
78
|
|
|
68
79
|
/**
|
|
69
80
|
* Default route map for Handler type.
|
|
@@ -71,30 +82,32 @@ export type DefaultReverseRouteMap =
|
|
|
71
82
|
* circular dependencies: router.tsx -> urls.tsx -> handler.tsx -> RegisteredRoutes -> router.tsx.
|
|
72
83
|
* GeneratedRouteMap is declared in a standalone gen file with no imports.
|
|
73
84
|
*/
|
|
74
|
-
export type DefaultHandlerRouteMap =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
: RSCRouter.GeneratedRouteMap;
|
|
85
|
+
export type DefaultHandlerRouteMap = keyof Rango.GeneratedRouteMap extends never
|
|
86
|
+
? {}
|
|
87
|
+
: Rango.GeneratedRouteMap;
|
|
78
88
|
|
|
79
89
|
/**
|
|
80
|
-
* Default environment type - uses global augmentation if available
|
|
90
|
+
* Default environment type - uses global augmentation if available.
|
|
91
|
+
*
|
|
92
|
+
* Falls back to `unknown` (not `any`) when `Rango.Env` is not augmented, so
|
|
93
|
+
* an app that forgets to register its bindings gets a compile error on
|
|
94
|
+
* `ctx.env.SOMETHING` instead of silently losing all env type-checking. Augment
|
|
95
|
+
* `Rango.Env` to type `ctx.env`.
|
|
81
96
|
*/
|
|
82
|
-
export type DefaultEnv = keyof
|
|
83
|
-
? any
|
|
84
|
-
: RSCRouter.Env;
|
|
97
|
+
export type DefaultEnv = keyof Rango.Env extends never ? unknown : Rango.Env;
|
|
85
98
|
|
|
86
99
|
/**
|
|
87
100
|
* Default variables type - uses global augmentation if available, Record<string, any> otherwise
|
|
88
101
|
*/
|
|
89
|
-
export type DefaultVars = keyof
|
|
102
|
+
export type DefaultVars = keyof Rango.Vars extends never
|
|
90
103
|
? Record<string, any>
|
|
91
|
-
:
|
|
104
|
+
: Rango.Vars;
|
|
92
105
|
|
|
93
106
|
/**
|
|
94
107
|
* Default route name type for public `routeName` on contexts.
|
|
95
108
|
* When GeneratedRouteMap is augmented, narrows to the known route names.
|
|
96
109
|
* Otherwise falls back to `string` for untyped usage.
|
|
97
110
|
*/
|
|
98
|
-
export type DefaultRouteName = keyof
|
|
111
|
+
export type DefaultRouteName = keyof Rango.GeneratedRouteMap extends never
|
|
99
112
|
? string
|
|
100
|
-
: keyof
|
|
113
|
+
: keyof Rango.GeneratedRouteMap & string;
|
|
@@ -43,7 +43,7 @@ export type { MiddlewareFn } from "../router/middleware.js";
|
|
|
43
43
|
*/
|
|
44
44
|
export type ScopedRouteMap<
|
|
45
45
|
TPrefix extends string,
|
|
46
|
-
TMap =
|
|
46
|
+
TMap = Rango.GeneratedRouteMap,
|
|
47
47
|
> = {
|
|
48
48
|
[K in keyof TMap as K extends `${TPrefix}.${infer Rest}`
|
|
49
49
|
? Rest
|
|
@@ -767,7 +767,7 @@ export type Revalidate<
|
|
|
767
767
|
* Middleware function with typed params and environment
|
|
768
768
|
*
|
|
769
769
|
* @template TParams - Params object (defaults to generic)
|
|
770
|
-
* @template TEnv - Environment type (defaults to global
|
|
770
|
+
* @template TEnv - Environment type (defaults to global Rango.Env)
|
|
771
771
|
*
|
|
772
772
|
* Note: Middleware cannot directly use route names for params typing because
|
|
773
773
|
* middleware is defined during router setup, before RegisteredRoutes is populated.
|
|
@@ -775,7 +775,7 @@ export type Revalidate<
|
|
|
775
775
|
*
|
|
776
776
|
* @example
|
|
777
777
|
* ```typescript
|
|
778
|
-
* // Basic middleware (uses global
|
|
778
|
+
* // Basic middleware (uses global Rango.Env via module augmentation)
|
|
779
779
|
* const middleware: Middleware = async (ctx, next) => {
|
|
780
780
|
* ctx.set("user", { id: "123" }); // Type-safe!
|
|
781
781
|
* await next();
|
|
@@ -264,7 +264,7 @@ export type PathHelpers<TEnv> = {
|
|
|
264
264
|
* Define an intercepting route for soft navigation
|
|
265
265
|
* Note: routeName must match a named path() in this urlpatterns
|
|
266
266
|
*/
|
|
267
|
-
intercept: keyof
|
|
267
|
+
intercept: keyof Rango.GeneratedRouteMap extends never
|
|
268
268
|
? (
|
|
269
269
|
slotName: `@${string}`,
|
|
270
270
|
routeName: string,
|
|
@@ -273,7 +273,7 @@ export type PathHelpers<TEnv> = {
|
|
|
273
273
|
) => InterceptItem
|
|
274
274
|
: (
|
|
275
275
|
slotName: `@${string}`,
|
|
276
|
-
routeName: (keyof
|
|
276
|
+
routeName: (keyof Rango.GeneratedRouteMap & string) | `.${string}`,
|
|
277
277
|
handler: ReactNode | Handler<any, any, TEnv>,
|
|
278
278
|
use?: () => InterceptUseItem[],
|
|
279
279
|
) => InterceptItem;
|
|
@@ -88,6 +88,40 @@ export interface UrlPatterns<
|
|
|
88
88
|
readonly _responses?: TResponses;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Extract the phantom env type carried by a UrlPatterns value.
|
|
93
|
+
*/
|
|
94
|
+
export type UrlPatternsEnv<T> =
|
|
95
|
+
T extends UrlPatterns<infer TEnv, any, any> ? TEnv : never;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Guards `routes()` env compatibility without over-constraining.
|
|
99
|
+
*
|
|
100
|
+
* - An env-agnostic block (its env is `unknown` — e.g. a shared urls() module,
|
|
101
|
+
* or an app that does not augment `Rango.Env`) attaches to any router.
|
|
102
|
+
* - A block carrying a concrete env is accepted only when the router env
|
|
103
|
+
* (`TRouterEnv`) satisfies it; resolves to `never` otherwise, so a
|
|
104
|
+
* `urls<{ DB: D1Database }>()` cannot be mounted on a `createRouter<{}>()`.
|
|
105
|
+
*
|
|
106
|
+
* Use as `patterns: T & EnvCompatible<T, TEnv>` so `T` still infers from the
|
|
107
|
+
* argument — a bare `EnvCompatible<T, TEnv>` parameter sits in a non-inferrable
|
|
108
|
+
* conditional position and would collapse `T` to its constraint.
|
|
109
|
+
*
|
|
110
|
+
* Known limitation: `TRouterEnv extends ...` distributes over a union router env,
|
|
111
|
+
* so a `urls<A>()` block is accepted on `createRouter<A | B>()` even though the
|
|
112
|
+
* `B` arm cannot supply `A`'s env. Suppressing distribution with
|
|
113
|
+
* `[TRouterEnv] extends [...]` would close that edge but breaks the common
|
|
114
|
+
* generic-`TEnv` call sites (a deferred type parameter can't resolve the tuple
|
|
115
|
+
* conditional, so the intersection stops reducing to `T`). A router has one env,
|
|
116
|
+
* so a union env is not a supported pattern; the distributive form is kept.
|
|
117
|
+
*/
|
|
118
|
+
export type EnvCompatible<TPatterns, TRouterEnv> =
|
|
119
|
+
unknown extends UrlPatternsEnv<TPatterns>
|
|
120
|
+
? TPatterns
|
|
121
|
+
: TRouterEnv extends UrlPatternsEnv<TPatterns>
|
|
122
|
+
? TPatterns
|
|
123
|
+
: never;
|
|
124
|
+
|
|
91
125
|
/**
|
|
92
126
|
* Options for include()
|
|
93
127
|
*/
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtractParams } from "../types.js";
|
|
2
|
+
import type { JsonSerialize } from "../serialize.js";
|
|
2
3
|
import type {
|
|
3
4
|
TypedRouteItem,
|
|
4
5
|
TypedIncludeItem,
|
|
@@ -362,11 +363,15 @@ export type ResponseEnvelope<T> =
|
|
|
362
363
|
* type HealthData = RouteResponse<typeof apiPatterns, "health">;
|
|
363
364
|
* // ResponseEnvelope<{ status: string; timestamp: number }>
|
|
364
365
|
* ```
|
|
366
|
+
*
|
|
367
|
+
* The payload is the JSON wire shape (via `Rango.JsonSerialize`), matching
|
|
368
|
+
* `Rango.PathResponse` and what `fetch().then(r => r.json())` actually yields —
|
|
369
|
+
* e.g. a `Date` field resolves as `string`.
|
|
365
370
|
*/
|
|
366
371
|
export type RouteResponse<TPatterns, TName extends string> = TPatterns extends {
|
|
367
372
|
readonly _responses?: infer R;
|
|
368
373
|
}
|
|
369
374
|
? TName extends keyof R
|
|
370
|
-
? ResponseEnvelope<Exclude<R[TName], Response
|
|
375
|
+
? ResponseEnvelope<JsonSerialize<Exclude<R[TName], Response>>>
|
|
371
376
|
: never
|
|
372
377
|
: never;
|
package/src/use-loader.tsx
CHANGED
|
@@ -501,7 +501,7 @@ function useLoaderInternal<T>(
|
|
|
501
501
|
export function useLoader<T>(
|
|
502
502
|
loader: LoaderDefinition<T>,
|
|
503
503
|
options?: UseLoaderOptions,
|
|
504
|
-
): UseLoaderResult<T
|
|
504
|
+
): UseLoaderResult<Rango.FlightSerialize<T>> {
|
|
505
505
|
const result = useLoaderInternal(loader, options);
|
|
506
506
|
|
|
507
507
|
// Strict mode: throw if data is not in context
|
|
@@ -513,7 +513,7 @@ export function useLoader<T>(
|
|
|
513
513
|
);
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
-
return result as UseLoaderResult<T
|
|
516
|
+
return result as UseLoaderResult<Rango.FlightSerialize<T>>;
|
|
517
517
|
}
|
|
518
518
|
|
|
519
519
|
/**
|
|
@@ -565,6 +565,8 @@ export function useLoader<T>(
|
|
|
565
565
|
export function useFetchLoader<T>(
|
|
566
566
|
loader: LoaderDefinition<T>,
|
|
567
567
|
options?: UseLoaderOptions,
|
|
568
|
-
): UseFetchLoaderResult<T
|
|
569
|
-
return useLoaderInternal(loader, options)
|
|
568
|
+
): UseFetchLoaderResult<Rango.FlightSerialize<T>> {
|
|
569
|
+
return useLoaderInternal(loader, options) as UseFetchLoaderResult<
|
|
570
|
+
Rango.FlightSerialize<T>
|
|
571
|
+
>;
|
|
570
572
|
}
|