@rangojs/router 0.0.0-experimental.2
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/CLAUDE.md +7 -0
- package/README.md +19 -0
- package/dist/vite/index.js +1298 -0
- package/package.json +140 -0
- package/skills/caching/SKILL.md +319 -0
- package/skills/document-cache/SKILL.md +152 -0
- package/skills/hooks/SKILL.md +359 -0
- package/skills/intercept/SKILL.md +292 -0
- package/skills/layout/SKILL.md +216 -0
- package/skills/loader/SKILL.md +365 -0
- package/skills/middleware/SKILL.md +442 -0
- package/skills/parallel/SKILL.md +255 -0
- package/skills/route/SKILL.md +141 -0
- package/skills/router-setup/SKILL.md +403 -0
- package/skills/theme/SKILL.md +54 -0
- package/skills/typesafety/SKILL.md +352 -0
- package/src/__mocks__/version.ts +6 -0
- package/src/__tests__/component-utils.test.ts +76 -0
- package/src/__tests__/route-definition.test.ts +63 -0
- package/src/__tests__/urls.test.tsx +436 -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 +893 -0
- package/src/browser/navigation-client.ts +162 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +559 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +275 -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-href.tsx +208 -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 +164 -0
- package/src/browser/rsc-router.tsx +353 -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 +464 -0
- package/src/cache/__tests__/document-cache.test.ts +522 -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 +428 -0
- package/src/cache/cf/cf-cache-store.ts +428 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/document-cache.ts +340 -0
- package/src/cache/index.ts +58 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +387 -0
- package/src/client.rsc.tsx +88 -0
- package/src/client.tsx +621 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +23 -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 +193 -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-context.ts +33 -0
- package/src/href.ts +177 -0
- package/src/index.rsc.ts +79 -0
- package/src/index.ts +87 -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 +1371 -0
- package/src/route-map-builder.ts +146 -0
- package/src/route-types.ts +198 -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 +158 -0
- package/src/router/loader-resolution.ts +326 -0
- package/src/router/manifest.ts +138 -0
- package/src/router/match-context.ts +264 -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 +266 -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 +214 -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 +272 -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 +3876 -0
- package/src/rsc/__tests__/helpers.test.ts +175 -0
- package/src/rsc/handler.ts +1060 -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 +237 -0
- package/src/segment-system.tsx +456 -0
- package/src/server/__tests__/request-context.test.ts +171 -0
- package/src/server/context.ts +417 -0
- package/src/server/handle-store.ts +230 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +554 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +146 -0
- package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
- package/src/ssr/index.tsx +234 -0
- package/src/theme/ThemeProvider.tsx +291 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/__tests__/theme.test.ts +120 -0
- package/src/theme/constants.ts +55 -0
- package/src/theme/index.ts +58 -0
- package/src/theme/theme-context.ts +70 -0
- package/src/theme/theme-script.ts +152 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types.ts +1561 -0
- package/src/urls.ts +726 -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 +787 -0
- package/src/vite/package-resolution.ts +125 -0
- package/src/vite/version.d.ts +12 -0
- package/src/vite/virtual-entries.ts +109 -0
package/src/href.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
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
|
+
* Note: When using createRSCRouter, types accumulate automatically through the
|
|
12
|
+
* builder chain, so this type is typically not needed.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* // Manual type merging (rarely needed):
|
|
17
|
+
* type AppRoutes = MergeRoutes<[
|
|
18
|
+
* typeof homeRoutes,
|
|
19
|
+
* PrefixRoutePatterns<typeof blogRoutes, "/blog">,
|
|
20
|
+
* ]>;
|
|
21
|
+
*
|
|
22
|
+
* // Preferred: Let router accumulate types automatically
|
|
23
|
+
* const router = createRSCRouter<AppEnv>()
|
|
24
|
+
* .routes(homeRoutes).map(...)
|
|
25
|
+
* .routes("/blog", blogRoutes).map(...);
|
|
26
|
+
* type AppRoutes = typeof router.routeMap;
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export type MergeRoutes<T extends Record<string, string>[]> = T extends [
|
|
30
|
+
infer First extends Record<string, string>,
|
|
31
|
+
...infer Rest extends Record<string, string>[]
|
|
32
|
+
]
|
|
33
|
+
? First & MergeRoutes<Rest>
|
|
34
|
+
: {};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Add key prefix to all entries in a route map
|
|
38
|
+
* { "cart": "/cart" } with prefix "shop" -> { "shop.cart": "/shop/cart" }
|
|
39
|
+
*/
|
|
40
|
+
export type PrefixRouteKeys<
|
|
41
|
+
T extends Record<string, string>,
|
|
42
|
+
Prefix extends string
|
|
43
|
+
> = Prefix extends ""
|
|
44
|
+
? T
|
|
45
|
+
: { [K in keyof T as `${Prefix}.${K & string}`]: T[K] };
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Add path prefix to all patterns in a route map
|
|
49
|
+
* { "cart": "/cart" } with prefix "/shop" -> { "cart": "/shop/cart" }
|
|
50
|
+
*/
|
|
51
|
+
export type PrefixRoutePatterns<
|
|
52
|
+
T extends Record<string, string>,
|
|
53
|
+
PathPrefix extends string
|
|
54
|
+
> = {
|
|
55
|
+
[K in keyof T]: PathPrefix extends "" | "/"
|
|
56
|
+
? T[K]
|
|
57
|
+
: T[K] extends "/"
|
|
58
|
+
? PathPrefix
|
|
59
|
+
: `${PathPrefix}${T[K]}`;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Combined: prefix both keys and patterns
|
|
64
|
+
* Used for module augmentation registration
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* // Given shopRoutes = { "index": "/", "cart": "/cart", "products.detail": "/product/:slug" }
|
|
69
|
+
* // PrefixedRoutes<typeof shopRoutes, "shop"> produces:
|
|
70
|
+
* // { "shop.index": "/shop", "shop.cart": "/shop/cart", "shop.products.detail": "/shop/product/:slug" }
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export type PrefixedRoutes<
|
|
74
|
+
T extends Record<string, string>,
|
|
75
|
+
KeyPrefix extends string,
|
|
76
|
+
PathPrefix extends string = KeyPrefix extends "" ? "" : `/${KeyPrefix}`
|
|
77
|
+
> = PrefixRouteKeys<PrefixRoutePatterns<T, PathPrefix>, KeyPrefix>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Extract params type for a route
|
|
81
|
+
*/
|
|
82
|
+
export type ParamsFor<
|
|
83
|
+
TRoutes extends Record<string, string>,
|
|
84
|
+
TName extends keyof TRoutes
|
|
85
|
+
> = TRoutes[TName] extends string ? ExtractParams<TRoutes[TName]> : never;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if an object type has any keys
|
|
89
|
+
*/
|
|
90
|
+
type IsEmptyObject<T> = keyof T extends never ? true : false;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Type-safe href function signature
|
|
94
|
+
* Provides overloads for routes with and without params
|
|
95
|
+
*/
|
|
96
|
+
export type HrefFunction<TRoutes extends Record<string, string>> = {
|
|
97
|
+
// Overload 1: Routes without params - second arg optional
|
|
98
|
+
<TName extends keyof TRoutes & string>(
|
|
99
|
+
name: IsEmptyObject<ParamsFor<TRoutes, TName>> extends true ? TName : never
|
|
100
|
+
): string;
|
|
101
|
+
|
|
102
|
+
// Overload 2: Routes with params - params required
|
|
103
|
+
<TName extends keyof TRoutes & string>(
|
|
104
|
+
name: TName,
|
|
105
|
+
params: ParamsFor<TRoutes, TName>
|
|
106
|
+
): string;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Type-safe scoped href function signature for use with useHref<typeof patterns>()
|
|
111
|
+
* Accepts local route names (from the patterns), absolute names (with dot), or path-based URLs.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```typescript
|
|
115
|
+
* // In a component rendered by blog routes:
|
|
116
|
+
* const href = useHref<typeof blogPatterns>();
|
|
117
|
+
*
|
|
118
|
+
* href("index") // Local name → resolved with current prefix
|
|
119
|
+
* href("post", { slug: "hello" }) // Local name with params
|
|
120
|
+
* href("shop.cart") // Absolute name → global lookup
|
|
121
|
+
* href("/about") // Path-based → used directly
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
export type ScopedHrefFunction<TLocalRoutes extends Record<string, string>> = {
|
|
125
|
+
// Overload 1: Local routes without params
|
|
126
|
+
<TName extends keyof TLocalRoutes & string>(
|
|
127
|
+
name: IsEmptyObject<ParamsFor<TLocalRoutes, TName>> extends true ? TName : never
|
|
128
|
+
): string;
|
|
129
|
+
|
|
130
|
+
// Overload 2: Local routes with params
|
|
131
|
+
<TName extends keyof TLocalRoutes & string>(
|
|
132
|
+
name: TName,
|
|
133
|
+
params: ParamsFor<TLocalRoutes, TName>
|
|
134
|
+
): string;
|
|
135
|
+
|
|
136
|
+
// Overload 3: Absolute name (contains dot) - global lookup
|
|
137
|
+
(name: `${string}.${string}`, params?: Record<string, string>): string;
|
|
138
|
+
|
|
139
|
+
// Overload 4: Path-based URL - used directly
|
|
140
|
+
(name: `/${string}`, params?: Record<string, string>): string;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create a type-safe href function for URL generation
|
|
145
|
+
*
|
|
146
|
+
* @param routeMap - Flattened route map with all registered routes
|
|
147
|
+
* @returns Type-safe href function
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* // Given routes: { cart: "/shop/cart", detail: "/shop/product/:slug" }
|
|
152
|
+
* const href = createHref(routeMap);
|
|
153
|
+
* href("cart"); // "/shop/cart"
|
|
154
|
+
* href("detail", { slug: "my-product" }); // "/shop/product/my-product"
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export function createHref<TRoutes extends Record<string, string>>(
|
|
158
|
+
routeMap: TRoutes
|
|
159
|
+
): HrefFunction<TRoutes> {
|
|
160
|
+
return ((name: string, params?: Record<string, string>) => {
|
|
161
|
+
const pattern = routeMap[name];
|
|
162
|
+
if (!pattern) {
|
|
163
|
+
throw new Error(`Unknown route: ${name}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!params) return pattern;
|
|
167
|
+
|
|
168
|
+
// Replace :param placeholders with actual values
|
|
169
|
+
return pattern.replace(/:([^/]+)/g, (_, key) => {
|
|
170
|
+
const value = params[key];
|
|
171
|
+
if (value === undefined) {
|
|
172
|
+
throw new Error(`Missing param "${key}" for route "${name}"`);
|
|
173
|
+
}
|
|
174
|
+
return encodeURIComponent(value);
|
|
175
|
+
});
|
|
176
|
+
}) as HrefFunction<TRoutes>;
|
|
177
|
+
}
|
package/src/index.rsc.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
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";
|
|
70
|
+
|
|
71
|
+
// Django-style URL patterns (RSC/server context)
|
|
72
|
+
export {
|
|
73
|
+
urls,
|
|
74
|
+
type PathHelpers,
|
|
75
|
+
type PathOptions,
|
|
76
|
+
type UrlPatterns,
|
|
77
|
+
type IncludeOptions,
|
|
78
|
+
type IncludeItem,
|
|
79
|
+
} from "./urls.js";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
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";
|
|
85
|
+
|
|
86
|
+
// Django-style URL patterns API - types only (urls() is in /server)
|
|
87
|
+
export type { UrlPatterns, PathHelpers } from "./urls.js";
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rsc-router/loader (RSC/server version)
|
|
3
|
+
*
|
|
4
|
+
* Server-side createLoader implementation with full loader functionality.
|
|
5
|
+
* Only used in react-server context via export conditions.
|
|
6
|
+
*
|
|
7
|
+
* For non-fetchable loaders: returns a loader definition with fn included
|
|
8
|
+
* For fetchable loaders: stores fn in registry and returns a serializable loader with action
|
|
9
|
+
*
|
|
10
|
+
* The $$id is injected by the Vite exposeLoaderId plugin as a hidden parameter.
|
|
11
|
+
* Users don't need to pass any name - IDs are auto-generated from file path.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
FetchableLoaderOptions,
|
|
16
|
+
LoaderDefinition,
|
|
17
|
+
LoaderFn,
|
|
18
|
+
} from "./types.js";
|
|
19
|
+
import type { MiddlewareFn } from "./router/middleware.js";
|
|
20
|
+
import { getRequestContext } from "./server/request-context.js";
|
|
21
|
+
|
|
22
|
+
// Internal registry for fetchable loaders (server-side only)
|
|
23
|
+
// Maps loader $$id to its function and middleware
|
|
24
|
+
//
|
|
25
|
+
// WHY TWO REGISTRIES?
|
|
26
|
+
// This registry (fetchableLoaderRegistry) is populated immediately when createLoader() runs.
|
|
27
|
+
// The other registry in loader-registry.ts (loaderRegistry) is a cache used by the RSC handler
|
|
28
|
+
// for GET-based fetching. The RSC handler calls getFetchableLoader() from here to populate
|
|
29
|
+
// its cache. This separation allows:
|
|
30
|
+
// 1. Server actions to look up loaders directly without going through lazy loading
|
|
31
|
+
// 2. The RSC handler to use lazy loading for production builds
|
|
32
|
+
// 3. Both to share the same source of truth (this registry)
|
|
33
|
+
const fetchableLoaderRegistry = new Map<
|
|
34
|
+
string,
|
|
35
|
+
{ fn: LoaderFn<any, any, any>; middleware: MiddlewareFn[] }
|
|
36
|
+
>();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Register a fetchable loader's function internally
|
|
40
|
+
* Called during module initialization with the $$id
|
|
41
|
+
*/
|
|
42
|
+
function registerFetchableLoader(
|
|
43
|
+
id: string,
|
|
44
|
+
fn: LoaderFn<any, any, any>,
|
|
45
|
+
middleware: MiddlewareFn[]
|
|
46
|
+
): void {
|
|
47
|
+
fetchableLoaderRegistry.set(id, { fn, middleware });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get a fetchable loader's function from the internal registry by $$id
|
|
52
|
+
*
|
|
53
|
+
* This is used internally by:
|
|
54
|
+
* - Server actions (loaderAction) to execute loader functions
|
|
55
|
+
* - loader-registry.ts to populate the main registry for GET-based fetching
|
|
56
|
+
*
|
|
57
|
+
* Loaders are registered here when createLoader() is called with fetchable: true.
|
|
58
|
+
* The $$id is injected by the Vite exposeLoaderId plugin.
|
|
59
|
+
*
|
|
60
|
+
* @param id - The loader's $$id (auto-generated from file path + export name)
|
|
61
|
+
* @returns The loader function and middleware, or undefined if not found
|
|
62
|
+
*
|
|
63
|
+
* @internal This is primarily for internal use by the router infrastructure
|
|
64
|
+
*/
|
|
65
|
+
export function getFetchableLoader(
|
|
66
|
+
id: string
|
|
67
|
+
): { fn: LoaderFn<any, any, any>; middleware: MiddlewareFn[] } | undefined {
|
|
68
|
+
return fetchableLoaderRegistry.get(id);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Overload 1: With function only (not fetchable)
|
|
72
|
+
export function createLoader<T>(
|
|
73
|
+
fn: LoaderFn<T, Record<string, string | undefined>, any>
|
|
74
|
+
): LoaderDefinition<Awaited<T>, Record<string, string | undefined>>;
|
|
75
|
+
|
|
76
|
+
// Overload 2: Fetchable with `true` (no middleware)
|
|
77
|
+
export function createLoader<T>(
|
|
78
|
+
fn: LoaderFn<T, Record<string, string | undefined>, any>,
|
|
79
|
+
fetchable: true
|
|
80
|
+
): LoaderDefinition<Awaited<T>, Record<string, string | undefined>>;
|
|
81
|
+
|
|
82
|
+
// Overload 3: Fetchable with middleware options
|
|
83
|
+
export function createLoader<T>(
|
|
84
|
+
fn: LoaderFn<T, Record<string, string | undefined>, any>,
|
|
85
|
+
options: FetchableLoaderOptions
|
|
86
|
+
): LoaderDefinition<Awaited<T>, Record<string, string | undefined>>;
|
|
87
|
+
|
|
88
|
+
// Implementation - the $$id parameter is injected by Vite plugin, not user-provided
|
|
89
|
+
export function createLoader<T>(
|
|
90
|
+
fn: LoaderFn<T, Record<string, string | undefined>, any>,
|
|
91
|
+
fetchable?: true | FetchableLoaderOptions,
|
|
92
|
+
// Hidden parameter injected by Vite exposeLoaderId plugin
|
|
93
|
+
__injectedId?: string
|
|
94
|
+
): LoaderDefinition<Awaited<T>, Record<string, string | undefined>> {
|
|
95
|
+
// The $$id will be set on the returned object by Vite plugin
|
|
96
|
+
// For fetchable loaders, __injectedId is also passed as a parameter
|
|
97
|
+
const loaderId = __injectedId || "";
|
|
98
|
+
|
|
99
|
+
// If not fetchable, return a simple stub with fn included
|
|
100
|
+
if (fetchable === undefined) {
|
|
101
|
+
return {
|
|
102
|
+
__brand: "loader",
|
|
103
|
+
$$id: loaderId,
|
|
104
|
+
fn: fn as LoaderFn<Awaited<T>, Record<string, string | undefined>, any>,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Fetchable loader - store fn in registry and return a serializable object
|
|
109
|
+
const middleware: MiddlewareFn[] =
|
|
110
|
+
fetchable === true ? [] : fetchable?.middleware || [];
|
|
111
|
+
|
|
112
|
+
// Register the function in the internal registry by $$id (server-side only)
|
|
113
|
+
// The server action will look it up by $$id when executed
|
|
114
|
+
if (fn && loaderId) {
|
|
115
|
+
registerFetchableLoader(loaderId, fn, middleware);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Create server action for form-based fetching
|
|
119
|
+
// This action is serializable and can be passed to client components
|
|
120
|
+
// The loaderId is captured in closure (it's a primitive string)
|
|
121
|
+
//
|
|
122
|
+
// IMPORTANT: The signature must be (prevState, formData) for useActionState compatibility.
|
|
123
|
+
// When used with useActionState, React passes the previous state as the first argument.
|
|
124
|
+
// The prevState is ignored here since loaders are stateless data fetchers.
|
|
125
|
+
async function loaderAction(
|
|
126
|
+
_prevState: Awaited<T> | null,
|
|
127
|
+
formData: FormData
|
|
128
|
+
): Promise<Awaited<T>> {
|
|
129
|
+
"use server";
|
|
130
|
+
|
|
131
|
+
// Look up the loader from registry by $$id
|
|
132
|
+
const registered = fetchableLoaderRegistry.get(loaderId);
|
|
133
|
+
if (!registered) {
|
|
134
|
+
throw new Error(`Loader "${loaderId}" not found in registry`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Get request context (env, request, url, variables) from the RSC handler
|
|
138
|
+
// This is set by runWithRequestContext in rsc/index.ts when executing actions
|
|
139
|
+
const requestCtx = getRequestContext();
|
|
140
|
+
|
|
141
|
+
// Convert FormData to params object
|
|
142
|
+
const params: Record<string, string> = {};
|
|
143
|
+
formData.forEach((value, key) => {
|
|
144
|
+
if (typeof value === "string") {
|
|
145
|
+
params[key] = value;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Use real request/url from context, or fall back to synthetic for edge cases
|
|
150
|
+
const actionUrl = requestCtx?.url ?? new URL("http://localhost/");
|
|
151
|
+
const actionRequest = requestCtx?.request ?? new Request(actionUrl, { method: "POST" });
|
|
152
|
+
const env = requestCtx?.env ?? {};
|
|
153
|
+
|
|
154
|
+
// Merge variables from request context (app-level middleware) with loader-specific variables
|
|
155
|
+
// requestCtx.var is the shared variables object from the handler
|
|
156
|
+
const variables: Record<string, any> = { ...requestCtx?.var };
|
|
157
|
+
|
|
158
|
+
// Execute middleware for auth checks, headers, cookies
|
|
159
|
+
// Headers/cookies set on ctx.res will be merged into the final response
|
|
160
|
+
if (registered.middleware.length > 0 && requestCtx?.res) {
|
|
161
|
+
const { executeServerActionMiddleware } = await import(
|
|
162
|
+
"./router/middleware.js"
|
|
163
|
+
);
|
|
164
|
+
await executeServerActionMiddleware(
|
|
165
|
+
registered.middleware,
|
|
166
|
+
actionRequest,
|
|
167
|
+
env,
|
|
168
|
+
params,
|
|
169
|
+
variables,
|
|
170
|
+
requestCtx.res
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Build context using createHandlerContext for consistency with route handlers
|
|
175
|
+
// Variables are now accessed from request context via getRequestContext()
|
|
176
|
+
const { createHandlerContext } = await import("./router/handler-context.js");
|
|
177
|
+
const baseCtx = createHandlerContext(
|
|
178
|
+
params,
|
|
179
|
+
actionRequest,
|
|
180
|
+
actionUrl.searchParams,
|
|
181
|
+
actionUrl.pathname,
|
|
182
|
+
actionUrl,
|
|
183
|
+
env
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Extend with server action specific properties
|
|
187
|
+
const ctx: any = {
|
|
188
|
+
...baseCtx,
|
|
189
|
+
method: "POST",
|
|
190
|
+
formData,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Execute and return result
|
|
194
|
+
return registered.fn(ctx);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Return a loader object with action for form-based fetching
|
|
198
|
+
// The exposeLoaderId plugin will set $$id on this object
|
|
199
|
+
return {
|
|
200
|
+
__brand: "loader",
|
|
201
|
+
$$id: loaderId,
|
|
202
|
+
action: loaderAction,
|
|
203
|
+
};
|
|
204
|
+
}
|
package/src/loader.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rsc-router/loader (client version)
|
|
3
|
+
*
|
|
4
|
+
* Client-only stub for createLoader. Returns a minimal loader definition
|
|
5
|
+
* that can be passed to hooks like useLoader. The actual loader function
|
|
6
|
+
* is not included - it only exists on the server.
|
|
7
|
+
*
|
|
8
|
+
* The $$id is injected by the Vite exposeLoaderId plugin.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
FetchableLoaderOptions,
|
|
13
|
+
LoaderDefinition,
|
|
14
|
+
LoaderFn,
|
|
15
|
+
} from "./types.js";
|
|
16
|
+
|
|
17
|
+
// Overload 1: With function only (not fetchable)
|
|
18
|
+
export function createLoader<T>(
|
|
19
|
+
fn: LoaderFn<T, Record<string, string | undefined>, any>
|
|
20
|
+
): LoaderDefinition<Awaited<T>, Record<string, string | undefined>>;
|
|
21
|
+
|
|
22
|
+
// Overload 2: Fetchable with `true` (no middleware)
|
|
23
|
+
export function createLoader<T>(
|
|
24
|
+
fn: LoaderFn<T, Record<string, string | undefined>, any>,
|
|
25
|
+
fetchable: true
|
|
26
|
+
): LoaderDefinition<Awaited<T>, Record<string, string | undefined>>;
|
|
27
|
+
|
|
28
|
+
// Overload 3: Fetchable with middleware options
|
|
29
|
+
export function createLoader<T>(
|
|
30
|
+
fn: LoaderFn<T, Record<string, string | undefined>, any>,
|
|
31
|
+
options: FetchableLoaderOptions
|
|
32
|
+
): LoaderDefinition<Awaited<T>, Record<string, string | undefined>>;
|
|
33
|
+
|
|
34
|
+
// Implementation - client stub that just returns the loader definition
|
|
35
|
+
// The $$id parameter is injected by Vite plugin, not user-provided
|
|
36
|
+
export function createLoader<T>(
|
|
37
|
+
_fn: LoaderFn<T, Record<string, string | undefined>, any>,
|
|
38
|
+
_fetchable?: true | FetchableLoaderOptions,
|
|
39
|
+
__injectedId?: string
|
|
40
|
+
): LoaderDefinition<Awaited<T>, Record<string, string | undefined>> {
|
|
41
|
+
// Client only needs the $$id for identification
|
|
42
|
+
// The actual loader function is only used on the server
|
|
43
|
+
return {
|
|
44
|
+
__brand: "loader",
|
|
45
|
+
$$id: __injectedId || "",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import type { NetworkError } from "./errors.js";
|
|
5
|
+
|
|
6
|
+
interface NetworkErrorThrowerProps {
|
|
7
|
+
error: NetworkError;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Client component that throws a NetworkError during render.
|
|
12
|
+
* Used to trigger the root error boundary when a network error occurs
|
|
13
|
+
* during navigation or server actions.
|
|
14
|
+
*
|
|
15
|
+
* This must be a separate component because:
|
|
16
|
+
* 1. Errors must be thrown during React's render phase to be caught by error boundaries
|
|
17
|
+
* 2. The error occurs in async code (fetch), so we need to propagate it to React's render
|
|
18
|
+
*/
|
|
19
|
+
export function NetworkErrorThrower({ error }: NetworkErrorThrowerProps): ReactNode {
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Context, createContext, type ReactNode } from "react";
|
|
2
|
+
import type { ResolvedSegment } from "./types";
|
|
3
|
+
|
|
4
|
+
export interface OutletContextValue {
|
|
5
|
+
content: ReactNode;
|
|
6
|
+
parallel?: ResolvedSegment[];
|
|
7
|
+
segment?: ResolvedSegment;
|
|
8
|
+
loaderData?: Record<string, any>;
|
|
9
|
+
parent?: OutletContextValue | null;
|
|
10
|
+
/** Loading component for Suspense fallback (from segment's loading() definition) */
|
|
11
|
+
loading?: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const OutletContext: Context<OutletContextValue | null> =
|
|
15
|
+
createContext<OutletContextValue | null>(null);
|