@ivogt/rsc-router 0.0.0-experimental.1
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 +19 -0
- package/package.json +131 -0
- package/src/__mocks__/version.ts +6 -0
- package/src/__tests__/route-definition.test.ts +63 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +891 -0
- package/src/browser/navigation-client.ts +155 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +545 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +228 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +178 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-navigation.ts +150 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +149 -0
- package/src/browser/rsc-router.tsx +310 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/server-action-bridge.ts +747 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +443 -0
- package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
- package/src/cache/__tests__/memory-store.test.ts +484 -0
- package/src/cache/cache-scope.ts +565 -0
- package/src/cache/cf/__tests__/cf-cache-store.test.ts +361 -0
- package/src/cache/cf/cf-cache-store.ts +274 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/index.ts +52 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +366 -0
- package/src/client.rsc.tsx +88 -0
- package/src/client.tsx +609 -0
- package/src/components/DefaultDocument.tsx +20 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +259 -0
- package/src/handle.ts +120 -0
- package/src/handles/MetaTags.tsx +178 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/href-client.ts +128 -0
- package/src/href.ts +139 -0
- package/src/index.rsc.ts +69 -0
- package/src/index.ts +84 -0
- package/src/loader.rsc.ts +204 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +198 -0
- package/src/route-definition.ts +1333 -0
- package/src/route-map-builder.ts +140 -0
- package/src/route-types.ts +148 -0
- package/src/route-utils.ts +89 -0
- package/src/router/__tests__/match-context.test.ts +104 -0
- package/src/router/__tests__/match-pipelines.test.ts +537 -0
- package/src/router/__tests__/match-result.test.ts +566 -0
- package/src/router/__tests__/on-error.test.ts +935 -0
- package/src/router/__tests__/pattern-matching.test.ts +577 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +60 -0
- package/src/router/loader-resolution.ts +326 -0
- package/src/router/manifest.ts +116 -0
- package/src/router/match-context.ts +261 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +261 -0
- package/src/router/match-middleware/cache-store.ts +250 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +268 -0
- package/src/router/match-middleware/segment-resolution.ts +174 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +212 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.test.ts +1355 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +271 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +299 -0
- package/src/router/types.ts +96 -0
- package/src/router.ts +3484 -0
- package/src/rsc/__tests__/helpers.test.ts +175 -0
- package/src/rsc/handler.ts +942 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +225 -0
- package/src/segment-system.tsx +405 -0
- package/src/server/__tests__/request-context.test.ts +171 -0
- package/src/server/context.ts +340 -0
- package/src/server/handle-store.ts +230 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +470 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +126 -0
- package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
- package/src/ssr/index.tsx +215 -0
- package/src/types.ts +1473 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +357 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/index.ts +608 -0
- package/src/vite/version.d.ts +12 -0
- package/src/vite/virtual-entries.ts +109 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in Meta handle for managing document metadata across route segments.
|
|
3
|
+
*
|
|
4
|
+
* Provides automatic deduplication: later routes override earlier ones
|
|
5
|
+
* for the same meta key (title, name, property, etc.)
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // In route handler
|
|
10
|
+
* route("product/:id", (ctx) => {
|
|
11
|
+
* const meta = ctx.use(Meta);
|
|
12
|
+
* meta({ title: "Product Details" });
|
|
13
|
+
* meta({ name: "description", content: "..." });
|
|
14
|
+
* meta({ property: "og:title", content: "..." });
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* // In layout (renders the collected meta tags)
|
|
18
|
+
* function RootLayout() {
|
|
19
|
+
* return (
|
|
20
|
+
* <html>
|
|
21
|
+
* <head>
|
|
22
|
+
* <MetaTags />
|
|
23
|
+
* </head>
|
|
24
|
+
* ...
|
|
25
|
+
* </html>
|
|
26
|
+
* );
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { createHandle, type Handle } from "../handle.ts";
|
|
32
|
+
import type {
|
|
33
|
+
MetaDescriptor,
|
|
34
|
+
TitleDescriptor,
|
|
35
|
+
UnsetDescriptor,
|
|
36
|
+
} from "../router/types.ts";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Type guard for unset descriptor
|
|
40
|
+
*/
|
|
41
|
+
function isUnsetDescriptor(
|
|
42
|
+
descriptor: MetaDescriptor
|
|
43
|
+
): descriptor is UnsetDescriptor {
|
|
44
|
+
return (
|
|
45
|
+
typeof descriptor === "object" &&
|
|
46
|
+
descriptor !== null &&
|
|
47
|
+
"unset" in descriptor &&
|
|
48
|
+
typeof (descriptor as UnsetDescriptor).unset === "string"
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Type guard for title descriptor (any form)
|
|
54
|
+
*/
|
|
55
|
+
function isTitleDescriptor(
|
|
56
|
+
descriptor: MetaDescriptor
|
|
57
|
+
): descriptor is { title: TitleDescriptor } {
|
|
58
|
+
return (
|
|
59
|
+
typeof descriptor === "object" &&
|
|
60
|
+
descriptor !== null &&
|
|
61
|
+
"title" in descriptor
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Type guard for title template descriptor
|
|
67
|
+
*/
|
|
68
|
+
function isTitleTemplate(
|
|
69
|
+
title: TitleDescriptor
|
|
70
|
+
): title is { template: string; default: string } {
|
|
71
|
+
return (
|
|
72
|
+
typeof title === "object" &&
|
|
73
|
+
title !== null &&
|
|
74
|
+
"template" in title &&
|
|
75
|
+
"default" in title
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Type guard for absolute title descriptor
|
|
81
|
+
*/
|
|
82
|
+
function isAbsoluteTitle(title: TitleDescriptor): title is { absolute: string } {
|
|
83
|
+
return typeof title === "object" && title !== null && "absolute" in title;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get a unique key for a meta descriptor for deduplication.
|
|
88
|
+
* Returns undefined for descriptors that shouldn't be deduplicated.
|
|
89
|
+
*/
|
|
90
|
+
function getMetaKey(descriptor: MetaDescriptor): string | undefined {
|
|
91
|
+
// Skip unset descriptors - they are processed separately
|
|
92
|
+
if (isUnsetDescriptor(descriptor)) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
if ("charSet" in descriptor) {
|
|
96
|
+
return "charSet";
|
|
97
|
+
}
|
|
98
|
+
if ("title" in descriptor) {
|
|
99
|
+
return "title";
|
|
100
|
+
}
|
|
101
|
+
if ("name" in descriptor && "content" in descriptor) {
|
|
102
|
+
return `name:${descriptor.name}`;
|
|
103
|
+
}
|
|
104
|
+
if ("property" in descriptor && "content" in descriptor) {
|
|
105
|
+
return `property:${descriptor.property}`;
|
|
106
|
+
}
|
|
107
|
+
if ("httpEquiv" in descriptor && "content" in descriptor) {
|
|
108
|
+
return `httpEquiv:${descriptor.httpEquiv}`;
|
|
109
|
+
}
|
|
110
|
+
if ("script:ld+json" in descriptor) {
|
|
111
|
+
// JSON-LD scripts can have multiple, don't dedupe by default
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
if ("tagName" in descriptor) {
|
|
115
|
+
// For link tags, dedupe by rel if present
|
|
116
|
+
if (descriptor.tagName === "link" && "rel" in descriptor) {
|
|
117
|
+
// Some link rels should be unique (canonical), others not (stylesheet)
|
|
118
|
+
const uniqueRels = ["canonical", "icon", "apple-touch-icon"];
|
|
119
|
+
if (uniqueRels.includes(descriptor.rel as string)) {
|
|
120
|
+
return `link:${descriptor.rel}`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Default meta descriptors included automatically.
|
|
130
|
+
* These can be overridden by route handlers.
|
|
131
|
+
*/
|
|
132
|
+
const defaultMetaDescriptors: MetaDescriptor[] = [
|
|
133
|
+
{ charSet: "utf-8" },
|
|
134
|
+
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Helper to add or replace a descriptor in the result array
|
|
139
|
+
*/
|
|
140
|
+
function addOrReplace(
|
|
141
|
+
result: MetaDescriptor[],
|
|
142
|
+
keyToIndex: Map<string, number>,
|
|
143
|
+
descriptor: MetaDescriptor,
|
|
144
|
+
key: string | undefined
|
|
145
|
+
): void {
|
|
146
|
+
if (key !== undefined && keyToIndex.has(key)) {
|
|
147
|
+
result[keyToIndex.get(key)!] = descriptor;
|
|
148
|
+
} else {
|
|
149
|
+
if (key !== undefined) {
|
|
150
|
+
keyToIndex.set(key, result.length);
|
|
151
|
+
}
|
|
152
|
+
result.push(descriptor);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Helper to update indices after removing an element
|
|
158
|
+
*/
|
|
159
|
+
function updateIndicesAfterRemoval(
|
|
160
|
+
keyToIndex: Map<string, number>,
|
|
161
|
+
removedIndex: number
|
|
162
|
+
): void {
|
|
163
|
+
for (const [key, index] of keyToIndex) {
|
|
164
|
+
if (index > removedIndex) {
|
|
165
|
+
keyToIndex.set(key, index - 1);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Collect function for Meta handle.
|
|
172
|
+
* Includes default meta descriptors, then deduplicates by key with later routes overriding earlier ones.
|
|
173
|
+
* Supports title templates, absolute titles, and unset descriptors.
|
|
174
|
+
*/
|
|
175
|
+
function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
|
|
176
|
+
const result: MetaDescriptor[] = [];
|
|
177
|
+
const keyToIndex = new Map<string, number>();
|
|
178
|
+
let titleTemplate: string | undefined;
|
|
179
|
+
|
|
180
|
+
// Add defaults first so they can be overridden
|
|
181
|
+
for (const descriptor of defaultMetaDescriptors) {
|
|
182
|
+
const key = getMetaKey(descriptor);
|
|
183
|
+
if (key !== undefined) {
|
|
184
|
+
keyToIndex.set(key, result.length);
|
|
185
|
+
}
|
|
186
|
+
result.push(descriptor);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const descriptors of segments) {
|
|
190
|
+
for (const descriptor of descriptors) {
|
|
191
|
+
// Handle unset descriptors
|
|
192
|
+
if (isUnsetDescriptor(descriptor)) {
|
|
193
|
+
const keyToRemove = descriptor.unset;
|
|
194
|
+
if (keyToIndex.has(keyToRemove)) {
|
|
195
|
+
const idx = keyToIndex.get(keyToRemove)!;
|
|
196
|
+
result.splice(idx, 1);
|
|
197
|
+
keyToIndex.delete(keyToRemove);
|
|
198
|
+
updateIndicesAfterRemoval(keyToIndex, idx);
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Handle title descriptors with template/absolute support
|
|
204
|
+
if (isTitleDescriptor(descriptor)) {
|
|
205
|
+
const titleValue = descriptor.title;
|
|
206
|
+
|
|
207
|
+
if (isTitleTemplate(titleValue)) {
|
|
208
|
+
// Store template for subsequent title descriptors in child segments
|
|
209
|
+
titleTemplate = titleValue.template;
|
|
210
|
+
// Set the default title
|
|
211
|
+
addOrReplace(result, keyToIndex, { title: titleValue.default }, "title");
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (isAbsoluteTitle(titleValue)) {
|
|
216
|
+
// Absolute title bypasses any template
|
|
217
|
+
addOrReplace(result, keyToIndex, { title: titleValue.absolute }, "title");
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// String title - apply template if one exists
|
|
222
|
+
const finalTitle = titleTemplate
|
|
223
|
+
? titleTemplate.replace("%s", titleValue as string)
|
|
224
|
+
: titleValue;
|
|
225
|
+
addOrReplace(result, keyToIndex, { title: finalTitle as string }, "title");
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Handle all other descriptors
|
|
230
|
+
const key = getMetaKey(descriptor);
|
|
231
|
+
addOrReplace(result, keyToIndex, descriptor, key);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Built-in handle for managing document metadata.
|
|
240
|
+
*
|
|
241
|
+
* Use `ctx.use(Meta)` in route handlers to push meta descriptors.
|
|
242
|
+
* Use `<MetaTags />` component to render them in the document head.
|
|
243
|
+
*/
|
|
244
|
+
export const Meta: Handle<MetaDescriptor, MetaDescriptor[]> = createHandle<MetaDescriptor, MetaDescriptor[]>(
|
|
245
|
+
collectMeta,
|
|
246
|
+
"__rsc_router_meta__"
|
|
247
|
+
);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-safe type-safe href function
|
|
3
|
+
*
|
|
4
|
+
* This is a compile-time only href that validates paths against registered routes.
|
|
5
|
+
* No runtime route map lookup - just an identity function with TypeScript validation.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { href } from "rsc-router/client";
|
|
10
|
+
*
|
|
11
|
+
* href("/blog/my-post"); // ✓ matches /blog/:slug
|
|
12
|
+
* href("/shop/product/widget"); // ✓ matches /shop/product/:slug
|
|
13
|
+
* href("/invalid"); // ✗ TypeScript error
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { GetRegisteredRoutes } from "./types.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse constraint values into a union type for paths
|
|
21
|
+
* "a|b|c" → "a" | "b" | "c"
|
|
22
|
+
*/
|
|
23
|
+
type ParseConstraintPath<T extends string> =
|
|
24
|
+
T extends `${infer First}|${infer Rest}`
|
|
25
|
+
? First | ParseConstraintPath<Rest>
|
|
26
|
+
: T;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Convert a route pattern to a template literal type
|
|
30
|
+
*
|
|
31
|
+
* Supports:
|
|
32
|
+
* - Static: /about → "/about"
|
|
33
|
+
* - Dynamic: /blog/:slug → `/blog/${string}`
|
|
34
|
+
* - Optional: /:locale?/blog → "/blog" | `/${string}/blog`
|
|
35
|
+
* - Constrained: /:locale(en|gb)/blog → "/en/blog" | "/gb/blog"
|
|
36
|
+
* - Optional + Constrained: /:locale(en|gb)?/blog → "/blog" | "/en/blog" | "/gb/blog"
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* PatternToPath<"/blog/:slug"> = `/blog/${string}`
|
|
40
|
+
* PatternToPath<"/:locale?/blog"> = "/blog" | `/${string}/blog`
|
|
41
|
+
* PatternToPath<"/:locale(en|gb)/blog"> = "/en/blog" | "/gb/blog"
|
|
42
|
+
* PatternToPath<"/:locale(en|gb)?/blog"> = "/blog" | "/en/blog" | "/gb/blog"
|
|
43
|
+
*/
|
|
44
|
+
export type PatternToPath<T extends string> =
|
|
45
|
+
// Optional + constrained param in middle: /:param(a|b)?/rest
|
|
46
|
+
T extends `${infer Before}:${infer _Name}(${infer Constraint})?/${infer After}`
|
|
47
|
+
? PatternToPath<`${Before}${After}`> | `${Before}${ParseConstraintPath<Constraint>}/${PatternToPath<After>}`
|
|
48
|
+
// Optional + constrained param at end: /path/:param(a|b)?
|
|
49
|
+
: T extends `${infer Before}:${infer _Name}(${infer Constraint})?`
|
|
50
|
+
? Before | `${Before}${ParseConstraintPath<Constraint>}`
|
|
51
|
+
// Constrained param in middle: /:param(a|b)/rest
|
|
52
|
+
: T extends `${infer Before}:${infer _Name}(${infer Constraint})/${infer After}`
|
|
53
|
+
? `${Before}${ParseConstraintPath<Constraint>}/${PatternToPath<After>}`
|
|
54
|
+
// Constrained param at end: /path/:param(a|b)
|
|
55
|
+
: T extends `${infer Before}:${infer _Name}(${infer Constraint})`
|
|
56
|
+
? `${Before}${ParseConstraintPath<Constraint>}`
|
|
57
|
+
// Optional param in middle: /:param?/rest
|
|
58
|
+
: T extends `${infer Before}:${infer _Param}?/${infer After}`
|
|
59
|
+
? PatternToPath<`${Before}${After}`> | `${Before}${string}/${PatternToPath<After>}`
|
|
60
|
+
// Optional param at end: /path/:param?
|
|
61
|
+
: T extends `${infer Before}:${infer _Param}?`
|
|
62
|
+
? Before | `${Before}${string}`
|
|
63
|
+
// Required param in middle: /:param/rest
|
|
64
|
+
: T extends `${infer Before}:${infer _Param}/${infer After}`
|
|
65
|
+
? `${Before}${string}/${PatternToPath<After>}`
|
|
66
|
+
// Required param at end: /path/:param
|
|
67
|
+
: T extends `${infer Before}:${infer _Param}`
|
|
68
|
+
? `${Before}${string}`
|
|
69
|
+
// Static path
|
|
70
|
+
: T;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Allow optional query string (?...) and/or hash fragment (#...) suffix
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* WithSuffix<"/about"> = "/about" | "/about?..." | "/about#..." | "/about?...#..."
|
|
77
|
+
*/
|
|
78
|
+
type WithSuffix<T extends string> =
|
|
79
|
+
| T
|
|
80
|
+
| `${T}?${string}`
|
|
81
|
+
| `${T}#${string}`
|
|
82
|
+
| `${T}?${string}#${string}`;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Union of all valid paths from registered routes
|
|
86
|
+
*
|
|
87
|
+
* Generated from RSCRouter.RegisteredRoutes via module augmentation.
|
|
88
|
+
* Allows optional query strings and hash fragments.
|
|
89
|
+
*/
|
|
90
|
+
export type ValidPaths<TRoutes extends Record<string, string> = GetRegisteredRoutes> =
|
|
91
|
+
WithSuffix<PatternToPath<TRoutes[keyof TRoutes]>>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Type-safe href function for client-side use
|
|
95
|
+
*
|
|
96
|
+
* This is an identity function - it returns the path unchanged.
|
|
97
|
+
* The value is in TypeScript validation: invalid paths cause compile errors.
|
|
98
|
+
*
|
|
99
|
+
* Works with:
|
|
100
|
+
* - Static paths: href("/about")
|
|
101
|
+
* - Dynamic segments: href("/blog/my-post")
|
|
102
|
+
* - Multiple segments: href("/shop/product/widget/reviews/123")
|
|
103
|
+
*
|
|
104
|
+
* Does NOT validate:
|
|
105
|
+
* - Query strings (passed through as-is)
|
|
106
|
+
* - Hash fragments (passed through as-is)
|
|
107
|
+
*
|
|
108
|
+
* @param path - A valid path matching one of the registered route patterns
|
|
109
|
+
* @returns The path unchanged
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* // Valid paths (compile)
|
|
114
|
+
* href("/blog/hello"); // matches /blog/:slug
|
|
115
|
+
* href("/shop/product/widget"); // matches /shop/product/:slug
|
|
116
|
+
* href("/shop/product/widget/reviews"); // matches /shop/product/:slug/reviews
|
|
117
|
+
*
|
|
118
|
+
* // Query strings and hashes pass through (not validated)
|
|
119
|
+
* href("/blog/hello?page=1");
|
|
120
|
+
* href("/about#contact");
|
|
121
|
+
*
|
|
122
|
+
* // Invalid paths (TypeScript error)
|
|
123
|
+
* href("/nonexistent"); // Error: not assignable to ValidPaths
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export function href<T extends ValidPaths>(path: T): T {
|
|
127
|
+
return path;
|
|
128
|
+
}
|
package/src/href.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { ExtractParams } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sanitize prefix string by removing leading slash
|
|
5
|
+
* "/shop" -> "shop", "blog" -> "blog", "" -> ""
|
|
6
|
+
*/
|
|
7
|
+
export type SanitizePrefix<T extends string> = T extends `/${infer P}` ? P : T;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Helper type to merge multiple route definitions into a single accumulated type.
|
|
11
|
+
* Use this to define your app's complete route map for type-safe router.href().
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { homeRoutes, blogRoutes, shopRoutes } from "./routes";
|
|
16
|
+
*
|
|
17
|
+
* type AppRoutes = MergeRoutes<[
|
|
18
|
+
* typeof homeRoutes,
|
|
19
|
+
* PrefixedRoutes<typeof blogRoutes, "blog">,
|
|
20
|
+
* PrefixedRoutes<typeof shopRoutes, "shop">,
|
|
21
|
+
* ]>;
|
|
22
|
+
*
|
|
23
|
+
* export const router = createRSCRouter<AppEnv>() as RSCRouter<AppEnv, AppRoutes>;
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export type MergeRoutes<T extends Record<string, string>[]> = T extends [
|
|
27
|
+
infer First extends Record<string, string>,
|
|
28
|
+
...infer Rest extends Record<string, string>[]
|
|
29
|
+
]
|
|
30
|
+
? First & MergeRoutes<Rest>
|
|
31
|
+
: {};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Add key prefix to all entries in a route map
|
|
35
|
+
* { "cart": "/cart" } with prefix "shop" -> { "shop.cart": "/shop/cart" }
|
|
36
|
+
*/
|
|
37
|
+
export type PrefixRouteKeys<
|
|
38
|
+
T extends Record<string, string>,
|
|
39
|
+
Prefix extends string
|
|
40
|
+
> = Prefix extends ""
|
|
41
|
+
? T
|
|
42
|
+
: { [K in keyof T as `${Prefix}.${K & string}`]: T[K] };
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Add path prefix to all patterns in a route map
|
|
46
|
+
* { "cart": "/cart" } with prefix "/shop" -> { "cart": "/shop/cart" }
|
|
47
|
+
*/
|
|
48
|
+
export type PrefixRoutePatterns<
|
|
49
|
+
T extends Record<string, string>,
|
|
50
|
+
PathPrefix extends string
|
|
51
|
+
> = {
|
|
52
|
+
[K in keyof T]: PathPrefix extends "" | "/"
|
|
53
|
+
? T[K]
|
|
54
|
+
: T[K] extends "/"
|
|
55
|
+
? PathPrefix
|
|
56
|
+
: `${PathPrefix}${T[K]}`;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Combined: prefix both keys and patterns
|
|
61
|
+
* Used for module augmentation registration
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* // Given shopRoutes = { "index": "/", "cart": "/cart", "products.detail": "/product/:slug" }
|
|
66
|
+
* // PrefixedRoutes<typeof shopRoutes, "shop"> produces:
|
|
67
|
+
* // { "shop.index": "/shop", "shop.cart": "/shop/cart", "shop.products.detail": "/shop/product/:slug" }
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export type PrefixedRoutes<
|
|
71
|
+
T extends Record<string, string>,
|
|
72
|
+
KeyPrefix extends string,
|
|
73
|
+
PathPrefix extends string = KeyPrefix extends "" ? "" : `/${KeyPrefix}`
|
|
74
|
+
> = PrefixRouteKeys<PrefixRoutePatterns<T, PathPrefix>, KeyPrefix>;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Extract params type for a route
|
|
78
|
+
*/
|
|
79
|
+
export type ParamsFor<
|
|
80
|
+
TRoutes extends Record<string, string>,
|
|
81
|
+
TName extends keyof TRoutes
|
|
82
|
+
> = TRoutes[TName] extends string ? ExtractParams<TRoutes[TName]> : never;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if an object type has any keys
|
|
86
|
+
*/
|
|
87
|
+
type IsEmptyObject<T> = keyof T extends never ? true : false;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Type-safe href function signature
|
|
91
|
+
* Provides overloads for routes with and without params
|
|
92
|
+
*/
|
|
93
|
+
export type HrefFunction<TRoutes extends Record<string, string>> = {
|
|
94
|
+
// Overload 1: Routes without params - second arg optional
|
|
95
|
+
<TName extends keyof TRoutes & string>(
|
|
96
|
+
name: IsEmptyObject<ParamsFor<TRoutes, TName>> extends true ? TName : never
|
|
97
|
+
): string;
|
|
98
|
+
|
|
99
|
+
// Overload 2: Routes with params - params required
|
|
100
|
+
<TName extends keyof TRoutes & string>(
|
|
101
|
+
name: TName,
|
|
102
|
+
params: ParamsFor<TRoutes, TName>
|
|
103
|
+
): string;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create a type-safe href function for URL generation
|
|
108
|
+
*
|
|
109
|
+
* @param routeMap - Flattened route map with all registered routes
|
|
110
|
+
* @returns Type-safe href function
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```typescript
|
|
114
|
+
* const href = createHref(mergedRouteMap);
|
|
115
|
+
* href("shop.cart"); // "/shop/cart"
|
|
116
|
+
* href("shop.products.detail", { slug: "my-product" }); // "/shop/product/my-product"
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
export function createHref<TRoutes extends Record<string, string>>(
|
|
120
|
+
routeMap: TRoutes
|
|
121
|
+
): HrefFunction<TRoutes> {
|
|
122
|
+
return ((name: string, params?: Record<string, string>) => {
|
|
123
|
+
const pattern = routeMap[name];
|
|
124
|
+
if (!pattern) {
|
|
125
|
+
throw new Error(`Unknown route: ${name}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!params) return pattern;
|
|
129
|
+
|
|
130
|
+
// Replace :param placeholders with actual values
|
|
131
|
+
return pattern.replace(/:([^/]+)/g, (_, key) => {
|
|
132
|
+
const value = params[key];
|
|
133
|
+
if (value === undefined) {
|
|
134
|
+
throw new Error(`Missing param "${key}" for route "${name}"`);
|
|
135
|
+
}
|
|
136
|
+
return encodeURIComponent(value);
|
|
137
|
+
});
|
|
138
|
+
}) as HrefFunction<TRoutes>;
|
|
139
|
+
}
|
package/src/index.rsc.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rsc-router (react-server environment)
|
|
3
|
+
*
|
|
4
|
+
* This file is used when importing "rsc-router" from RSC (server components).
|
|
5
|
+
* It re-exports everything from the universal index.ts plus adds server-side
|
|
6
|
+
* createLoader that includes the actual loader function.
|
|
7
|
+
*
|
|
8
|
+
* The bundler uses the "react-server" export condition to select this file
|
|
9
|
+
* in RSC context, while the regular index.ts is used in client components.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Re-export all universal exports from index.ts
|
|
13
|
+
export {
|
|
14
|
+
// Universal rendering utilities
|
|
15
|
+
renderSegments,
|
|
16
|
+
// Error classes
|
|
17
|
+
RouteNotFoundError,
|
|
18
|
+
DataNotFoundError,
|
|
19
|
+
notFound,
|
|
20
|
+
MiddlewareError,
|
|
21
|
+
HandlerError,
|
|
22
|
+
BuildError,
|
|
23
|
+
InvalidHandlerError,
|
|
24
|
+
NetworkError,
|
|
25
|
+
isNetworkError,
|
|
26
|
+
sanitizeError,
|
|
27
|
+
// Route pattern definition
|
|
28
|
+
route,
|
|
29
|
+
} from "./index.js";
|
|
30
|
+
|
|
31
|
+
// Re-export all types from index.ts
|
|
32
|
+
export type {
|
|
33
|
+
RouterEnv,
|
|
34
|
+
DefaultEnv,
|
|
35
|
+
RouteDefinition,
|
|
36
|
+
ResolvedRouteMap,
|
|
37
|
+
Handler,
|
|
38
|
+
HandlerContext,
|
|
39
|
+
HandlersForRouteMap,
|
|
40
|
+
ResolvedSegment,
|
|
41
|
+
SegmentMetadata,
|
|
42
|
+
MatchResult,
|
|
43
|
+
ExtractParams,
|
|
44
|
+
GenericParams,
|
|
45
|
+
RevalidateParams,
|
|
46
|
+
ShouldRevalidateFn,
|
|
47
|
+
MiddlewareFn,
|
|
48
|
+
RouteKeys,
|
|
49
|
+
RouteHandler,
|
|
50
|
+
RouteRevalidateFn,
|
|
51
|
+
RouteMiddlewareFn,
|
|
52
|
+
LoaderDefinition,
|
|
53
|
+
LoaderFn,
|
|
54
|
+
LoaderContext,
|
|
55
|
+
ErrorInfo,
|
|
56
|
+
ErrorBoundaryFallbackProps,
|
|
57
|
+
ErrorBoundaryHandler,
|
|
58
|
+
ClientErrorBoundaryFallbackProps,
|
|
59
|
+
NotFoundInfo,
|
|
60
|
+
NotFoundBoundaryFallbackProps,
|
|
61
|
+
NotFoundBoundaryHandler,
|
|
62
|
+
RSCRouterOptions,
|
|
63
|
+
PerformanceMetric,
|
|
64
|
+
MetricsStore,
|
|
65
|
+
} from "./index.js";
|
|
66
|
+
|
|
67
|
+
// Server-side createLoader - includes the actual loader function
|
|
68
|
+
// This is the key addition for RSC context
|
|
69
|
+
export { createLoader } from "./route-definition.js";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rsc-router
|
|
3
|
+
*
|
|
4
|
+
* Universal exports - types and utilities safe for both server and client
|
|
5
|
+
*
|
|
6
|
+
* For server-only exports (route, map, createRSCRouter, etc.):
|
|
7
|
+
* import from "rsc-router/server"
|
|
8
|
+
*
|
|
9
|
+
* For client-only exports (Outlet, useOutlet, etc.):
|
|
10
|
+
* import from "rsc-router/client"
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Universal rendering utilities (work on both server and client)
|
|
14
|
+
export { renderSegments } from "./segment-system.js";
|
|
15
|
+
|
|
16
|
+
// Error classes (can be used on both server and client)
|
|
17
|
+
export {
|
|
18
|
+
RouteNotFoundError,
|
|
19
|
+
DataNotFoundError,
|
|
20
|
+
notFound,
|
|
21
|
+
MiddlewareError,
|
|
22
|
+
HandlerError,
|
|
23
|
+
BuildError,
|
|
24
|
+
InvalidHandlerError,
|
|
25
|
+
NetworkError,
|
|
26
|
+
isNetworkError,
|
|
27
|
+
sanitizeError,
|
|
28
|
+
} from "./errors.js";
|
|
29
|
+
|
|
30
|
+
// Types (safe to import anywhere - no runtime code)
|
|
31
|
+
export type {
|
|
32
|
+
DocumentProps,
|
|
33
|
+
RouterEnv,
|
|
34
|
+
DefaultEnv,
|
|
35
|
+
RouteDefinition,
|
|
36
|
+
ResolvedRouteMap,
|
|
37
|
+
Handler,
|
|
38
|
+
HandlerContext,
|
|
39
|
+
HandlersForRouteMap,
|
|
40
|
+
ResolvedSegment,
|
|
41
|
+
SegmentMetadata,
|
|
42
|
+
MatchResult,
|
|
43
|
+
ExtractParams,
|
|
44
|
+
GenericParams,
|
|
45
|
+
RevalidateParams,
|
|
46
|
+
ShouldRevalidateFn,
|
|
47
|
+
MiddlewareFn,
|
|
48
|
+
RouteKeys,
|
|
49
|
+
RouteHandler,
|
|
50
|
+
RouteRevalidateFn,
|
|
51
|
+
RouteMiddlewareFn,
|
|
52
|
+
LoaderDefinition,
|
|
53
|
+
LoaderFn,
|
|
54
|
+
LoaderContext,
|
|
55
|
+
// Fetchable loader types
|
|
56
|
+
FetchableLoaderOptions,
|
|
57
|
+
LoadOptions,
|
|
58
|
+
LoaderActionContext,
|
|
59
|
+
LoaderAction,
|
|
60
|
+
LoaderMiddlewareFn,
|
|
61
|
+
// Error boundary types
|
|
62
|
+
ErrorInfo,
|
|
63
|
+
ErrorBoundaryFallbackProps,
|
|
64
|
+
ErrorBoundaryHandler,
|
|
65
|
+
ClientErrorBoundaryFallbackProps,
|
|
66
|
+
// NotFound boundary types
|
|
67
|
+
NotFoundInfo,
|
|
68
|
+
NotFoundBoundaryFallbackProps,
|
|
69
|
+
NotFoundBoundaryHandler,
|
|
70
|
+
} from "./types.js";
|
|
71
|
+
|
|
72
|
+
// Router options type
|
|
73
|
+
export type { RSCRouterOptions } from "./router.js";
|
|
74
|
+
|
|
75
|
+
// Metrics types
|
|
76
|
+
export type { PerformanceMetric, MetricsStore } from "./server/context.js";
|
|
77
|
+
|
|
78
|
+
// Client-safe createLoader - only stores the $$id, function is not included
|
|
79
|
+
// Use this when defining loaders that will be imported by client components
|
|
80
|
+
export { createLoader } from "./loader.js";
|
|
81
|
+
|
|
82
|
+
// Route pattern definition helper
|
|
83
|
+
// Used to define route patterns in a shared routes.ts file
|
|
84
|
+
export { route } from "./route-utils.js";
|