@rangojs/router 0.0.0-experimental.88a3b2f7 → 0.0.0-experimental.8bd1b239
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/dist/vite/index.js +307 -128
- package/package.json +3 -3
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/middleware/SKILL.md +32 -3
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +764 -0
- package/skills/parallel/SKILL.md +59 -0
- package/skills/rango/SKILL.md +24 -22
- package/skills/route/SKILL.md +24 -0
- package/src/browser/navigation-bridge.ts +21 -2
- package/src/browser/navigation-client.ts +34 -6
- package/src/browser/partial-update.ts +14 -2
- package/src/browser/prefetch/cache.ts +16 -6
- package/src/browser/prefetch/fetch.ts +60 -4
- package/src/browser/react/Link.tsx +25 -2
- package/src/browser/react/use-navigation.ts +11 -10
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/build/route-trie.ts +50 -24
- package/src/client.tsx +84 -230
- package/src/index.ts +41 -9
- package/src/reverse.ts +4 -1
- package/src/route-definition/dsl-helpers.ts +164 -21
- package/src/route-definition/helpers-types.ts +61 -14
- package/src/router/handler-context.ts +4 -1
- package/src/router/loader-resolution.ts +70 -46
- package/src/router/match-middleware/cache-lookup.ts +10 -5
- package/src/router/match-middleware/segment-resolution.ts +1 -1
- package/src/router/match-result.ts +82 -4
- package/src/router/segment-resolution/fresh.ts +52 -0
- package/src/router/segment-resolution/revalidation.ts +69 -1
- package/src/rsc/handler.ts +13 -5
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/progressive-enhancement.ts +10 -2
- package/src/rsc/rsc-rendering.ts +5 -1
- package/src/rsc/server-action.ts +6 -0
- package/src/rsc/types.ts +1 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +11 -61
- package/src/server/handle-store.ts +19 -0
- package/src/server/request-context.ts +54 -14
- package/src/types/segments.ts +1 -1
- package/src/urls/path-helper-types.ts +34 -5
- package/src/use-loader.tsx +77 -5
- package/src/vite/discovery/prerender-collection.ts +124 -83
- package/src/vite/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
- package/src/vite/plugins/expose-internal-ids.ts +257 -40
- package/src/vite/router-discovery.ts +25 -3
- package/src/vite/utils/prerender-utils.ts +20 -6
package/src/build/route-trie.ts
CHANGED
|
@@ -98,8 +98,14 @@ export function buildRouteTrie(
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
|
-
* Insert a route into the trie
|
|
102
|
-
*
|
|
101
|
+
* Insert a route into the trie. Optional params expand into two branches at
|
|
102
|
+
* registration time (skip-first, then present), so each terminal lives at the
|
|
103
|
+
* correct depth for its number of bound params and carries a branch-local
|
|
104
|
+
* `pa` listing only those names. The trie's single-slot `node.p` is reused
|
|
105
|
+
* across branches because matching ignores `node.p.n` — the leaf's `pa` is
|
|
106
|
+
* the source of truth for naming. Skip-first ordering lets `mergeLeaf`'s
|
|
107
|
+
* last-wins rule produce greedy-leftmost semantics for free at any shared
|
|
108
|
+
* terminal depth.
|
|
103
109
|
*/
|
|
104
110
|
function insertRoute(
|
|
105
111
|
node: TrieNode,
|
|
@@ -107,14 +113,13 @@ function insertRoute(
|
|
|
107
113
|
index: number,
|
|
108
114
|
leaf: Omit<TrieLeaf, "op" | "cv" | "pa">,
|
|
109
115
|
): void {
|
|
110
|
-
//
|
|
111
|
-
|
|
116
|
+
// op (full optional list) and cv (full constraint map) are route-level and
|
|
117
|
+
// identical on every terminal, so compute them once on the shared base.
|
|
112
118
|
const optionalParams: string[] = [];
|
|
113
119
|
const constraints: Record<string, string[]> = {};
|
|
114
120
|
|
|
115
121
|
for (const seg of segments) {
|
|
116
122
|
if (seg.type === "param") {
|
|
117
|
-
paramNames.push(seg.value);
|
|
118
123
|
if (seg.optional) {
|
|
119
124
|
optionalParams.push(seg.value);
|
|
120
125
|
}
|
|
@@ -124,21 +129,15 @@ function insertRoute(
|
|
|
124
129
|
}
|
|
125
130
|
}
|
|
126
131
|
|
|
127
|
-
const
|
|
132
|
+
const leafBase: Omit<TrieLeaf, "pa"> = {
|
|
128
133
|
...leaf,
|
|
129
|
-
...(paramNames.length > 0 ? { pa: paramNames } : {}),
|
|
130
134
|
...(optionalParams.length > 0 ? { op: optionalParams } : {}),
|
|
131
135
|
...(Object.keys(constraints).length > 0 ? { cv: constraints } : {}),
|
|
132
136
|
};
|
|
133
137
|
|
|
134
|
-
insertSegments(node, segments, index,
|
|
138
|
+
insertSegments(node, segments, index, leafBase, []);
|
|
135
139
|
}
|
|
136
140
|
|
|
137
|
-
/**
|
|
138
|
-
* Recursively insert segments into the trie.
|
|
139
|
-
* For optional params, we add a terminal at the current node (param absent)
|
|
140
|
-
* AND continue inserting into the param child (param present).
|
|
141
|
-
*/
|
|
142
141
|
/**
|
|
143
142
|
* Extract ancestry map from a built trie by visiting all leaf nodes.
|
|
144
143
|
* Returns { routeName: ancestryShortCodes[] } for every route in the trie.
|
|
@@ -218,15 +217,25 @@ function mergeLeaf(node: TrieNode, leaf: TrieLeaf): void {
|
|
|
218
217
|
node.r = mergeLeaves(node.r, leaf);
|
|
219
218
|
}
|
|
220
219
|
|
|
220
|
+
function buildLeaf(
|
|
221
|
+
leafBase: Omit<TrieLeaf, "pa">,
|
|
222
|
+
paramNames: string[],
|
|
223
|
+
): TrieLeaf {
|
|
224
|
+
return paramNames.length > 0
|
|
225
|
+
? { ...leafBase, pa: [...paramNames] }
|
|
226
|
+
: { ...leafBase };
|
|
227
|
+
}
|
|
228
|
+
|
|
221
229
|
function insertSegments(
|
|
222
230
|
node: TrieNode,
|
|
223
231
|
segments: ParsedSegment[],
|
|
224
232
|
index: number,
|
|
225
|
-
|
|
233
|
+
leafBase: Omit<TrieLeaf, "pa">,
|
|
234
|
+
paramNames: string[],
|
|
226
235
|
): void {
|
|
227
|
-
// Base case: all segments consumed, add terminal
|
|
236
|
+
// Base case: all segments consumed, add terminal with branch-local pa
|
|
228
237
|
if (index >= segments.length) {
|
|
229
|
-
mergeLeaf(node,
|
|
238
|
+
mergeLeaf(node, buildLeaf(leafBase, paramNames));
|
|
230
239
|
return;
|
|
231
240
|
}
|
|
232
241
|
|
|
@@ -235,12 +244,19 @@ function insertSegments(
|
|
|
235
244
|
if (segment.type === "static") {
|
|
236
245
|
if (!node.s) node.s = {};
|
|
237
246
|
if (!node.s[segment.value]) node.s[segment.value] = {};
|
|
238
|
-
insertSegments(
|
|
247
|
+
insertSegments(
|
|
248
|
+
node.s[segment.value],
|
|
249
|
+
segments,
|
|
250
|
+
index + 1,
|
|
251
|
+
leafBase,
|
|
252
|
+
paramNames,
|
|
253
|
+
);
|
|
239
254
|
} else if (segment.type === "param") {
|
|
240
255
|
if (segment.optional) {
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
//
|
|
256
|
+
// SKIP first: continue at the same node without binding this name.
|
|
257
|
+
// Skip-first ordering means the present-branch's TAKE overwrites any
|
|
258
|
+
// shared terminal later, giving greedy-leftmost semantics.
|
|
259
|
+
insertSegments(node, segments, index + 1, leafBase, paramNames);
|
|
244
260
|
}
|
|
245
261
|
if (segment.suffix) {
|
|
246
262
|
// Suffix param: keyed by suffix string (e.g., ".html")
|
|
@@ -248,16 +264,26 @@ function insertSegments(
|
|
|
248
264
|
if (!node.xp[segment.suffix]) {
|
|
249
265
|
node.xp[segment.suffix] = { n: segment.value, c: {} };
|
|
250
266
|
}
|
|
251
|
-
insertSegments(node.xp[segment.suffix].c, segments, index + 1,
|
|
267
|
+
insertSegments(node.xp[segment.suffix].c, segments, index + 1, leafBase, [
|
|
268
|
+
...paramNames,
|
|
269
|
+
segment.value,
|
|
270
|
+
]);
|
|
252
271
|
} else {
|
|
253
272
|
if (!node.p) {
|
|
254
273
|
node.p = { n: segment.value, c: {} };
|
|
255
274
|
}
|
|
256
|
-
insertSegments(node.p.c, segments, index + 1,
|
|
275
|
+
insertSegments(node.p.c, segments, index + 1, leafBase, [
|
|
276
|
+
...paramNames,
|
|
277
|
+
segment.value,
|
|
278
|
+
]);
|
|
257
279
|
}
|
|
258
280
|
} else if (segment.type === "wildcard") {
|
|
259
|
-
// Wildcard consumes all remaining segments
|
|
260
|
-
|
|
281
|
+
// Wildcard consumes all remaining segments. Carry any params bound before
|
|
282
|
+
// the wildcard in pa so they zip correctly against paramValues at match.
|
|
283
|
+
const wildLeaf: TrieLeaf & { pn: string } = {
|
|
284
|
+
...buildLeaf(leafBase, paramNames),
|
|
285
|
+
pn: "*",
|
|
286
|
+
};
|
|
261
287
|
const existing = node.w ? ({ ...node.w } as TrieLeaf) : undefined;
|
|
262
288
|
const merged = mergeLeaves(existing, wildLeaf);
|
|
263
289
|
node.w = merged as TrieLeaf & { pn: string };
|
package/src/client.tsx
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
type ClientErrorBoundaryFallbackProps,
|
|
14
14
|
type ErrorInfo,
|
|
15
15
|
type LoaderDefinition,
|
|
16
|
-
type LoaderFn,
|
|
17
16
|
type ResolvedSegment,
|
|
18
17
|
} from "./types";
|
|
19
18
|
import {
|
|
@@ -22,6 +21,83 @@ import {
|
|
|
22
21
|
} from "./route-content-wrapper.js";
|
|
23
22
|
import { OutletProvider } from "./outlet-provider.js";
|
|
24
23
|
import { MountContextProvider } from "./browser/react/mount-context.js";
|
|
24
|
+
import { getMemoizedContentPromise } from "./segment-content-promise.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Render the content for a named parallel/intercept slot segment.
|
|
28
|
+
*
|
|
29
|
+
* Shared by Outlet (with `name` prop) and ParallelOutlet — both resolve a
|
|
30
|
+
* segment from context.parallel by slot name and then render it through the
|
|
31
|
+
* same layout/loader/mountPath wrapping pipeline.
|
|
32
|
+
*/
|
|
33
|
+
function renderSlotContent(segment: ResolvedSegment | null): ReactNode {
|
|
34
|
+
if (!segment) return null;
|
|
35
|
+
|
|
36
|
+
const content: ReactNode =
|
|
37
|
+
segment.loading || segment.component instanceof Promise ? (
|
|
38
|
+
<RouteContentWrapper
|
|
39
|
+
content={getMemoizedContentPromise(segment.component)}
|
|
40
|
+
fallback={segment.loading}
|
|
41
|
+
segmentId={segment.id}
|
|
42
|
+
/>
|
|
43
|
+
) : (
|
|
44
|
+
(segment.component ?? null)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const hasOwnLoaders = !!(segment.loaderDataPromise && segment.loaderIds);
|
|
48
|
+
const loaderWrapped = hasOwnLoaders ? (
|
|
49
|
+
<LoaderBoundary
|
|
50
|
+
loaderDataPromise={segment.loaderDataPromise!}
|
|
51
|
+
loaderIds={segment.loaderIds!}
|
|
52
|
+
fallback={segment.loading}
|
|
53
|
+
outletKey={segment.id + "-loader"}
|
|
54
|
+
outletContent={null}
|
|
55
|
+
segment={segment}
|
|
56
|
+
>
|
|
57
|
+
{content}
|
|
58
|
+
</LoaderBoundary>
|
|
59
|
+
) : null;
|
|
60
|
+
|
|
61
|
+
let result: ReactNode;
|
|
62
|
+
if (segment.layout) {
|
|
63
|
+
// Layout renders immediately; if loaders exist, the LoaderBoundary becomes
|
|
64
|
+
// the outlet content so layout's <Outlet /> suspends until loaders resolve.
|
|
65
|
+
result = (
|
|
66
|
+
<OutletProvider
|
|
67
|
+
content={hasOwnLoaders ? loaderWrapped : content}
|
|
68
|
+
segment={segment}
|
|
69
|
+
>
|
|
70
|
+
{segment.layout}
|
|
71
|
+
</OutletProvider>
|
|
72
|
+
);
|
|
73
|
+
} else if (hasOwnLoaders) {
|
|
74
|
+
// No layout but has loaders — wrap content with LoaderBoundary for useLoader context.
|
|
75
|
+
// Common for intercept routes that use useLoader without a custom layout.
|
|
76
|
+
result = loaderWrapped;
|
|
77
|
+
} else {
|
|
78
|
+
result = content;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (segment.mountPath) {
|
|
82
|
+
return (
|
|
83
|
+
<MountContextProvider value={segment.mountPath}>
|
|
84
|
+
{result}
|
|
85
|
+
</MountContextProvider>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function useSlotSegment(
|
|
93
|
+
context: OutletContextValue | null,
|
|
94
|
+
name: `@${string}` | undefined,
|
|
95
|
+
): ResolvedSegment | null {
|
|
96
|
+
return useMemo(() => {
|
|
97
|
+
if (!name || !context?.parallel) return null;
|
|
98
|
+
return context.parallel.find((seg) => seg.slot === name) ?? null;
|
|
99
|
+
}, [context, name]);
|
|
100
|
+
}
|
|
25
101
|
|
|
26
102
|
/**
|
|
27
103
|
* Outlet component - renders child content in layouts
|
|
@@ -62,95 +138,10 @@ import { MountContextProvider } from "./browser/react/mount-context.js";
|
|
|
62
138
|
*/
|
|
63
139
|
export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
|
|
64
140
|
const context = useContext(OutletContext);
|
|
141
|
+
const namedSegment = useSlotSegment(context, name);
|
|
65
142
|
|
|
66
|
-
// If name provided, render parallel/intercept content for that slot
|
|
67
143
|
if (name) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!segment) return null;
|
|
71
|
-
|
|
72
|
-
// Determine the content to render
|
|
73
|
-
let content: ReactNode;
|
|
74
|
-
if (segment.loading || segment.component instanceof Promise) {
|
|
75
|
-
// Use RouteContentWrapper to handle Suspense wrapping properly
|
|
76
|
-
content = (
|
|
77
|
-
<RouteContentWrapper
|
|
78
|
-
content={
|
|
79
|
-
segment.component instanceof Promise
|
|
80
|
-
? segment.component
|
|
81
|
-
: Promise.resolve(segment.component)
|
|
82
|
-
}
|
|
83
|
-
fallback={segment.loading}
|
|
84
|
-
segmentId={segment.id}
|
|
85
|
-
/>
|
|
86
|
-
);
|
|
87
|
-
} else {
|
|
88
|
-
content = segment.component ?? null;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
let result: ReactNode;
|
|
92
|
-
|
|
93
|
-
// If segment has a layout, wrap appropriately
|
|
94
|
-
if (segment.layout) {
|
|
95
|
-
// Check if this segment has loaders that need streaming
|
|
96
|
-
// The layout renders immediately, LoaderBoundary becomes the outlet content
|
|
97
|
-
// When layout renders <Outlet />, it gets the LoaderBoundary which suspends
|
|
98
|
-
if (segment.loaderDataPromise && segment.loaderIds) {
|
|
99
|
-
const loaderAwareContent = (
|
|
100
|
-
<LoaderBoundary
|
|
101
|
-
loaderDataPromise={segment.loaderDataPromise}
|
|
102
|
-
loaderIds={segment.loaderIds}
|
|
103
|
-
fallback={segment.loading}
|
|
104
|
-
outletKey={segment.id + "-loader"}
|
|
105
|
-
outletContent={null}
|
|
106
|
-
segment={segment}
|
|
107
|
-
>
|
|
108
|
-
{content}
|
|
109
|
-
</LoaderBoundary>
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
result = (
|
|
113
|
-
<OutletProvider content={loaderAwareContent} segment={segment}>
|
|
114
|
-
{segment.layout}
|
|
115
|
-
</OutletProvider>
|
|
116
|
-
);
|
|
117
|
-
} else {
|
|
118
|
-
// No loaders - wrap in OutletProvider so layout can use <Outlet />
|
|
119
|
-
result = (
|
|
120
|
-
<OutletProvider content={content} segment={segment}>
|
|
121
|
-
{segment.layout}
|
|
122
|
-
</OutletProvider>
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
} else if (segment.loaderDataPromise && segment.loaderIds) {
|
|
126
|
-
// No layout but has loaders - wrap content with LoaderBoundary for useLoader context
|
|
127
|
-
// This is common for intercept routes that use useLoader without a custom layout
|
|
128
|
-
result = (
|
|
129
|
-
<LoaderBoundary
|
|
130
|
-
loaderDataPromise={segment.loaderDataPromise}
|
|
131
|
-
loaderIds={segment.loaderIds}
|
|
132
|
-
fallback={segment.loading}
|
|
133
|
-
outletKey={segment.id + "-loader"}
|
|
134
|
-
outletContent={null}
|
|
135
|
-
segment={segment}
|
|
136
|
-
>
|
|
137
|
-
{content}
|
|
138
|
-
</LoaderBoundary>
|
|
139
|
-
);
|
|
140
|
-
} else {
|
|
141
|
-
result = content;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Wrap with MountContextProvider for include() scoped parallel/intercept slots
|
|
145
|
-
if (segment.mountPath) {
|
|
146
|
-
return (
|
|
147
|
-
<MountContextProvider value={segment.mountPath}>
|
|
148
|
-
{result}
|
|
149
|
-
</MountContextProvider>
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return result;
|
|
144
|
+
return renderSlotContent(namedSegment);
|
|
154
145
|
}
|
|
155
146
|
|
|
156
147
|
// Default: render child content
|
|
@@ -164,6 +155,7 @@ export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
|
|
|
164
155
|
|
|
165
156
|
return content;
|
|
166
157
|
}
|
|
158
|
+
|
|
167
159
|
/**
|
|
168
160
|
* ParallelOutlet component - renders content for a named parallel slot
|
|
169
161
|
*
|
|
@@ -188,94 +180,9 @@ export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
|
|
|
188
180
|
*/
|
|
189
181
|
export function ParallelOutlet({ name }: { name: `@${string}` }): ReactNode {
|
|
190
182
|
const context = useContext(OutletContext);
|
|
191
|
-
const segment =
|
|
192
|
-
if (!context?.parallel) return null;
|
|
193
|
-
return context.parallel.find((seg) => seg.slot === name) ?? null;
|
|
194
|
-
}, [context, name]);
|
|
183
|
+
const segment = useSlotSegment(context, name);
|
|
195
184
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// Determine the content to render
|
|
199
|
-
let content: ReactNode;
|
|
200
|
-
if (segment.loading || segment.component instanceof Promise) {
|
|
201
|
-
// Use RouteContentWrapper to handle Suspense wrapping properly
|
|
202
|
-
content = (
|
|
203
|
-
<RouteContentWrapper
|
|
204
|
-
content={
|
|
205
|
-
segment.component instanceof Promise
|
|
206
|
-
? segment.component
|
|
207
|
-
: Promise.resolve(segment.component)
|
|
208
|
-
}
|
|
209
|
-
fallback={segment.loading}
|
|
210
|
-
segmentId={segment.id}
|
|
211
|
-
/>
|
|
212
|
-
);
|
|
213
|
-
} else {
|
|
214
|
-
content = segment.component ?? null;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
let result: ReactNode;
|
|
218
|
-
|
|
219
|
-
// If segment has a layout, wrap appropriately
|
|
220
|
-
if (segment.layout) {
|
|
221
|
-
// Check if this segment has loaders that need streaming
|
|
222
|
-
// The layout renders immediately, LoaderBoundary becomes the outlet content
|
|
223
|
-
if (segment.loaderDataPromise && segment.loaderIds) {
|
|
224
|
-
const loaderAwareContent = (
|
|
225
|
-
<LoaderBoundary
|
|
226
|
-
loaderDataPromise={segment.loaderDataPromise}
|
|
227
|
-
loaderIds={segment.loaderIds}
|
|
228
|
-
fallback={segment.loading}
|
|
229
|
-
outletKey={segment.id + "-loader"}
|
|
230
|
-
outletContent={null}
|
|
231
|
-
segment={segment}
|
|
232
|
-
>
|
|
233
|
-
{content}
|
|
234
|
-
</LoaderBoundary>
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
result = (
|
|
238
|
-
<OutletProvider content={loaderAwareContent} segment={segment}>
|
|
239
|
-
{segment.layout}
|
|
240
|
-
</OutletProvider>
|
|
241
|
-
);
|
|
242
|
-
} else {
|
|
243
|
-
// No loaders - wrap in OutletProvider so layout can use <Outlet />
|
|
244
|
-
result = (
|
|
245
|
-
<OutletProvider content={content} segment={segment}>
|
|
246
|
-
{segment.layout}
|
|
247
|
-
</OutletProvider>
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
} else if (segment.loaderDataPromise && segment.loaderIds) {
|
|
251
|
-
// No layout but has loaders - wrap content with LoaderBoundary for useLoader context
|
|
252
|
-
// This is common for intercept routes that use useLoader without a custom layout
|
|
253
|
-
result = (
|
|
254
|
-
<LoaderBoundary
|
|
255
|
-
loaderDataPromise={segment.loaderDataPromise}
|
|
256
|
-
loaderIds={segment.loaderIds}
|
|
257
|
-
fallback={segment.loading}
|
|
258
|
-
outletKey={segment.id + "-loader"}
|
|
259
|
-
outletContent={null}
|
|
260
|
-
segment={segment}
|
|
261
|
-
>
|
|
262
|
-
{content}
|
|
263
|
-
</LoaderBoundary>
|
|
264
|
-
);
|
|
265
|
-
} else {
|
|
266
|
-
result = content;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Wrap with MountContextProvider for include() scoped parallel/intercept slots
|
|
270
|
-
if (segment.mountPath) {
|
|
271
|
-
return (
|
|
272
|
-
<MountContextProvider value={segment.mountPath}>
|
|
273
|
-
{result}
|
|
274
|
-
</MountContextProvider>
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return result;
|
|
185
|
+
return renderSlotContent(segment);
|
|
279
186
|
}
|
|
280
187
|
|
|
281
188
|
// OutletProvider is defined in outlet-provider.tsx to break a circular
|
|
@@ -313,57 +220,6 @@ export {
|
|
|
313
220
|
type UseLoaderOptions,
|
|
314
221
|
} from "./use-loader.js";
|
|
315
222
|
|
|
316
|
-
/**
|
|
317
|
-
* Client-safe createLoader factory
|
|
318
|
-
*
|
|
319
|
-
* Creates a loader definition that can be used with useLoader().
|
|
320
|
-
* This is the client-side version that only stores the $$id - the function
|
|
321
|
-
* is ignored since loaders only execute on the server.
|
|
322
|
-
*
|
|
323
|
-
* The $$id is injected by the exposeLoaderId Vite plugin. In most cases,
|
|
324
|
-
* you should import the loader directly from the server file rather than
|
|
325
|
-
* creating a reference manually.
|
|
326
|
-
*
|
|
327
|
-
* @param fn - Loader function (ignored on client, kept for API compatibility)
|
|
328
|
-
* @param _fetchable - Optional fetchable flag (ignored on client)
|
|
329
|
-
* @param __injectedId - $$id injected by Vite plugin
|
|
330
|
-
*
|
|
331
|
-
* @example
|
|
332
|
-
* ```tsx
|
|
333
|
-
* "use client";
|
|
334
|
-
* import { useLoader } from "rsc-router/client";
|
|
335
|
-
* import { CartLoader } from "../loaders/cart"; // Import from server file
|
|
336
|
-
*
|
|
337
|
-
* export function CartIcon() {
|
|
338
|
-
* const cart = useLoader(CartLoader);
|
|
339
|
-
* return <span>Cart ({cart?.items.length ?? 0})</span>;
|
|
340
|
-
* }
|
|
341
|
-
* ```
|
|
342
|
-
*/
|
|
343
|
-
// Overload 1: With function only (not fetchable)
|
|
344
|
-
export function createLoader<T>(
|
|
345
|
-
fn: LoaderFn<T, Record<string, string | undefined>, any>,
|
|
346
|
-
): LoaderDefinition<Awaited<T>, Record<string, string | undefined>>;
|
|
347
|
-
|
|
348
|
-
// Overload 2: With function and fetchable flag
|
|
349
|
-
export function createLoader<T>(
|
|
350
|
-
fn: LoaderFn<T, Record<string, string | undefined>, any>,
|
|
351
|
-
fetchable: true,
|
|
352
|
-
): LoaderDefinition<Awaited<T>, Record<string, string | undefined>>;
|
|
353
|
-
|
|
354
|
-
// Implementation - function is ignored at runtime on client
|
|
355
|
-
// The $$id is injected by Vite plugin as hidden third parameter
|
|
356
|
-
export function createLoader(
|
|
357
|
-
_fn: LoaderFn<any, Record<string, string | undefined>, any>,
|
|
358
|
-
_fetchable?: true,
|
|
359
|
-
__injectedId?: string,
|
|
360
|
-
): LoaderDefinition<any, Record<string, string | undefined>> {
|
|
361
|
-
return {
|
|
362
|
-
__brand: "loader",
|
|
363
|
-
$$id: __injectedId || "",
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
|
|
367
223
|
/**
|
|
368
224
|
* Props for the ErrorBoundary component
|
|
369
225
|
*/
|
|
@@ -534,10 +390,8 @@ export {
|
|
|
534
390
|
type ScrollRestorationProps,
|
|
535
391
|
} from "./browser/react/ScrollRestoration.js";
|
|
536
392
|
|
|
537
|
-
// Handle
|
|
538
|
-
export {
|
|
539
|
-
|
|
540
|
-
// Handle data hook
|
|
393
|
+
// Handle data hook (client-side only — createHandle/isHandle are server APIs from the root export)
|
|
394
|
+
export { type Handle } from "./handle.js";
|
|
541
395
|
export { useHandle } from "./browser/react/use-handle.js";
|
|
542
396
|
|
|
543
397
|
// Built-in handles
|
package/src/index.ts
CHANGED
|
@@ -147,24 +147,52 @@ export { createVar, type ContextVar } from "./context-var.js";
|
|
|
147
147
|
export { nonce } from "./rsc/nonce.js";
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
|
-
*
|
|
150
|
+
* SSR/client stub for server-only `Prerender` function.
|
|
151
|
+
*
|
|
152
|
+
* Returns a lightweight stub object instead of throwing so that the
|
|
153
|
+
* production SSR build can safely bundle the RSC entry chunk — the SSR
|
|
154
|
+
* bundler resolves `@rangojs/router` to this (SSR) entry, so Prerender
|
|
155
|
+
* calls in RSC code must not crash at module-evaluation time.
|
|
151
156
|
*/
|
|
152
|
-
export function Prerender(
|
|
153
|
-
|
|
157
|
+
export function Prerender(
|
|
158
|
+
_handler?: any,
|
|
159
|
+
_optionsOrId?: any,
|
|
160
|
+
__injectedId?: string,
|
|
161
|
+
): any {
|
|
162
|
+
const id =
|
|
163
|
+
typeof _optionsOrId === "string" ? _optionsOrId : __injectedId || "";
|
|
164
|
+
return { __brand: "prerenderHandler" as const, $$id: id };
|
|
154
165
|
}
|
|
155
166
|
|
|
156
167
|
/**
|
|
157
|
-
*
|
|
168
|
+
* SSR/client stub for server-only `Passthrough` function.
|
|
158
169
|
*/
|
|
159
|
-
export function Passthrough(
|
|
160
|
-
|
|
170
|
+
export function Passthrough(
|
|
171
|
+
_handler?: any,
|
|
172
|
+
_optionsOrId?: any,
|
|
173
|
+
__injectedId?: string,
|
|
174
|
+
): any {
|
|
175
|
+
const id =
|
|
176
|
+
typeof _optionsOrId === "string" ? _optionsOrId : __injectedId || "";
|
|
177
|
+
return { __brand: "passthroughHandler" as const, $$id: id };
|
|
161
178
|
}
|
|
162
179
|
|
|
163
180
|
/**
|
|
164
|
-
*
|
|
181
|
+
* SSR/client stub for server-only `Static` function.
|
|
182
|
+
*
|
|
183
|
+
* Returns a lightweight stub object instead of throwing so that the
|
|
184
|
+
* production SSR build can safely bundle the RSC entry chunk — the SSR
|
|
185
|
+
* bundler resolves `@rangojs/router` to this (SSR) entry, so Static
|
|
186
|
+
* calls in RSC code must not crash at module-evaluation time.
|
|
165
187
|
*/
|
|
166
|
-
export function Static(
|
|
167
|
-
|
|
188
|
+
export function Static(
|
|
189
|
+
_handler?: any,
|
|
190
|
+
_optionsOrId?: any,
|
|
191
|
+
__injectedId?: string,
|
|
192
|
+
): any {
|
|
193
|
+
const id =
|
|
194
|
+
typeof _optionsOrId === "string" ? _optionsOrId : __injectedId || "";
|
|
195
|
+
return { __brand: "staticHandler" as const, $$id: id };
|
|
168
196
|
}
|
|
169
197
|
|
|
170
198
|
/**
|
|
@@ -243,6 +271,10 @@ export type {
|
|
|
243
271
|
ReadonlyHeaders,
|
|
244
272
|
} from "./server/cookie-store.js";
|
|
245
273
|
|
|
274
|
+
// Built-in handles (universal — work on both server and client)
|
|
275
|
+
export { Meta } from "./handles/meta.js";
|
|
276
|
+
export { Breadcrumbs } from "./handles/breadcrumbs.js";
|
|
277
|
+
|
|
246
278
|
// Meta types
|
|
247
279
|
export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
|
|
248
280
|
|
package/src/reverse.ts
CHANGED
|
@@ -311,7 +311,10 @@ export function createReverse<TRoutes extends Record<string, string>>(
|
|
|
311
311
|
/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(\?)/g,
|
|
312
312
|
(_, key, _constraint, optional) => {
|
|
313
313
|
const value = params[key];
|
|
314
|
-
|
|
314
|
+
// Empty string is treated as omitted — the trie matcher fills
|
|
315
|
+
// unmatched optional params with "" (not undefined), so reverse
|
|
316
|
+
// must collapse those segments instead of leaving empty slots.
|
|
317
|
+
if (value === undefined || value === "") {
|
|
315
318
|
hadOmittedOptional = true;
|
|
316
319
|
return "";
|
|
317
320
|
}
|