@rangojs/router 0.0.0-experimental.62 → 0.0.0-experimental.64
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 +61 -8
- package/dist/bin/rango.js +2 -1
- package/dist/vite/index.js +142 -62
- package/dist/vite/index.js.bak +5448 -0
- package/package.json +14 -15
- package/skills/prerender/SKILL.md +110 -68
- package/src/__internal.ts +1 -1
- package/src/build/generate-manifest.ts +3 -6
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/client.tsx +2 -56
- package/src/index.rsc.ts +3 -1
- package/src/index.ts +8 -0
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/route-definition/dsl-helpers.ts +42 -19
- package/src/route-definition/helpers-types.ts +4 -1
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-types.ts +11 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/handler-context.ts +20 -5
- package/src/router/match-api.ts +124 -189
- package/src/router/match-middleware/cache-lookup.ts +2 -6
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/prerender-match.ts +104 -8
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-interfaces.ts +11 -0
- package/src/router/segment-resolution/fresh.ts +44 -2
- package/src/router/segment-resolution/revalidation.ts +53 -5
- package/src/router.ts +13 -1
- package/src/rsc/handler.ts +456 -373
- package/src/rsc/ssr-setup.ts +1 -1
- package/src/server/context.ts +5 -1
- package/src/server/request-context.ts +7 -0
- package/src/static-handler.ts +18 -6
- package/src/types/handler-context.ts +12 -2
- package/src/types/route-entry.ts +1 -1
- package/src/urls/path-helper-types.ts +9 -2
- package/src/urls/path-helper.ts +47 -12
- package/src/urls/response-types.ts +16 -6
- package/src/use-loader.tsx +73 -4
- package/src/vite/discovery/bundle-postprocess.ts +30 -33
- package/src/vite/discovery/prerender-collection.ts +14 -1
- package/src/vite/discovery/state.ts +13 -4
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +60 -5
- package/src/vite/rango.ts +2 -1
- package/src/vite/router-discovery.ts +153 -34
package/src/rsc/ssr-setup.ts
CHANGED
|
@@ -98,7 +98,7 @@ export function getSSRSetup<TEnv>(
|
|
|
98
98
|
* the isRscRequest decision in rsc-rendering.ts.
|
|
99
99
|
*
|
|
100
100
|
* Note: response/mime routes are excluded by the caller — this function
|
|
101
|
-
* runs after
|
|
101
|
+
* runs after classifyRequest() determines the request mode.
|
|
102
102
|
*/
|
|
103
103
|
export function mayNeedSSR(request: Request, url: URL): boolean {
|
|
104
104
|
if (
|
package/src/server/context.ts
CHANGED
|
@@ -191,8 +191,12 @@ export type EntryData =
|
|
|
191
191
|
/** Original PrerenderHandlerDefinition (for build-time getParams access) */
|
|
192
192
|
prerenderDef?: {
|
|
193
193
|
getParams?: (ctx: any) => Promise<any[]> | any[];
|
|
194
|
-
options?: {
|
|
194
|
+
options?: { concurrency?: number };
|
|
195
195
|
};
|
|
196
|
+
/** Set when route is wrapped with Passthrough() — has a separate live handler */
|
|
197
|
+
isPassthrough?: true;
|
|
198
|
+
/** Live handler for runtime fallback (only set on Passthrough routes) */
|
|
199
|
+
liveHandler?: Handler<any, any, any>;
|
|
196
200
|
/** Set when handler is a Static definition (build-time only) */
|
|
197
201
|
isStaticPrerender?: true;
|
|
198
202
|
/** Static handler $$id for build-time store lookup */
|
|
@@ -290,6 +290,12 @@ export interface RequestContext<
|
|
|
290
290
|
|
|
291
291
|
/** @internal Router basename for this request (used by redirect()) */
|
|
292
292
|
_basename?: string;
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* @internal RouteSnapshot from classifyRequest, reused by match/matchPartial
|
|
296
|
+
* to avoid a second resolveRoute call. Cleared on HMR invalidation.
|
|
297
|
+
*/
|
|
298
|
+
_classifiedRoute?: import("../router/route-snapshot.js").RouteSnapshot;
|
|
293
299
|
}
|
|
294
300
|
|
|
295
301
|
/**
|
|
@@ -322,6 +328,7 @@ export type PublicRequestContext<
|
|
|
322
328
|
| "_basename"
|
|
323
329
|
| "_setStatus"
|
|
324
330
|
| "_variables"
|
|
331
|
+
| "_classifiedRoute"
|
|
325
332
|
| "res"
|
|
326
333
|
>;
|
|
327
334
|
|
package/src/static-handler.ts
CHANGED
|
@@ -32,11 +32,21 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import type { ReactNode } from "react";
|
|
34
34
|
import type { Handler } from "./types.js";
|
|
35
|
-
import type {
|
|
35
|
+
import type { StaticBuildContext } from "./prerender.js";
|
|
36
|
+
import type { UseItems, HandlerUseItem } from "./route-types.js";
|
|
36
37
|
import { isCachedFunction } from "./cache/taint.js";
|
|
37
38
|
|
|
38
39
|
// -- Types ------------------------------------------------------------------
|
|
39
40
|
|
|
41
|
+
export interface StaticHandlerOptions {
|
|
42
|
+
/**
|
|
43
|
+
* Keep handler in server bundle for live fallback (default: false).
|
|
44
|
+
* false: handler replaced with stub, source-only APIs excluded from bundle.
|
|
45
|
+
* true: handler stays in bundle, renders live at request time.
|
|
46
|
+
*/
|
|
47
|
+
passthrough?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
export interface StaticHandlerDefinition<
|
|
41
51
|
TParams extends Record<string, any> = any,
|
|
42
52
|
> {
|
|
@@ -46,14 +56,16 @@ export interface StaticHandlerDefinition<
|
|
|
46
56
|
/** In dev mode, the actual handler function that layout/path/parallel can call. */
|
|
47
57
|
handler: Handler<TParams>;
|
|
48
58
|
/** Static handler options (passthrough support). */
|
|
49
|
-
options?:
|
|
59
|
+
options?: StaticHandlerOptions;
|
|
60
|
+
/** Composable default DSL items merged when the handler is mounted. */
|
|
61
|
+
use?: () => UseItems<HandlerUseItem>;
|
|
50
62
|
}
|
|
51
63
|
|
|
52
64
|
// -- Function ---------------------------------------------------------------
|
|
53
65
|
|
|
54
66
|
export function Static<TParams extends Record<string, any> = {}>(
|
|
55
67
|
handler: (ctx: StaticBuildContext) => ReactNode | Promise<ReactNode>,
|
|
56
|
-
options?:
|
|
68
|
+
options?: StaticHandlerOptions,
|
|
57
69
|
__injectedId?: string,
|
|
58
70
|
): StaticHandlerDefinition<TParams>;
|
|
59
71
|
|
|
@@ -61,7 +73,7 @@ export function Static<TParams extends Record<string, any> = {}>(
|
|
|
61
73
|
|
|
62
74
|
export function Static<TParams extends Record<string, any>>(
|
|
63
75
|
handler: Function,
|
|
64
|
-
optionsOrId?:
|
|
76
|
+
optionsOrId?: StaticHandlerOptions | string,
|
|
65
77
|
maybeId?: string,
|
|
66
78
|
): StaticHandlerDefinition<TParams> {
|
|
67
79
|
if (isCachedFunction(handler)) {
|
|
@@ -72,13 +84,13 @@ export function Static<TParams extends Record<string, any>>(
|
|
|
72
84
|
);
|
|
73
85
|
}
|
|
74
86
|
|
|
75
|
-
let options:
|
|
87
|
+
let options: StaticHandlerOptions | undefined;
|
|
76
88
|
let id: string;
|
|
77
89
|
|
|
78
90
|
if (typeof optionsOrId === "string") {
|
|
79
91
|
id = optionsOrId;
|
|
80
92
|
} else {
|
|
81
|
-
options = optionsOrId as
|
|
93
|
+
options = optionsOrId as StaticHandlerOptions | undefined;
|
|
82
94
|
id = maybeId ?? "";
|
|
83
95
|
}
|
|
84
96
|
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
ResolvedRouteMap,
|
|
20
20
|
} from "./route-config.js";
|
|
21
21
|
import type { LoaderDefinition } from "./loader-types.js";
|
|
22
|
+
import type { UseItems, HandlerUseItem } from "../route-types.js";
|
|
22
23
|
|
|
23
24
|
// Re-export MiddlewareFn for internal/advanced use
|
|
24
25
|
export type { MiddlewareFn } from "../router/middleware.js";
|
|
@@ -135,7 +136,7 @@ export type Handler<
|
|
|
135
136
|
| Record<string, any> = {},
|
|
136
137
|
TRouteMap extends {} = DefaultHandlerRouteMap,
|
|
137
138
|
TEnv = DefaultEnv,
|
|
138
|
-
> = (
|
|
139
|
+
> = ((
|
|
139
140
|
ctx: HandlerContext<
|
|
140
141
|
T extends `.${infer Local}`
|
|
141
142
|
? Local extends keyof TRouteMap
|
|
@@ -160,7 +161,10 @@ export type Handler<
|
|
|
160
161
|
: ExtractSearchFromEntry<DefaultHandlerRouteMap, T>,
|
|
161
162
|
TRouteMap extends DefaultHandlerRouteMap ? never : TRouteMap
|
|
162
163
|
>,
|
|
163
|
-
) => ReactNode | Promise<ReactNode> | Response | Promise<Response
|
|
164
|
+
) => ReactNode | Promise<ReactNode> | Response | Promise<Response>) & {
|
|
165
|
+
/** Composable default DSL items merged when the handler is mounted. */
|
|
166
|
+
use?: () => UseItems<HandlerUseItem>;
|
|
167
|
+
};
|
|
164
168
|
|
|
165
169
|
/**
|
|
166
170
|
* Context passed to handlers (Hono-inspired type-safe context)
|
|
@@ -205,6 +209,12 @@ export type HandlerContext<
|
|
|
205
209
|
* Live request rendering, including passthrough fallback, uses `false`.
|
|
206
210
|
*/
|
|
207
211
|
build: boolean;
|
|
212
|
+
/**
|
|
213
|
+
* True when running in Vite dev mode, false during production build or
|
|
214
|
+
* live request rendering. Use this to branch on runtime mode without
|
|
215
|
+
* changing build semantics (e.g., skip expensive operations in dev).
|
|
216
|
+
*/
|
|
217
|
+
dev: boolean;
|
|
208
218
|
/**
|
|
209
219
|
* The original incoming Request object (transport URL intact).
|
|
210
220
|
* Use `ctx.url` / `ctx.searchParams` for application logic — those have
|
package/src/types/route-entry.ts
CHANGED
|
@@ -69,7 +69,7 @@ export interface RouteEntry<TEnv = any> {
|
|
|
69
69
|
prerenderRouteKeys?: Set<string>;
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
* Route keys in this entry that
|
|
72
|
+
* Route keys in this entry that are wrapped with `Passthrough()`.
|
|
73
73
|
* Used by the non-trie match path to set the `pt` flag.
|
|
74
74
|
*/
|
|
75
75
|
passthroughRouteKeys?: Set<string>;
|
|
@@ -37,7 +37,10 @@ import type {
|
|
|
37
37
|
UseItems,
|
|
38
38
|
} from "../route-types.js";
|
|
39
39
|
import type { SearchSchema } from "../search-params.js";
|
|
40
|
-
import type {
|
|
40
|
+
import type {
|
|
41
|
+
PrerenderHandlerDefinition,
|
|
42
|
+
PassthroughHandlerDefinition,
|
|
43
|
+
} from "../prerender.js";
|
|
41
44
|
import type { StaticHandlerDefinition } from "../static-handler.js";
|
|
42
45
|
import type { InterceptWhenFn } from "../server/context";
|
|
43
46
|
import type {
|
|
@@ -70,6 +73,7 @@ export type PathFn<TEnv> = <
|
|
|
70
73
|
ctx: HandlerContext<TParams, TEnv, TSearch>,
|
|
71
74
|
) => ReactNode | Promise<ReactNode> | Response | Promise<Response>)
|
|
72
75
|
| PrerenderHandlerDefinition<TParams>
|
|
76
|
+
| PassthroughHandlerDefinition<TParams, TEnv>
|
|
73
77
|
| StaticHandlerDefinition<TParams>,
|
|
74
78
|
optionsOrUse?: PathOptions<TName, TSearch> | (() => UseItems<RouteUseItem>),
|
|
75
79
|
use?: () => UseItems<RouteUseItem>,
|
|
@@ -280,7 +284,10 @@ export type PathHelpers<TEnv> = {
|
|
|
280
284
|
/**
|
|
281
285
|
* Attach a loading component to the current route/layout
|
|
282
286
|
*/
|
|
283
|
-
loading: (
|
|
287
|
+
loading: (
|
|
288
|
+
component: ReactNode | (() => ReactNode),
|
|
289
|
+
options?: { ssr?: boolean },
|
|
290
|
+
) => LoadingItem;
|
|
284
291
|
|
|
285
292
|
/**
|
|
286
293
|
* Attach an error boundary to catch errors in this segment
|
package/src/urls/path-helper.ts
CHANGED
|
@@ -12,10 +12,11 @@ import {
|
|
|
12
12
|
getNamePrefix,
|
|
13
13
|
getRootScoped,
|
|
14
14
|
} from "../server/context";
|
|
15
|
-
import { invariant } from "../errors";
|
|
15
|
+
import { invariant, DataNotFoundError } from "../errors";
|
|
16
16
|
import { validateUserRouteName } from "../route-name.js";
|
|
17
17
|
import {
|
|
18
18
|
isPrerenderHandler,
|
|
19
|
+
isPassthroughHandler,
|
|
19
20
|
type PrerenderHandlerDefinition,
|
|
20
21
|
} from "../prerender.js";
|
|
21
22
|
import {
|
|
@@ -34,6 +35,10 @@ import type {
|
|
|
34
35
|
JsonResponsePathFn,
|
|
35
36
|
TextResponsePathFn,
|
|
36
37
|
} from "./path-helper-types.js";
|
|
38
|
+
import {
|
|
39
|
+
resolveHandlerUse,
|
|
40
|
+
mergeHandlerUse,
|
|
41
|
+
} from "../route-definition/resolve-handler-use.js";
|
|
37
42
|
|
|
38
43
|
/**
|
|
39
44
|
* Check if a value is a valid use item
|
|
@@ -142,6 +147,12 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
|
|
|
142
147
|
use = maybeUse;
|
|
143
148
|
}
|
|
144
149
|
|
|
150
|
+
// Merge handler.use() defaults with explicit use()
|
|
151
|
+
// Response routes (path.json, path.text, etc.) only allow middleware + cache
|
|
152
|
+
const handlerUseFn = resolveHandlerUse(handler);
|
|
153
|
+
const mountSite = resolveResponseType(options) ? "response" : "path";
|
|
154
|
+
const mergedUse = mergeHandlerUse(handlerUseFn, use, mountSite);
|
|
155
|
+
|
|
145
156
|
// Get prefixes from context (set by include())
|
|
146
157
|
const urlPrefix = getUrlPrefix();
|
|
147
158
|
const namePrefix = getNamePrefix();
|
|
@@ -176,14 +187,31 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
|
|
|
176
187
|
}
|
|
177
188
|
|
|
178
189
|
// Ensure handler is always a function (wrap ReactNode or extract from prerender/static def)
|
|
190
|
+
// For prerender stubs (production builds where handler code is evicted),
|
|
191
|
+
// handler.handler is undefined — provide a notFound fallback so requests
|
|
192
|
+
// for non-prerendered params get 404 instead of "handler is not a function".
|
|
179
193
|
const wrappedHandler: Handler<any, any, TEnv> =
|
|
180
194
|
typeof handler === "function"
|
|
181
195
|
? (handler as Handler<any, any, TEnv>)
|
|
182
|
-
:
|
|
183
|
-
?
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
196
|
+
: isPassthroughHandler(handler)
|
|
197
|
+
? typeof handler.prerenderDef.handler === "function"
|
|
198
|
+
? (handler.prerenderDef.handler as Handler<any, any, TEnv>)
|
|
199
|
+
: () => {
|
|
200
|
+
throw new DataNotFoundError(
|
|
201
|
+
"No prerender data found for this route",
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
: isPrerenderHandler(handler)
|
|
205
|
+
? typeof handler.handler === "function"
|
|
206
|
+
? (handler.handler as Handler<any, any, TEnv>)
|
|
207
|
+
: () => {
|
|
208
|
+
throw new DataNotFoundError(
|
|
209
|
+
"No prerender data found for this route",
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
: isStaticHandler(handler)
|
|
213
|
+
? (handler.handler as Handler<any, any, TEnv>)
|
|
214
|
+
: () => handler;
|
|
187
215
|
|
|
188
216
|
const entry = {
|
|
189
217
|
id: namespace,
|
|
@@ -203,12 +231,19 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
|
|
|
203
231
|
intercept: [],
|
|
204
232
|
loader: [],
|
|
205
233
|
...(urlPrefix ? { mountPath: urlPrefix } : {}),
|
|
206
|
-
...(
|
|
234
|
+
...(isPassthroughHandler(handler)
|
|
207
235
|
? {
|
|
208
236
|
isPrerender: true as const,
|
|
209
|
-
prerenderDef: handler as PrerenderHandlerDefinition,
|
|
237
|
+
prerenderDef: handler.prerenderDef as PrerenderHandlerDefinition,
|
|
238
|
+
isPassthrough: true as const,
|
|
239
|
+
liveHandler: handler.liveHandler as Handler<any, any, TEnv>,
|
|
210
240
|
}
|
|
211
|
-
:
|
|
241
|
+
: isPrerenderHandler(handler)
|
|
242
|
+
? {
|
|
243
|
+
isPrerender: true as const,
|
|
244
|
+
prerenderDef: handler as PrerenderHandlerDefinition,
|
|
245
|
+
}
|
|
246
|
+
: {}),
|
|
212
247
|
...(isStaticHandler(handler)
|
|
213
248
|
? {
|
|
214
249
|
isStaticPrerender: true as const,
|
|
@@ -264,9 +299,9 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
|
|
|
264
299
|
registerSearchSchema(routeName, options.search);
|
|
265
300
|
}
|
|
266
301
|
|
|
267
|
-
// Run use callback if
|
|
268
|
-
if (
|
|
269
|
-
const result = store.run(namespace, entry,
|
|
302
|
+
// Run merged use callback (handler.use defaults + explicit use) if present
|
|
303
|
+
if (mergedUse) {
|
|
304
|
+
const result = store.run(namespace, entry, mergedUse)?.flat(3);
|
|
270
305
|
invariant(
|
|
271
306
|
Array.isArray(result) && result.every((item) => isValidUseItem(item)),
|
|
272
307
|
`path() use() callback must return an array of use items [${namespace}]`,
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
DefaultReverseRouteMap,
|
|
5
5
|
DefaultVars,
|
|
6
6
|
} from "../types/global-namespace.js";
|
|
7
|
+
import type { UseItems, ResponseRouteUseItem } from "../route-types.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Reverse function for response handler contexts.
|
|
@@ -38,9 +39,12 @@ export const RESPONSE_TYPE: unique symbol = Symbol.for(
|
|
|
38
39
|
* Handler that must return Response (not ReactNode).
|
|
39
40
|
* Used by path.image(), path.stream(), path.any() (binary/streaming data).
|
|
40
41
|
*/
|
|
41
|
-
export type ResponseHandler<TParams = Record<string, string>, TEnv = any> = (
|
|
42
|
+
export type ResponseHandler<TParams = Record<string, string>, TEnv = any> = ((
|
|
42
43
|
ctx: ResponseHandlerContext<TParams, TEnv>,
|
|
43
|
-
) => Response | Promise<Response
|
|
44
|
+
) => Response | Promise<Response>) & {
|
|
45
|
+
/** Composable default DSL items merged when the handler is mounted. */
|
|
46
|
+
use?: () => UseItems<ResponseRouteUseItem>;
|
|
47
|
+
};
|
|
44
48
|
|
|
45
49
|
/**
|
|
46
50
|
* JSON-serializable value type for auto-wrap support.
|
|
@@ -60,9 +64,12 @@ export type JsonValue =
|
|
|
60
64
|
export type JsonResponseHandler<
|
|
61
65
|
TParams = Record<string, string>,
|
|
62
66
|
TEnv = any,
|
|
63
|
-
> = (
|
|
67
|
+
> = ((
|
|
64
68
|
ctx: ResponseHandlerContext<TParams, TEnv>,
|
|
65
|
-
) => JsonValue | Response | Promise<JsonValue | Response
|
|
69
|
+
) => JsonValue | Response | Promise<JsonValue | Response>) & {
|
|
70
|
+
/** Composable default DSL items merged when the handler is mounted. */
|
|
71
|
+
use?: () => UseItems<ResponseRouteUseItem>;
|
|
72
|
+
};
|
|
66
73
|
|
|
67
74
|
/**
|
|
68
75
|
* Handler for text-based response routes (text, html, xml).
|
|
@@ -71,9 +78,12 @@ export type JsonResponseHandler<
|
|
|
71
78
|
export type TextResponseHandler<
|
|
72
79
|
TParams = Record<string, string>,
|
|
73
80
|
TEnv = any,
|
|
74
|
-
> = (
|
|
81
|
+
> = ((
|
|
75
82
|
ctx: ResponseHandlerContext<TParams, TEnv>,
|
|
76
|
-
) => string | Response | Promise<string | Response
|
|
83
|
+
) => string | Response | Promise<string | Response>) & {
|
|
84
|
+
/** Composable default DSL items merged when the handler is mounted. */
|
|
85
|
+
use?: () => UseItems<ResponseRouteUseItem>;
|
|
86
|
+
};
|
|
77
87
|
|
|
78
88
|
/**
|
|
79
89
|
* Lighter handler context for response routes.
|
package/src/use-loader.tsx
CHANGED
|
@@ -1,9 +1,70 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
isValidElement,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
} from "react";
|
|
4
13
|
import { OutletContext, type OutletContextValue } from "./outlet-context.js";
|
|
5
14
|
import type { LoaderDefinition, LoadOptions } from "./types.js";
|
|
6
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Extract a specific loader's data from a content ReactNode.
|
|
18
|
+
*
|
|
19
|
+
* When a route registers loaders via loader(), the resolved data lives in
|
|
20
|
+
* the route's OutletProvider (rendered as <Outlet /> content). Parallel
|
|
21
|
+
* slots are siblings of <Outlet />, so they can't find it by walking
|
|
22
|
+
* the parent context chain. This helper traverses wrapper elements
|
|
23
|
+
* (MountContextProvider, ViewTransition, etc.) to reach the OutletProvider
|
|
24
|
+
* and extract the loader data directly.
|
|
25
|
+
*/
|
|
26
|
+
const NOT_FOUND = Symbol("not-found");
|
|
27
|
+
|
|
28
|
+
function extractContentLoaderData(
|
|
29
|
+
node: ReactNode,
|
|
30
|
+
loaderId: string,
|
|
31
|
+
): unknown | typeof NOT_FOUND {
|
|
32
|
+
if (!isValidElement(node)) return NOT_FOUND;
|
|
33
|
+
const props = node.props as Record<string, any> | undefined;
|
|
34
|
+
if (!props) return NOT_FOUND;
|
|
35
|
+
|
|
36
|
+
// Direct OutletProvider with loaderData
|
|
37
|
+
if (props.loaderData && loaderId in props.loaderData) {
|
|
38
|
+
return props.loaderData[loaderId];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// LoaderBoundary: loaderIds + loaderDataPromise (already resolved array).
|
|
42
|
+
// When the segment has loading(), loaderData is resolved inside
|
|
43
|
+
// LoaderBoundary via use(). If the promise was pre-awaited (forceAwait
|
|
44
|
+
// or isAction), the prop is a raw array we can index into.
|
|
45
|
+
if (
|
|
46
|
+
props.loaderIds &&
|
|
47
|
+
Array.isArray(props.loaderIds) &&
|
|
48
|
+
props.loaderDataPromise &&
|
|
49
|
+
!(props.loaderDataPromise instanceof Promise)
|
|
50
|
+
) {
|
|
51
|
+
const idx = (props.loaderIds as string[]).indexOf(loaderId);
|
|
52
|
+
if (idx !== -1) {
|
|
53
|
+
const data = (props.loaderDataPromise as any[])[idx];
|
|
54
|
+
// loaderDataPromise entries may be { ok, data } result objects
|
|
55
|
+
if (data && typeof data === "object" && "ok" in data) {
|
|
56
|
+
return data.ok ? data.data : NOT_FOUND;
|
|
57
|
+
}
|
|
58
|
+
return data;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Traverse into wrapper elements (MountContextProvider, ViewTransition,
|
|
63
|
+
// Suspense wrappers, etc.)
|
|
64
|
+
if (props.children) return extractContentLoaderData(props.children, loaderId);
|
|
65
|
+
return NOT_FOUND;
|
|
66
|
+
}
|
|
67
|
+
|
|
7
68
|
/**
|
|
8
69
|
* Payload returned by loader RSC requests
|
|
9
70
|
*/
|
|
@@ -71,19 +132,27 @@ function useLoaderInternal<T>(
|
|
|
71
132
|
const context = useContext(OutletContext);
|
|
72
133
|
|
|
73
134
|
// Get data from context (SSR/navigation)
|
|
74
|
-
const
|
|
135
|
+
const contextData = useMemo((): T | undefined => {
|
|
75
136
|
let current: OutletContextValue | null | undefined = context;
|
|
76
137
|
while (current) {
|
|
77
138
|
if (current.loaderData && loader.$$id in current.loaderData) {
|
|
78
139
|
return current.loaderData[loader.$$id] as T;
|
|
79
140
|
}
|
|
141
|
+
// Check content element — the route's OutletProvider is rendered as
|
|
142
|
+
// <Outlet /> content (a child), so its loaderData isn't in the parent
|
|
143
|
+
// chain. Parallel slots need to reach into it to find route-level loaders.
|
|
144
|
+
const contentData = extractContentLoaderData(
|
|
145
|
+
current.content,
|
|
146
|
+
loader.$$id,
|
|
147
|
+
);
|
|
148
|
+
if (contentData !== NOT_FOUND) {
|
|
149
|
+
return contentData as T;
|
|
150
|
+
}
|
|
80
151
|
current = current.parent;
|
|
81
152
|
}
|
|
82
153
|
return undefined;
|
|
83
154
|
}, [context, loader.$$id]);
|
|
84
155
|
|
|
85
|
-
const contextData = getContextData();
|
|
86
|
-
|
|
87
156
|
// Local state for fetched data (from load() calls)
|
|
88
157
|
const [fetchedData, setFetchedData] = useState<T | undefined>(undefined);
|
|
89
158
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -31,25 +31,25 @@ export function postprocessBundle(state: DiscoveryState): void {
|
|
|
31
31
|
state.rscEntryFileName ?? "index.js",
|
|
32
32
|
);
|
|
33
33
|
|
|
34
|
-
// 1. Evict handler code from
|
|
35
|
-
//
|
|
34
|
+
// 1. Evict handler code from whichever chunks contain handler exports.
|
|
35
|
+
// handlerChunkInfoMap/staticHandlerChunkInfoMap are populated by generateBundle
|
|
36
36
|
// after the production RSC build. In Vite 6 multi-environment builds, the
|
|
37
|
-
// RSC build runs twice (analysis + production).
|
|
38
|
-
//
|
|
37
|
+
// RSC build runs twice (analysis + production). The maps are cleared at the
|
|
38
|
+
// start of each generateBundle pass so only production data is used here.
|
|
39
39
|
const evictionTargets: Array<{
|
|
40
|
-
|
|
40
|
+
infos: Iterable<import("./state.js").ChunkInfo>;
|
|
41
41
|
fnName: string;
|
|
42
42
|
brand: string;
|
|
43
43
|
label: string;
|
|
44
44
|
}> = [
|
|
45
45
|
{
|
|
46
|
-
|
|
46
|
+
infos: state.handlerChunkInfoMap.values(),
|
|
47
47
|
fnName: "Prerender",
|
|
48
48
|
brand: "prerenderHandler",
|
|
49
49
|
label: "handler code from RSC bundle",
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
|
-
|
|
52
|
+
infos: state.staticHandlerChunkInfoMap.values(),
|
|
53
53
|
fnName: "Static",
|
|
54
54
|
brand: "staticHandler",
|
|
55
55
|
label: "static handler code",
|
|
@@ -57,35 +57,32 @@ export function postprocessBundle(state: DiscoveryState): void {
|
|
|
57
57
|
];
|
|
58
58
|
|
|
59
59
|
for (const target of evictionTargets) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
60
|
+
for (const info of target.infos) {
|
|
61
|
+
const chunkPath = resolve(state.projectRoot, "dist/rsc", info.fileName);
|
|
62
|
+
try {
|
|
63
|
+
const code = readFileSync(chunkPath, "utf-8");
|
|
64
|
+
const result = evictHandlerCode(
|
|
65
|
+
code,
|
|
66
|
+
info.exports,
|
|
67
|
+
target.fnName,
|
|
68
|
+
target.brand,
|
|
69
|
+
);
|
|
70
|
+
if (result) {
|
|
71
|
+
writeFileSync(chunkPath, result.code);
|
|
72
|
+
const savedKB = (result.savedBytes / 1024).toFixed(1);
|
|
73
|
+
console.log(
|
|
74
|
+
`[rsc-router] Evicted ${target.label} (${savedKB} KB saved): ${info.fileName}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
} catch (replaceErr: any) {
|
|
78
|
+
console.warn(
|
|
79
|
+
`[rsc-router] Failed to evict ${target.label}: ${replaceErr.message}`,
|
|
79
80
|
);
|
|
80
81
|
}
|
|
81
|
-
} catch (replaceErr: any) {
|
|
82
|
-
console.warn(
|
|
83
|
-
`[rsc-router] Failed to evict ${target.label}: ${replaceErr.message}`,
|
|
84
|
-
);
|
|
85
82
|
}
|
|
86
83
|
}
|
|
87
|
-
state.
|
|
88
|
-
state.
|
|
84
|
+
state.handlerChunkInfoMap.clear();
|
|
85
|
+
state.staticHandlerChunkInfoMap.clear();
|
|
89
86
|
|
|
90
87
|
// 2. Write prerender data as separate importable asset modules
|
|
91
88
|
// and inject a lazy manifest loader into the RSC entry.
|
|
@@ -138,7 +135,7 @@ export function postprocessBundle(state: DiscoveryState): void {
|
|
|
138
135
|
// and inject a __STATIC_MANIFEST import into the RSC entry.
|
|
139
136
|
if (hasStaticData && existsSync(rscEntryPath)) {
|
|
140
137
|
const rscCode = readFileSync(rscEntryPath, "utf-8");
|
|
141
|
-
if (!rscCode.includes("
|
|
138
|
+
if (!rscCode.includes("__static-manifest.js")) {
|
|
142
139
|
try {
|
|
143
140
|
const manifestEntries: string[] = [];
|
|
144
141
|
let totalBytes = copyStagedBuildAssets(
|
|
@@ -54,11 +54,12 @@ export async function expandPrerenderRoutes(
|
|
|
54
54
|
for (const { manifest } of allManifests) {
|
|
55
55
|
if (!manifest.prerenderRoutes) continue;
|
|
56
56
|
const defs = manifest._prerenderDefs || {};
|
|
57
|
+
const passthroughSet = new Set(manifest.passthroughRoutes || []);
|
|
57
58
|
for (const routeName of manifest.prerenderRoutes) {
|
|
58
59
|
const pattern = manifest.routeManifest[routeName];
|
|
59
60
|
if (!pattern) continue;
|
|
60
61
|
const def = defs[routeName];
|
|
61
|
-
const isPassthroughRoute =
|
|
62
|
+
const isPassthroughRoute = passthroughSet.has(routeName);
|
|
62
63
|
const hasDynamic = pattern.includes(":") || pattern.includes("*");
|
|
63
64
|
if (!hasDynamic) {
|
|
64
65
|
// Static route: use pattern directly (strip trailing slash for URL)
|
|
@@ -73,12 +74,21 @@ export async function expandPrerenderRoutes(
|
|
|
73
74
|
if (def?.getParams) {
|
|
74
75
|
try {
|
|
75
76
|
const buildVars: Record<string, any> = {};
|
|
77
|
+
const buildEnv = state.resolvedBuildEnv;
|
|
76
78
|
const getParamsCtx = {
|
|
77
79
|
build: true as const,
|
|
80
|
+
dev: !state.isBuildMode,
|
|
78
81
|
set: ((keyOrVar: any, value: any) => {
|
|
79
82
|
contextSet(buildVars, keyOrVar, value);
|
|
80
83
|
}) as any,
|
|
81
84
|
reverse: getParamsReverse,
|
|
85
|
+
get env() {
|
|
86
|
+
if (buildEnv !== undefined) return buildEnv;
|
|
87
|
+
throw new Error(
|
|
88
|
+
"[rsc-router] ctx.env is not available during build-time getParams(). " +
|
|
89
|
+
"Configure buildEnv in your rango() plugin options to enable build-time env access.",
|
|
90
|
+
);
|
|
91
|
+
},
|
|
82
92
|
};
|
|
83
93
|
const paramsList = await def.getParams(getParamsCtx);
|
|
84
94
|
const concurrency = def.options?.concurrency ?? 1;
|
|
@@ -175,6 +185,7 @@ export async function expandPrerenderRoutes(
|
|
|
175
185
|
{},
|
|
176
186
|
entry.buildVars,
|
|
177
187
|
entry.isPassthroughRoute,
|
|
188
|
+
state.resolvedBuildEnv,
|
|
178
189
|
);
|
|
179
190
|
if (!result) continue;
|
|
180
191
|
|
|
@@ -326,6 +337,8 @@ export async function renderStaticHandlers(
|
|
|
326
337
|
def.handler,
|
|
327
338
|
def.$$id,
|
|
328
339
|
(def as any).$$routePrefix,
|
|
340
|
+
state.resolvedBuildEnv,
|
|
341
|
+
!state.isBuildMode,
|
|
329
342
|
);
|
|
330
343
|
if (result) {
|
|
331
344
|
const hasHandles = Object.keys(result.handles).length > 0;
|
|
@@ -16,6 +16,10 @@ export interface PluginOptions {
|
|
|
16
16
|
// Mutable ref for deferred auto-discovery (node preset).
|
|
17
17
|
// The auto-discover config() hook populates this before configResolved.
|
|
18
18
|
routerPathRef?: { path?: string };
|
|
19
|
+
/** Build-time env option from rango() config. */
|
|
20
|
+
buildEnv?: import("../plugin-types.js").BuildEnvOption;
|
|
21
|
+
/** Deployment preset (needed for buildEnv "auto" resolution). */
|
|
22
|
+
preset?: "node" | "cloudflare";
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
export interface PrecomputedEntry {
|
|
@@ -56,8 +60,8 @@ export interface DiscoveryState {
|
|
|
56
60
|
|
|
57
61
|
prerenderManifestEntries: Record<string, string> | null;
|
|
58
62
|
staticManifestEntries: Record<string, string> | null;
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
handlerChunkInfoMap: Map<string, ChunkInfo>;
|
|
64
|
+
staticHandlerChunkInfoMap: Map<string, ChunkInfo>;
|
|
61
65
|
rscEntryFileName: string | null;
|
|
62
66
|
resolvedPrerenderModules: Map<string, string[]> | undefined;
|
|
63
67
|
resolvedStaticModules: Map<string, string[]> | undefined;
|
|
@@ -67,6 +71,11 @@ export interface DiscoveryState {
|
|
|
67
71
|
devServer: any;
|
|
68
72
|
selfWrittenGenFiles: Map<string, { at: number; hash: string }>;
|
|
69
73
|
SELF_WRITE_WINDOW_MS: number;
|
|
74
|
+
|
|
75
|
+
/** Resolved build-time env bindings (set during buildStart/configureServer). */
|
|
76
|
+
resolvedBuildEnv?: Record<string, unknown>;
|
|
77
|
+
/** Cleanup function for build-time env resources (e.g., miniflare). */
|
|
78
|
+
buildEnvDispose?: (() => Promise<void> | void) | null;
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
export function createDiscoveryState(
|
|
@@ -93,8 +102,8 @@ export function createDiscoveryState(
|
|
|
93
102
|
|
|
94
103
|
prerenderManifestEntries: null,
|
|
95
104
|
staticManifestEntries: null,
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
handlerChunkInfoMap: new Map(),
|
|
106
|
+
staticHandlerChunkInfoMap: new Map(),
|
|
98
107
|
rscEntryFileName: null,
|
|
99
108
|
resolvedPrerenderModules: undefined,
|
|
100
109
|
resolvedStaticModules: undefined,
|