@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1b930379
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 +46 -12
- package/dist/bin/rango.js +109 -15
- package/dist/vite/index.js +323 -121
- package/package.json +15 -16
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +33 -31
- package/skills/host-router/SKILL.md +218 -0
- package/skills/loader/SKILL.md +55 -15
- package/skills/prerender/SKILL.md +2 -2
- package/skills/rango/SKILL.md +0 -1
- package/skills/route/SKILL.md +3 -4
- package/skills/router-setup/SKILL.md +8 -3
- package/skills/typesafety/SKILL.md +25 -23
- package/src/__internal.ts +92 -0
- package/src/bin/rango.ts +18 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +95 -5
- package/src/browser/navigation-client.ts +97 -72
- package/src/browser/prefetch/cache.ts +112 -25
- package/src/browser/prefetch/fetch.ts +28 -30
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/react/Link.tsx +19 -7
- package/src/browser/rsc-router.tsx +11 -2
- package/src/browser/server-action-bridge.ts +448 -432
- package/src/browser/types.ts +24 -0
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/router-processing.ts +125 -15
- package/src/client.rsc.tsx +2 -1
- package/src/client.tsx +1 -46
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/host/index.ts +0 -3
- package/src/index.rsc.ts +5 -36
- package/src/index.ts +32 -66
- package/src/prerender/store.ts +56 -15
- package/src/route-definition/index.ts +0 -3
- package/src/router/handler-context.ts +30 -3
- package/src/router/loader-resolution.ts +1 -1
- package/src/router/match-api.ts +1 -1
- package/src/router/match-result.ts +0 -9
- package/src/router/metrics.ts +233 -13
- package/src/router/middleware-types.ts +53 -10
- package/src/router/middleware.ts +170 -81
- package/src/router/pattern-matching.ts +20 -5
- package/src/router/prerender-match.ts +4 -0
- package/src/router/revalidation.ts +27 -7
- package/src/router/router-interfaces.ts +14 -1
- package/src/router/router-options.ts +13 -8
- package/src/router/segment-resolution/fresh.ts +18 -0
- package/src/router/segment-resolution/helpers.ts +1 -1
- package/src/router/segment-resolution/revalidation.ts +22 -9
- package/src/router/trie-matching.ts +20 -2
- package/src/router.ts +29 -9
- package/src/rsc/handler.ts +106 -11
- package/src/rsc/index.ts +0 -20
- package/src/rsc/progressive-enhancement.ts +21 -8
- package/src/rsc/rsc-rendering.ts +30 -43
- package/src/rsc/server-action.ts +14 -10
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +2 -0
- package/src/search-params.ts +16 -13
- package/src/server/context.ts +8 -2
- package/src/server/request-context.ts +38 -16
- package/src/server.ts +6 -0
- package/src/theme/index.ts +4 -13
- package/src/types/handler-context.ts +12 -16
- package/src/types/route-config.ts +17 -8
- package/src/types/segments.ts +0 -5
- package/src/vite/discovery/bundle-postprocess.ts +31 -56
- package/src/vite/discovery/discover-routers.ts +18 -4
- package/src/vite/discovery/prerender-collection.ts +34 -14
- package/src/vite/discovery/state.ts +4 -7
- package/src/vite/index.ts +4 -3
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/rango.ts +11 -0
- package/src/vite/router-discovery.ts +16 -0
- package/src/vite/utils/prerender-utils.ts +60 -0
- package/skills/testing/SKILL.md +0 -226
- package/src/route-definition/route-function.ts +0 -119
- /package/{CLAUDE.md → AGENTS.md} +0 -0
package/src/browser/types.ts
CHANGED
|
@@ -55,6 +55,11 @@ export interface RscMetadata {
|
|
|
55
55
|
* Used to detect version mismatches after HMR/deployment.
|
|
56
56
|
*/
|
|
57
57
|
version?: string;
|
|
58
|
+
/**
|
|
59
|
+
* TTL in milliseconds for the client-side in-memory prefetch cache.
|
|
60
|
+
* Sent on initial render so the browser can configure its cache duration.
|
|
61
|
+
*/
|
|
62
|
+
prefetchCacheTTL?: number;
|
|
58
63
|
/**
|
|
59
64
|
* Theme configuration from router.
|
|
60
65
|
* Included when theme is enabled in router config.
|
|
@@ -227,6 +232,25 @@ export type HistoryState =
|
|
|
227
232
|
export interface NavigateOptions {
|
|
228
233
|
replace?: boolean;
|
|
229
234
|
scroll?: boolean;
|
|
235
|
+
/**
|
|
236
|
+
* Whether to revalidate server data on navigation.
|
|
237
|
+
* Set to `false` to skip the RSC server fetch and only update the URL.
|
|
238
|
+
*
|
|
239
|
+
* Only takes effect when the pathname stays the same (search param / hash changes).
|
|
240
|
+
* If the pathname changes, this option is ignored and a full navigation occurs.
|
|
241
|
+
*
|
|
242
|
+
* All location-aware hooks (`useSearchParams`, `useNavigation`, etc.) still update.
|
|
243
|
+
* Server components do not re-render.
|
|
244
|
+
*
|
|
245
|
+
* @default true
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* ```tsx
|
|
249
|
+
* router.push("/products?color=blue", { revalidate: false });
|
|
250
|
+
* router.replace("/products?page=3", { revalidate: false });
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
revalidate?: boolean;
|
|
230
254
|
/**
|
|
231
255
|
* State to pass to history.pushState/replaceState
|
|
232
256
|
* Accessible via useLocationState() hook.
|
|
@@ -28,6 +28,8 @@ export {
|
|
|
28
28
|
buildCombinedRouteMapForRouterFile,
|
|
29
29
|
detectUnresolvableIncludes,
|
|
30
30
|
detectUnresolvableIncludesForUrlsFile,
|
|
31
|
+
findNestedRouterConflict,
|
|
32
|
+
formatNestedRouterConflictError,
|
|
31
33
|
findRouterFiles,
|
|
32
34
|
writeCombinedRouteTypes,
|
|
33
35
|
} from "./route-types/router-processing.js";
|
package/src/build/route-trie.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface TrieNode {
|
|
|
47
47
|
s?: Record<string, TrieNode>;
|
|
48
48
|
/** Param child: { n: paramName, c: child node } */
|
|
49
49
|
p?: { n: string; c: TrieNode };
|
|
50
|
+
/** Suffix-param children keyed by suffix (e.g., ".html" → { n: "productId", c: ... }) */
|
|
51
|
+
xp?: Record<string, { n: string; c: TrieNode }>;
|
|
50
52
|
/** Wildcard terminal: leaf + paramName */
|
|
51
53
|
w?: TrieLeaf & { pn: string };
|
|
52
54
|
}
|
|
@@ -158,6 +160,11 @@ export function extractAncestryFromTrie(
|
|
|
158
160
|
visit(child);
|
|
159
161
|
}
|
|
160
162
|
}
|
|
163
|
+
if (node.xp) {
|
|
164
|
+
for (const child of Object.values(node.xp)) {
|
|
165
|
+
visit(child.c);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
161
168
|
if (node.p) {
|
|
162
169
|
visit(node.p.c);
|
|
163
170
|
}
|
|
@@ -235,10 +242,19 @@ function insertSegments(
|
|
|
235
242
|
mergeLeaf(node, leaf);
|
|
236
243
|
// AND continue with param child (param present)
|
|
237
244
|
}
|
|
238
|
-
if (
|
|
239
|
-
|
|
245
|
+
if (segment.suffix) {
|
|
246
|
+
// Suffix param: keyed by suffix string (e.g., ".html")
|
|
247
|
+
if (!node.xp) node.xp = {};
|
|
248
|
+
if (!node.xp[segment.suffix]) {
|
|
249
|
+
node.xp[segment.suffix] = { n: segment.value, c: {} };
|
|
250
|
+
}
|
|
251
|
+
insertSegments(node.xp[segment.suffix].c, segments, index + 1, leaf);
|
|
252
|
+
} else {
|
|
253
|
+
if (!node.p) {
|
|
254
|
+
node.p = { n: segment.value, c: {} };
|
|
255
|
+
}
|
|
256
|
+
insertSegments(node.p.c, segments, index + 1, leaf);
|
|
240
257
|
}
|
|
241
|
-
insertSegments(node.p.c, segments, index + 1, leaf);
|
|
242
258
|
} else if (segment.type === "wildcard") {
|
|
243
259
|
// Wildcard consumes all remaining segments
|
|
244
260
|
const wildLeaf = { ...leaf, pn: "*" };
|
|
@@ -1,9 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
readFileSync,
|
|
3
|
+
writeFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
unlinkSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import {
|
|
9
|
+
join,
|
|
10
|
+
dirname,
|
|
11
|
+
resolve,
|
|
12
|
+
sep,
|
|
13
|
+
basename as pathBasename,
|
|
14
|
+
} from "node:path";
|
|
3
15
|
import ts from "typescript";
|
|
4
16
|
import { generateRouteTypesSource } from "./codegen.js";
|
|
5
17
|
import type { ScanFilter } from "./scan-filter.js";
|
|
6
|
-
import { findTsFiles } from "./scan-filter.js";
|
|
7
18
|
import {
|
|
8
19
|
resolveImportedVariable,
|
|
9
20
|
resolveImportPath,
|
|
@@ -26,6 +37,111 @@ function countPublicRouteEntries(source: string): number {
|
|
|
26
37
|
return count;
|
|
27
38
|
}
|
|
28
39
|
|
|
40
|
+
const ROUTER_CALL_PATTERN = /\bcreateRouter\s*[<(]/;
|
|
41
|
+
|
|
42
|
+
function isRoutableSourceFile(name: string): boolean {
|
|
43
|
+
return (
|
|
44
|
+
(name.endsWith(".ts") ||
|
|
45
|
+
name.endsWith(".tsx") ||
|
|
46
|
+
name.endsWith(".js") ||
|
|
47
|
+
name.endsWith(".jsx")) &&
|
|
48
|
+
!name.includes(".gen.")
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function findRouterFilesRecursive(
|
|
53
|
+
dir: string,
|
|
54
|
+
filter: ScanFilter | undefined,
|
|
55
|
+
results: string[],
|
|
56
|
+
): void {
|
|
57
|
+
let entries;
|
|
58
|
+
try {
|
|
59
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.warn(
|
|
62
|
+
`[rsc-router] Failed to scan directory ${dir}: ${(err as Error).message}`,
|
|
63
|
+
);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const childDirs: string[] = [];
|
|
68
|
+
const routerFilesInDir: string[] = [];
|
|
69
|
+
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
const fullPath = join(dir, entry.name);
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
74
|
+
childDirs.push(fullPath);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!isRoutableSourceFile(entry.name)) continue;
|
|
79
|
+
if (filter && !filter(fullPath)) continue;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const source = readFileSync(fullPath, "utf-8");
|
|
83
|
+
if (ROUTER_CALL_PATTERN.test(source)) {
|
|
84
|
+
routerFilesInDir.push(fullPath);
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// A directory that contains a router file is treated as a router root.
|
|
92
|
+
// Once found, deeper directories are skipped to avoid redundant scans.
|
|
93
|
+
if (routerFilesInDir.length > 0) {
|
|
94
|
+
results.push(...routerFilesInDir);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const childDir of childDirs) {
|
|
99
|
+
findRouterFilesRecursive(childDir, filter, results);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function findNestedRouterConflict(
|
|
104
|
+
routerFiles: string[],
|
|
105
|
+
): { ancestor: string; nested: string } | null {
|
|
106
|
+
const routerDirs = [
|
|
107
|
+
...new Set(routerFiles.map((filePath) => dirname(resolve(filePath)))),
|
|
108
|
+
].sort((a, b) => a.length - b.length);
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < routerDirs.length; i++) {
|
|
111
|
+
const ancestorDir = routerDirs[i];
|
|
112
|
+
const prefix = ancestorDir.endsWith(sep)
|
|
113
|
+
? ancestorDir
|
|
114
|
+
: `${ancestorDir}${sep}`;
|
|
115
|
+
for (let j = i + 1; j < routerDirs.length; j++) {
|
|
116
|
+
const nestedDir = routerDirs[j];
|
|
117
|
+
if (!nestedDir.startsWith(prefix)) continue;
|
|
118
|
+
const ancestorFile = routerFiles.find(
|
|
119
|
+
(filePath) => dirname(resolve(filePath)) === ancestorDir,
|
|
120
|
+
);
|
|
121
|
+
const nestedFile = routerFiles.find(
|
|
122
|
+
(filePath) => dirname(resolve(filePath)) === nestedDir,
|
|
123
|
+
);
|
|
124
|
+
if (ancestorFile && nestedFile) {
|
|
125
|
+
return { ancestor: ancestorFile, nested: nestedFile };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function formatNestedRouterConflictError(
|
|
134
|
+
conflict: { ancestor: string; nested: string },
|
|
135
|
+
prefix = "[rsc-router]",
|
|
136
|
+
): string {
|
|
137
|
+
return (
|
|
138
|
+
`${prefix} Nested router roots are not supported.\n` +
|
|
139
|
+
`Router root: ${conflict.ancestor}\n` +
|
|
140
|
+
`Nested router: ${conflict.nested}\n` +
|
|
141
|
+
`Move the nested router into a sibling directory or configure it as a separate app root.`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
29
145
|
// ---------------------------------------------------------------------------
|
|
30
146
|
// Router file URL extraction
|
|
31
147
|
// ---------------------------------------------------------------------------
|
|
@@ -235,19 +351,8 @@ export function detectUnresolvableIncludesForUrlsFile(
|
|
|
235
351
|
* Call once at startup; the result can be reused on subsequent watcher triggers.
|
|
236
352
|
*/
|
|
237
353
|
export function findRouterFiles(root: string, filter?: ScanFilter): string[] {
|
|
238
|
-
const files = findTsFiles(root, filter);
|
|
239
354
|
const result: string[] = [];
|
|
240
|
-
|
|
241
|
-
if (filePath.includes(".gen.")) continue;
|
|
242
|
-
try {
|
|
243
|
-
const source = readFileSync(filePath, "utf-8");
|
|
244
|
-
if (/\bcreateRouter\s*[<(]/.test(source)) {
|
|
245
|
-
result.push(filePath);
|
|
246
|
-
}
|
|
247
|
-
} catch {
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
355
|
+
findRouterFilesRecursive(root, filter, result);
|
|
251
356
|
return result;
|
|
252
357
|
}
|
|
253
358
|
|
|
@@ -276,6 +381,11 @@ export function writeCombinedRouteTypes(
|
|
|
276
381
|
const routerFilePaths = knownRouterFiles ?? findRouterFiles(root);
|
|
277
382
|
if (routerFilePaths.length === 0) return;
|
|
278
383
|
|
|
384
|
+
const nestedRouterConflict = findNestedRouterConflict(routerFilePaths);
|
|
385
|
+
if (nestedRouterConflict) {
|
|
386
|
+
throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
|
|
387
|
+
}
|
|
388
|
+
|
|
279
389
|
for (const routerFilePath of routerFilePaths) {
|
|
280
390
|
let routerSource: string;
|
|
281
391
|
try {
|
package/src/client.rsc.tsx
CHANGED
|
@@ -17,7 +17,6 @@ export {
|
|
|
17
17
|
OutletProvider,
|
|
18
18
|
useOutlet,
|
|
19
19
|
useLoader,
|
|
20
|
-
useLoaderData,
|
|
21
20
|
ErrorBoundary,
|
|
22
21
|
type ErrorBoundaryProps,
|
|
23
22
|
} from "./client.js";
|
|
@@ -64,6 +63,8 @@ export { Meta } from "./handles/meta.js";
|
|
|
64
63
|
// MetaTags is a "use client" component that can be imported from RSC
|
|
65
64
|
export { MetaTags } from "./handles/MetaTags.js";
|
|
66
65
|
export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
|
|
66
|
+
// Breadcrumbs handle works in RSC context
|
|
67
|
+
export { Breadcrumbs, type BreadcrumbItem } from "./handles/breadcrumbs.js";
|
|
67
68
|
|
|
68
69
|
// Location state - createLocationState works in RSC (just creates definition)
|
|
69
70
|
// useLocationState is NOT exported here as it uses client hooks
|
package/src/client.tsx
CHANGED
|
@@ -313,52 +313,6 @@ export {
|
|
|
313
313
|
type UseLoaderOptions,
|
|
314
314
|
} from "./use-loader.js";
|
|
315
315
|
|
|
316
|
-
/**
|
|
317
|
-
* Hook to access all loader data in the current context
|
|
318
|
-
*
|
|
319
|
-
* Returns a record of all loader data available in the current outlet context
|
|
320
|
-
* and all parent contexts. Useful for debugging or when you need access to
|
|
321
|
-
* multiple loaders.
|
|
322
|
-
*
|
|
323
|
-
* @returns Record of loader name to data, or empty object if no loaders
|
|
324
|
-
*
|
|
325
|
-
* @example
|
|
326
|
-
* ```tsx
|
|
327
|
-
* "use client";
|
|
328
|
-
* import { useLoaderData } from "rsc-router/client";
|
|
329
|
-
*
|
|
330
|
-
* export function DebugPanel() {
|
|
331
|
-
* const loaderData = useLoaderData();
|
|
332
|
-
* return <pre>{JSON.stringify(loaderData, null, 2)}</pre>;
|
|
333
|
-
* }
|
|
334
|
-
* ```
|
|
335
|
-
*/
|
|
336
|
-
export function useLoaderData(): Record<string, any> {
|
|
337
|
-
const context = useContext(OutletContext);
|
|
338
|
-
|
|
339
|
-
// Collect all loader data from the context chain
|
|
340
|
-
// Child loaders override parent loaders with the same name
|
|
341
|
-
const result: Record<string, any> = {};
|
|
342
|
-
const stack: OutletContextValue[] = [];
|
|
343
|
-
|
|
344
|
-
// Build stack from current to root
|
|
345
|
-
let current: OutletContextValue | null | undefined = context;
|
|
346
|
-
while (current) {
|
|
347
|
-
stack.push(current);
|
|
348
|
-
current = current.parent;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Apply from root to current (so children override parents)
|
|
352
|
-
for (let i = stack.length - 1; i >= 0; i--) {
|
|
353
|
-
const ctx = stack[i];
|
|
354
|
-
if (ctx.loaderData) {
|
|
355
|
-
Object.assign(result, ctx.loaderData);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return result;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
316
|
/**
|
|
363
317
|
* Client-safe createLoader factory
|
|
364
318
|
*
|
|
@@ -590,6 +544,7 @@ export { useHandle } from "./browser/react/use-handle.js";
|
|
|
590
544
|
export { Meta } from "./handles/meta.js";
|
|
591
545
|
export { MetaTags } from "./handles/MetaTags.js";
|
|
592
546
|
export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
|
|
547
|
+
export { Breadcrumbs, type BreadcrumbItem } from "./handles/breadcrumbs.js";
|
|
593
548
|
|
|
594
549
|
// Location state - type-safe navigation state
|
|
595
550
|
export {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in Breadcrumbs handle for accumulating breadcrumb items across route segments.
|
|
3
|
+
*
|
|
4
|
+
* Each layout/route pushes breadcrumb items via `ctx.use(Breadcrumbs)`.
|
|
5
|
+
* Items are collected in parent-to-child order with automatic deduplication
|
|
6
|
+
* by `href` (last item for each href wins).
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* // In route handler
|
|
11
|
+
* route("/blog/:slug", (ctx) => {
|
|
12
|
+
* const breadcrumb = ctx.use(Breadcrumbs);
|
|
13
|
+
* breadcrumb({ label: "Blog", href: "/blog" });
|
|
14
|
+
* breadcrumb({ label: post.title, href: `/blog/${ctx.params.slug}` });
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* // In client component (consume with useHandle)
|
|
18
|
+
* const crumbs = useHandle(Breadcrumbs);
|
|
19
|
+
* crumbs.map((c) => <a href={c.href}>{c.label}</a>);
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { ReactNode } from "react";
|
|
24
|
+
import { createHandle, type Handle } from "../handle.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A single breadcrumb item.
|
|
28
|
+
*
|
|
29
|
+
* @property label - Display text for the breadcrumb
|
|
30
|
+
* @property href - URL the breadcrumb links to
|
|
31
|
+
* @property content - Optional extra content (sync or async) rendered alongside the label
|
|
32
|
+
*/
|
|
33
|
+
export interface BreadcrumbItem {
|
|
34
|
+
label: string;
|
|
35
|
+
href: string;
|
|
36
|
+
content?: ReactNode | Promise<ReactNode>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Collect function for Breadcrumbs handle.
|
|
41
|
+
* Flattens segments in parent-to-child order with deduplication by href
|
|
42
|
+
* (last item for each href wins).
|
|
43
|
+
*/
|
|
44
|
+
function collectBreadcrumbs(segments: BreadcrumbItem[][]): BreadcrumbItem[] {
|
|
45
|
+
const all = segments.flat();
|
|
46
|
+
const seen = new Map<string, number>();
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < all.length; i++) {
|
|
49
|
+
seen.set(all[i].href, i);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Return items in order, keeping only the last occurrence per href
|
|
53
|
+
return all.filter((item, index) => seen.get(item.href) === index);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Built-in handle for accumulating breadcrumb navigation items.
|
|
58
|
+
*
|
|
59
|
+
* Use `ctx.use(Breadcrumbs)` in route handlers to push breadcrumb items.
|
|
60
|
+
* Use `useHandle(Breadcrumbs)` in client components to consume them.
|
|
61
|
+
*/
|
|
62
|
+
export const Breadcrumbs: Handle<BreadcrumbItem, BreadcrumbItem[]> =
|
|
63
|
+
createHandle<BreadcrumbItem, BreadcrumbItem[]>(
|
|
64
|
+
collectBreadcrumbs,
|
|
65
|
+
"__rsc_router_breadcrumbs__",
|
|
66
|
+
);
|
package/src/handles/index.ts
CHANGED
package/src/host/index.ts
CHANGED
|
@@ -25,9 +25,6 @@
|
|
|
25
25
|
// Core router
|
|
26
26
|
export { createHostRouter } from "./router.js";
|
|
27
27
|
|
|
28
|
-
// Host router registry for build-time discovery
|
|
29
|
-
export { HostRouterRegistry, type HostRouterRegistryEntry } from "./router.js";
|
|
30
|
-
|
|
31
28
|
// Utilities
|
|
32
29
|
export { defineHosts } from "./utils.js";
|
|
33
30
|
|
package/src/index.rsc.ts
CHANGED
|
@@ -11,8 +11,6 @@
|
|
|
11
11
|
|
|
12
12
|
// Re-export all universal exports from index.ts
|
|
13
13
|
export {
|
|
14
|
-
// Universal rendering utilities
|
|
15
|
-
renderSegments,
|
|
16
14
|
// Error classes
|
|
17
15
|
RouteNotFoundError,
|
|
18
16
|
DataNotFoundError,
|
|
@@ -21,9 +19,6 @@ export {
|
|
|
21
19
|
HandlerError,
|
|
22
20
|
BuildError,
|
|
23
21
|
InvalidHandlerError,
|
|
24
|
-
NetworkError,
|
|
25
|
-
isNetworkError,
|
|
26
|
-
sanitizeError,
|
|
27
22
|
RouterError,
|
|
28
23
|
Skip,
|
|
29
24
|
isSkip,
|
|
@@ -40,7 +35,6 @@ export type {
|
|
|
40
35
|
TrailingSlashMode,
|
|
41
36
|
// Handler types
|
|
42
37
|
Handler,
|
|
43
|
-
ScopedRouteMap,
|
|
44
38
|
HandlerContext,
|
|
45
39
|
ExtractParams,
|
|
46
40
|
GenericParams,
|
|
@@ -120,7 +114,6 @@ export { nonce } from "./rsc/nonce.js";
|
|
|
120
114
|
// Pre-render handler API
|
|
121
115
|
export {
|
|
122
116
|
Prerender,
|
|
123
|
-
isPrerenderHandler,
|
|
124
117
|
type PrerenderHandlerDefinition,
|
|
125
118
|
type PrerenderPassthroughContext,
|
|
126
119
|
type PrerenderOptions,
|
|
@@ -130,16 +123,11 @@ export {
|
|
|
130
123
|
} from "./prerender.js";
|
|
131
124
|
|
|
132
125
|
// Static handler API
|
|
133
|
-
export {
|
|
134
|
-
Static,
|
|
135
|
-
isStaticHandler,
|
|
136
|
-
type StaticHandlerDefinition,
|
|
137
|
-
} from "./static-handler.js";
|
|
126
|
+
export { Static, type StaticHandlerDefinition } from "./static-handler.js";
|
|
138
127
|
|
|
139
128
|
// Django-style URL patterns (RSC/server context)
|
|
140
129
|
export {
|
|
141
130
|
urls,
|
|
142
|
-
RESPONSE_TYPE,
|
|
143
131
|
type PathHelpers,
|
|
144
132
|
type PathOptions,
|
|
145
133
|
type UrlPatterns,
|
|
@@ -171,6 +159,7 @@ export type { HandlerCacheConfig } from "./rsc/types.js";
|
|
|
171
159
|
|
|
172
160
|
// Built-in handles (server-side)
|
|
173
161
|
export { Meta } from "./handles/meta.js";
|
|
162
|
+
export { Breadcrumbs, type BreadcrumbItem } from "./handles/breadcrumbs.js";
|
|
174
163
|
|
|
175
164
|
// Request context (for accessing request data in server actions/components).
|
|
176
165
|
// Re-exported with a narrowed return type so that public consumers only see
|
|
@@ -179,9 +168,10 @@ export { Meta } from "./handles/meta.js";
|
|
|
179
168
|
import { getRequestContext as _getRequestContextInternal } from "./server/request-context.js";
|
|
180
169
|
export type { PublicRequestContext as RequestContext } from "./server/request-context.js";
|
|
181
170
|
import type { PublicRequestContext } from "./server/request-context.js";
|
|
171
|
+
import type { DefaultEnv } from "./types/global-namespace.js";
|
|
182
172
|
|
|
183
173
|
export const getRequestContext: <
|
|
184
|
-
TEnv =
|
|
174
|
+
TEnv = DefaultEnv,
|
|
185
175
|
>() => PublicRequestContext<TEnv> = _getRequestContextInternal;
|
|
186
176
|
|
|
187
177
|
// Request-scoped shorthands
|
|
@@ -205,8 +195,6 @@ export type {
|
|
|
205
195
|
ReverseFunction,
|
|
206
196
|
ExtractLocalRoutes,
|
|
207
197
|
ParamsFor,
|
|
208
|
-
SanitizePrefix,
|
|
209
|
-
MergeRoutes,
|
|
210
198
|
} from "./reverse.js";
|
|
211
199
|
export { scopedReverse, createReverse } from "./reverse.js";
|
|
212
200
|
|
|
@@ -219,12 +207,6 @@ export type {
|
|
|
219
207
|
RouteParams,
|
|
220
208
|
} from "./search-params.js";
|
|
221
209
|
|
|
222
|
-
// Debug utilities for route matching (development only)
|
|
223
|
-
export {
|
|
224
|
-
enableMatchDebug,
|
|
225
|
-
getMatchDebugStats,
|
|
226
|
-
} from "./router/pattern-matching.js";
|
|
227
|
-
|
|
228
210
|
// Location state (universal)
|
|
229
211
|
export {
|
|
230
212
|
createLocationState,
|
|
@@ -240,20 +222,7 @@ export type { PathResponse } from "./href-client.js";
|
|
|
240
222
|
export { createConsoleSink } from "./router/telemetry.js";
|
|
241
223
|
export { createOTelSink } from "./router/telemetry-otel.js";
|
|
242
224
|
export type { OTelTracer, OTelSpan } from "./router/telemetry-otel.js";
|
|
243
|
-
export type {
|
|
244
|
-
TelemetrySink,
|
|
245
|
-
TelemetryEvent,
|
|
246
|
-
RequestStartEvent,
|
|
247
|
-
RequestEndEvent,
|
|
248
|
-
RequestErrorEvent,
|
|
249
|
-
RequestTimeoutEvent,
|
|
250
|
-
LoaderStartEvent,
|
|
251
|
-
LoaderEndEvent,
|
|
252
|
-
LoaderErrorEvent,
|
|
253
|
-
HandlerErrorEvent,
|
|
254
|
-
CacheDecisionEvent,
|
|
255
|
-
RevalidationDecisionEvent,
|
|
256
|
-
} from "./router/telemetry.js";
|
|
225
|
+
export type { TelemetrySink, TelemetryEvent } from "./router/telemetry.js";
|
|
257
226
|
|
|
258
227
|
// Timeout types and error class
|
|
259
228
|
export { RouterTimeoutError } from "./router/timeout.js";
|