@rangojs/router 0.0.0-experimental.7dc955ec → 0.0.0-experimental.80
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 +76 -18
- package/dist/bin/rango.js +130 -47
- package/dist/vite/index.js +700 -236
- package/package.json +3 -3
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +3 -1
- package/skills/loader/SKILL.md +53 -43
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +764 -0
- package/skills/parallel/SKILL.md +59 -0
- package/skills/prerender/SKILL.md +110 -68
- package/skills/rango/SKILL.md +24 -22
- package/skills/route/SKILL.md +24 -0
- package/skills/router-setup/SKILL.md +87 -2
- package/src/__internal.ts +1 -1
- package/src/browser/app-version.ts +14 -0
- package/src/browser/navigation-bridge.ts +37 -5
- package/src/browser/navigation-client.ts +98 -46
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/partial-update.ts +41 -7
- package/src/browser/prefetch/cache.ts +16 -6
- package/src/browser/prefetch/fetch.ts +68 -6
- package/src/browser/prefetch/queue.ts +61 -29
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +67 -8
- package/src/browser/react/NavigationProvider.tsx +13 -4
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-router.ts +21 -8
- package/src/browser/rsc-router.tsx +26 -3
- package/src/browser/scroll-restoration.ts +10 -8
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +27 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-trie.ts +50 -24
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +211 -72
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/client.tsx +84 -230
- package/src/handle.ts +40 -0
- package/src/index.rsc.ts +3 -1
- package/src/index.ts +46 -6
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/reverse.ts +25 -1
- package/src/route-definition/dsl-helpers.ts +194 -32
- package/src/route-definition/helpers-types.ts +67 -19
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +9 -1
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-types.ts +18 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/handler-context.ts +51 -15
- package/src/router/intercept-resolution.ts +9 -4
- package/src/router/lazy-includes.ts +5 -5
- package/src/router/loader-resolution.ts +156 -21
- package/src/router/manifest.ts +22 -13
- package/src/router/match-api.ts +124 -189
- package/src/router/match-middleware/cache-lookup.ts +28 -8
- package/src/router/match-middleware/segment-resolution.ts +53 -0
- package/src/router/match-result.ts +82 -4
- package/src/router/middleware-types.ts +0 -6
- package/src/router/middleware.ts +0 -3
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/prerender-match.ts +110 -10
- 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 +36 -4
- package/src/router/router-options.ts +37 -11
- package/src/router/segment-resolution/fresh.ts +71 -17
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/revalidation.ts +87 -18
- package/src/router/types.ts +1 -0
- package/src/router.ts +54 -5
- package/src/rsc/handler.ts +472 -372
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +14 -2
- package/src/rsc/rsc-rendering.ts +10 -1
- package/src/rsc/server-action.ts +8 -0
- package/src/rsc/ssr-setup.ts +2 -2
- package/src/rsc/types.ts +9 -1
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +11 -61
- package/src/server/context.ts +65 -5
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +134 -9
- package/src/ssr/index.tsx +3 -0
- package/src/static-handler.ts +18 -6
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +30 -20
- package/src/types/loader-types.ts +36 -9
- package/src/types/route-entry.ts +12 -1
- package/src/types/segments.ts +1 -1
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +39 -6
- package/src/urls/path-helper.ts +47 -12
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +16 -6
- package/src/use-loader.tsx +77 -5
- package/src/vite/discovery/bundle-postprocess.ts +30 -33
- package/src/vite/discovery/discover-routers.ts +5 -1
- package/src/vite/discovery/prerender-collection.ts +128 -74
- 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/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
- package/src/vite/plugins/expose-internal-ids.ts +257 -40
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/rango.ts +19 -2
- package/src/vite/router-discovery.ts +178 -37
- package/src/vite/utils/prerender-utils.ts +37 -5
- package/src/vite/utils/shared-utils.ts +3 -2
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
import type { NonceProvider } from "../rsc/types.js";
|
|
9
9
|
import type { ExecutionContext } from "../server/request-context.js";
|
|
10
10
|
import type { UrlPatterns } from "../urls.js";
|
|
11
|
+
import type { UrlBuilder } from "../urls/pattern-types.js";
|
|
11
12
|
import type { NamedRouteEntry } from "./content-negotiation.js";
|
|
12
13
|
import type { TelemetrySink } from "./telemetry.js";
|
|
13
14
|
import type { RouterTimeouts, OnTimeoutCallback } from "./timeout.js";
|
|
@@ -95,6 +96,28 @@ export interface RSCRouterOptions<TEnv = any> {
|
|
|
95
96
|
*/
|
|
96
97
|
$$sourceFile?: string;
|
|
97
98
|
|
|
99
|
+
/**
|
|
100
|
+
* URL prefix applied to all routes registered with this router.
|
|
101
|
+
*
|
|
102
|
+
* Useful when the app is served under a sub-path (e.g. `/admin` or `/v2`).
|
|
103
|
+
* All `path()` patterns are automatically prefixed and `reverse()` returns
|
|
104
|
+
* full paths including the basename. Route names are NOT prefixed.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* const router = createRouter({
|
|
109
|
+
* basename: "/admin",
|
|
110
|
+
* }).routes(({ path }) => [
|
|
111
|
+
* path("/", Dashboard, { name: "home" }), // matches /admin
|
|
112
|
+
* path("/users", Users, { name: "users" }), // matches /admin/users
|
|
113
|
+
* ]);
|
|
114
|
+
*
|
|
115
|
+
* router.reverse("home"); // "/admin"
|
|
116
|
+
* router.reverse("users"); // "/admin/users"
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
basename?: string;
|
|
120
|
+
|
|
98
121
|
/**
|
|
99
122
|
* Enable performance metrics collection
|
|
100
123
|
* When enabled, metrics are output to console and available via Server-Timing header
|
|
@@ -337,25 +360,28 @@ export interface RSCRouterOptions<TEnv = any> {
|
|
|
337
360
|
/**
|
|
338
361
|
* URL patterns to register with the router.
|
|
339
362
|
*
|
|
340
|
-
*
|
|
341
|
-
* directly
|
|
363
|
+
* Accepts either a `UrlPatterns` object from `urls()` or a builder function
|
|
364
|
+
* directly (urls() is called implicitly).
|
|
342
365
|
*
|
|
343
366
|
* @example
|
|
344
367
|
* ```typescript
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
* const urlpatterns = urls(({ path, layout }) => [
|
|
348
|
-
* path("/", HomePage, { name: "home" }),
|
|
349
|
-
* path("/about", AboutPage, { name: "about" }),
|
|
350
|
-
* ]);
|
|
351
|
-
*
|
|
352
|
-
* const router = createRouter<AppEnv>({
|
|
368
|
+
* // With urls()
|
|
369
|
+
* createRouter<AppEnv>({
|
|
353
370
|
* document: Document,
|
|
354
371
|
* urls: urlpatterns,
|
|
355
372
|
* });
|
|
373
|
+
*
|
|
374
|
+
* // With builder function
|
|
375
|
+
* createRouter<AppEnv>({
|
|
376
|
+
* document: Document,
|
|
377
|
+
* urls: ({ path }) => [
|
|
378
|
+
* path("/", HomePage, { name: "home" }),
|
|
379
|
+
* path("/about", AboutPage, { name: "about" }),
|
|
380
|
+
* ],
|
|
381
|
+
* });
|
|
356
382
|
* ```
|
|
357
383
|
*/
|
|
358
|
-
urls?: UrlPatterns<TEnv, any>;
|
|
384
|
+
urls?: UrlPatterns<TEnv, any> | UrlBuilder<TEnv>;
|
|
359
385
|
|
|
360
386
|
/**
|
|
361
387
|
* Injected by the Vite transform at compile time.
|
|
@@ -30,7 +30,11 @@ import {
|
|
|
30
30
|
} from "./helpers.js";
|
|
31
31
|
import { getRouterContext } from "../router-context.js";
|
|
32
32
|
import { resolveSink, safeEmit } from "../telemetry.js";
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
track,
|
|
35
|
+
RSCRouterContext,
|
|
36
|
+
runInsideLoaderScope,
|
|
37
|
+
} from "../../server/context.js";
|
|
34
38
|
|
|
35
39
|
// ---------------------------------------------------------------------------
|
|
36
40
|
// Streamed handler telemetry
|
|
@@ -93,14 +97,6 @@ export async function resolveLoaders<TEnv>(
|
|
|
93
97
|
const loaderEntries = entry.loader ?? [];
|
|
94
98
|
if (loaderEntries.length === 0) return [];
|
|
95
99
|
|
|
96
|
-
// DSL loaders are always fresh (never cached), so temporarily clear the
|
|
97
|
-
// cache scope flag. This allows loaders to read non-cacheable vars even
|
|
98
|
-
// inside cache() boundaries. Handler ctx.use(loader) does NOT get this
|
|
99
|
-
// exemption — the handler is cached, so its loader results are too.
|
|
100
|
-
const store = RSCRouterContext.getStore();
|
|
101
|
-
const savedCacheScope = store?.insideCacheScope;
|
|
102
|
-
if (store) store.insideCacheScope = false;
|
|
103
|
-
|
|
104
100
|
const shortCode = shortCodeOverride ?? entry.shortCode;
|
|
105
101
|
const hasLoading = "loading" in entry && entry.loading !== undefined;
|
|
106
102
|
const loadingDisabled = hasLoading && entry.loading === false;
|
|
@@ -120,7 +116,9 @@ export async function resolveLoaders<TEnv>(
|
|
|
120
116
|
params: ctx.params,
|
|
121
117
|
loaderId: loader.$$id,
|
|
122
118
|
loaderData: deps.wrapLoaderPromise(
|
|
123
|
-
|
|
119
|
+
runInsideLoaderScope(() =>
|
|
120
|
+
resolveLoaderData(loaderEntry, ctx, ctx.pathname),
|
|
121
|
+
),
|
|
124
122
|
entry,
|
|
125
123
|
segmentId,
|
|
126
124
|
ctx.pathname,
|
|
@@ -128,8 +126,7 @@ export async function resolveLoaders<TEnv>(
|
|
|
128
126
|
belongsToRoute,
|
|
129
127
|
};
|
|
130
128
|
});
|
|
131
|
-
|
|
132
|
-
if (store) store.insideCacheScope = savedCacheScope;
|
|
129
|
+
|
|
133
130
|
return segments;
|
|
134
131
|
}
|
|
135
132
|
|
|
@@ -137,11 +134,11 @@ export async function resolveLoaders<TEnv>(
|
|
|
137
134
|
// settled promises so handlers don't stream loading placeholders.
|
|
138
135
|
const pendingLoaderData = loaderEntries.map((loaderEntry) => {
|
|
139
136
|
const start = performance.now();
|
|
140
|
-
const promise =
|
|
137
|
+
const promise = runInsideLoaderScope(() =>
|
|
138
|
+
resolveLoaderData(loaderEntry, ctx, ctx.pathname),
|
|
139
|
+
);
|
|
141
140
|
return { promise, start, loaderId: loaderEntry.loader.$$id };
|
|
142
141
|
});
|
|
143
|
-
// Restore cache scope after all loader promises are kicked off
|
|
144
|
-
if (store) store.insideCacheScope = savedCacheScope;
|
|
145
142
|
await Promise.all(pendingLoaderData.map((p) => p.promise));
|
|
146
143
|
|
|
147
144
|
return loaderEntries.map((loaderEntry, i) => {
|
|
@@ -287,9 +284,14 @@ export async function resolveSegment<TEnv>(
|
|
|
287
284
|
entry.shortCode,
|
|
288
285
|
);
|
|
289
286
|
if (component === undefined) {
|
|
287
|
+
// For Passthrough routes at runtime, use the live handler instead of
|
|
288
|
+
// the build handler. At build time (context.build === true), always
|
|
289
|
+
// use the build handler from entry.handler.
|
|
290
|
+
const handler =
|
|
291
|
+
!context.build && entry.liveHandler ? entry.liveHandler : entry.handler;
|
|
290
292
|
const doneRouteHandler = track(`handler:${entry.id}`, 2);
|
|
291
293
|
if (entry.loading) {
|
|
292
|
-
const result = handleHandlerResult(
|
|
294
|
+
const result = handleHandlerResult(handler(context));
|
|
293
295
|
if (result instanceof Promise) {
|
|
294
296
|
result.finally(doneRouteHandler).catch(() => {});
|
|
295
297
|
const tracked = deps.trackHandler(result, {
|
|
@@ -310,7 +312,7 @@ export async function resolveSegment<TEnv>(
|
|
|
310
312
|
component = result;
|
|
311
313
|
}
|
|
312
314
|
} else {
|
|
313
|
-
component = handleHandlerResult(await
|
|
315
|
+
component = handleHandlerResult(await handler(context));
|
|
314
316
|
doneRouteHandler();
|
|
315
317
|
}
|
|
316
318
|
}
|
|
@@ -325,6 +327,7 @@ export async function resolveSegment<TEnv>(
|
|
|
325
327
|
deps,
|
|
326
328
|
options,
|
|
327
329
|
routeKey,
|
|
330
|
+
entry,
|
|
328
331
|
);
|
|
329
332
|
segments.push(...orphanSegments);
|
|
330
333
|
}
|
|
@@ -380,6 +383,9 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
380
383
|
deps: SegmentResolutionDeps<TEnv>,
|
|
381
384
|
options?: ResolveSegmentOptions,
|
|
382
385
|
routeKey?: string,
|
|
386
|
+
/** Parent route entry — its loaders are inherited by the layout so
|
|
387
|
+
* parallel slots inside this layout can access them via useLoader(). */
|
|
388
|
+
parentRouteEntry?: EntryData,
|
|
383
389
|
): Promise<ResolvedSegment[]> {
|
|
384
390
|
invariant(
|
|
385
391
|
orphan.type === "layout" || orphan.type === "cache",
|
|
@@ -395,6 +401,30 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
395
401
|
deps,
|
|
396
402
|
);
|
|
397
403
|
segments.push(...loaderSegments);
|
|
404
|
+
|
|
405
|
+
// Inherit parent route's loaders so parallel slots inside this layout
|
|
406
|
+
// can access them via useLoader(). Without this, the route's loaders
|
|
407
|
+
// are only in the route's OutletProvider (rendered as <Outlet /> content),
|
|
408
|
+
// which is a child — not a parent — of the layout's context.
|
|
409
|
+
if (
|
|
410
|
+
parentRouteEntry &&
|
|
411
|
+
parentRouteEntry.loader &&
|
|
412
|
+
parentRouteEntry.loader.length > 0 &&
|
|
413
|
+
Object.keys(orphan.parallel).length > 0
|
|
414
|
+
) {
|
|
415
|
+
const inheritedLoaders = await resolveLoaders(
|
|
416
|
+
parentRouteEntry,
|
|
417
|
+
context,
|
|
418
|
+
belongsToRoute,
|
|
419
|
+
deps,
|
|
420
|
+
orphan.shortCode,
|
|
421
|
+
);
|
|
422
|
+
// Tag as inherited so buildMatchResult can deduplicate when safe
|
|
423
|
+
for (const s of inheritedLoaders) {
|
|
424
|
+
s._inherited = true;
|
|
425
|
+
}
|
|
426
|
+
segments.push(...inheritedLoaders);
|
|
427
|
+
}
|
|
398
428
|
}
|
|
399
429
|
|
|
400
430
|
// Handler-first: orphan layout handler executes before its parallels
|
|
@@ -683,6 +713,30 @@ export async function resolveLoadersOnly<TEnv>(
|
|
|
683
713
|
const childBelongsToRoute = belongsToRoute || entry.type === "route";
|
|
684
714
|
for (const layoutEntry of entry.layout) {
|
|
685
715
|
await collectEntryLoaders(layoutEntry, childBelongsToRoute);
|
|
716
|
+
// Inherit route loaders for orphan layouts with parallels.
|
|
717
|
+
// Resolve directly — do NOT re-enter collectEntryLoaders with the
|
|
718
|
+
// route entry, as that would re-iterate route.layout and loop.
|
|
719
|
+
if (
|
|
720
|
+
entry.type === "route" &&
|
|
721
|
+
entry.loader &&
|
|
722
|
+
entry.loader.length > 0 &&
|
|
723
|
+
Object.keys(layoutEntry.parallel).length > 0
|
|
724
|
+
) {
|
|
725
|
+
const inherited = await resolveLoaders(
|
|
726
|
+
entry,
|
|
727
|
+
context,
|
|
728
|
+
childBelongsToRoute,
|
|
729
|
+
deps,
|
|
730
|
+
layoutEntry.shortCode,
|
|
731
|
+
);
|
|
732
|
+
for (const seg of inherited) {
|
|
733
|
+
if (!seenIds.has(seg.id)) {
|
|
734
|
+
seenIds.add(seg.id);
|
|
735
|
+
seg._inherited = true;
|
|
736
|
+
loaderSegments.push(seg);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
686
740
|
}
|
|
687
741
|
}
|
|
688
742
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Error boundary segment creation
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type
|
|
11
|
+
import { createElement, type ReactNode } from "react";
|
|
12
12
|
import { DataNotFoundError } from "../../errors";
|
|
13
13
|
import {
|
|
14
14
|
createErrorInfo,
|
|
@@ -180,34 +180,39 @@ export function catchSegmentError<TEnv>(
|
|
|
180
180
|
|
|
181
181
|
if (error instanceof DataNotFoundError) {
|
|
182
182
|
const notFoundFallback = deps.findNearestNotFoundBoundary(entry);
|
|
183
|
+
// Fall back to router's notFound component, then a plain default
|
|
184
|
+
const notFoundOption = deps.notFoundComponent;
|
|
185
|
+
const defaultFallback =
|
|
186
|
+
typeof notFoundOption === "function"
|
|
187
|
+
? notFoundOption({ pathname: pathname ?? "" })
|
|
188
|
+
: (notFoundOption ?? createElement("h1", null, "Not Found"));
|
|
189
|
+
const effectiveNotFoundFallback = notFoundFallback ?? defaultFallback;
|
|
183
190
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
);
|
|
191
|
+
const notFoundInfo = createNotFoundInfo(
|
|
192
|
+
error,
|
|
193
|
+
entry.shortCode,
|
|
194
|
+
entry.type,
|
|
195
|
+
pathname,
|
|
196
|
+
);
|
|
191
197
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
198
|
+
reportError(true, {
|
|
199
|
+
notFound: true,
|
|
200
|
+
message: notFoundInfo.message,
|
|
201
|
+
});
|
|
196
202
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
203
|
+
debugLog("segment", "notFound boundary handled error", {
|
|
204
|
+
segmentId: entry.shortCode,
|
|
205
|
+
message: notFoundInfo.message,
|
|
206
|
+
});
|
|
201
207
|
|
|
202
|
-
|
|
208
|
+
setResponseStatus(404);
|
|
203
209
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
210
|
+
return createNotFoundSegment(
|
|
211
|
+
notFoundInfo,
|
|
212
|
+
effectiveNotFoundFallback,
|
|
213
|
+
entry,
|
|
214
|
+
params,
|
|
215
|
+
);
|
|
211
216
|
}
|
|
212
217
|
|
|
213
218
|
const fallback = deps.findNearestErrorBoundary(entry);
|
|
@@ -41,8 +41,11 @@ import {
|
|
|
41
41
|
} from "./helpers.js";
|
|
42
42
|
import { getRouterContext } from "../router-context.js";
|
|
43
43
|
import { resolveSink, safeEmit } from "../telemetry.js";
|
|
44
|
-
import {
|
|
45
|
-
|
|
44
|
+
import {
|
|
45
|
+
track,
|
|
46
|
+
RSCRouterContext,
|
|
47
|
+
runInsideLoaderScope,
|
|
48
|
+
} from "../../server/context.js";
|
|
46
49
|
|
|
47
50
|
// ---------------------------------------------------------------------------
|
|
48
51
|
// Telemetry helpers
|
|
@@ -146,12 +149,6 @@ export async function resolveLoadersWithRevalidation<TEnv>(
|
|
|
146
149
|
const loaderEntries = entry.loader ?? [];
|
|
147
150
|
if (loaderEntries.length === 0) return { segments: [], matchedIds: [] };
|
|
148
151
|
|
|
149
|
-
// DSL loaders are always fresh — temporarily clear cache scope
|
|
150
|
-
// so non-cacheable var reads are allowed inside loader functions.
|
|
151
|
-
const store = RSCRouterContext.getStore();
|
|
152
|
-
const savedCacheScope = store?.insideCacheScope;
|
|
153
|
-
if (store) store.insideCacheScope = false;
|
|
154
|
-
|
|
155
152
|
const shortCode = shortCodeOverride ?? entry.shortCode;
|
|
156
153
|
|
|
157
154
|
const loaderMeta = loaderEntries.map((loaderEntry, i) => ({
|
|
@@ -239,7 +236,9 @@ export async function resolveLoadersWithRevalidation<TEnv>(
|
|
|
239
236
|
params: ctx.params,
|
|
240
237
|
loaderId: loader.$$id,
|
|
241
238
|
loaderData: deps.wrapLoaderPromise(
|
|
242
|
-
|
|
239
|
+
runInsideLoaderScope(() =>
|
|
240
|
+
resolveLoaderData(loaderEntry, ctx, ctx.pathname),
|
|
241
|
+
),
|
|
243
242
|
entry,
|
|
244
243
|
segmentId,
|
|
245
244
|
ctx.pathname,
|
|
@@ -248,9 +247,6 @@ export async function resolveLoadersWithRevalidation<TEnv>(
|
|
|
248
247
|
}),
|
|
249
248
|
);
|
|
250
249
|
|
|
251
|
-
// Restore cache scope after all loader promises are kicked off
|
|
252
|
-
if (store) store.insideCacheScope = savedCacheScope;
|
|
253
|
-
|
|
254
250
|
return { segments, matchedIds };
|
|
255
251
|
}
|
|
256
252
|
|
|
@@ -323,6 +319,39 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
|
|
|
323
319
|
const childBelongsToRoute = belongsToRoute || entry.type === "route";
|
|
324
320
|
for (const layoutEntry of entry.layout) {
|
|
325
321
|
await collectEntryLoaders(layoutEntry, childBelongsToRoute);
|
|
322
|
+
// Inherit route loaders for orphan layouts with parallels.
|
|
323
|
+
// Resolve directly — do NOT re-enter collectEntryLoaders with the
|
|
324
|
+
// route entry, as that would re-iterate route.layout and loop.
|
|
325
|
+
if (
|
|
326
|
+
entry.type === "route" &&
|
|
327
|
+
entry.loader &&
|
|
328
|
+
entry.loader.length > 0 &&
|
|
329
|
+
Object.keys(layoutEntry.parallel).length > 0
|
|
330
|
+
) {
|
|
331
|
+
const inherited = await resolveLoadersWithRevalidation(
|
|
332
|
+
entry,
|
|
333
|
+
context,
|
|
334
|
+
childBelongsToRoute,
|
|
335
|
+
clientSegmentIds,
|
|
336
|
+
prevParams,
|
|
337
|
+
request,
|
|
338
|
+
prevUrl,
|
|
339
|
+
nextUrl,
|
|
340
|
+
routeKey,
|
|
341
|
+
deps,
|
|
342
|
+
actionContext,
|
|
343
|
+
layoutEntry.shortCode,
|
|
344
|
+
stale,
|
|
345
|
+
);
|
|
346
|
+
for (const seg of inherited.segments) {
|
|
347
|
+
if (!seenIds.has(seg.id)) {
|
|
348
|
+
seenIds.add(seg.id);
|
|
349
|
+
seg._inherited = true;
|
|
350
|
+
allLoaderSegments.push(seg);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
allMatchedIds.push(...inherited.matchedIds);
|
|
354
|
+
}
|
|
326
355
|
}
|
|
327
356
|
}
|
|
328
357
|
|
|
@@ -692,13 +721,20 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
692
721
|
return staticComponent;
|
|
693
722
|
}
|
|
694
723
|
const routeEntry = entry as Extract<EntryData, { type: "route" }>;
|
|
724
|
+
// For Passthrough routes at runtime, use the live handler instead of
|
|
725
|
+
// the build handler. At build time (context.build === true), always
|
|
726
|
+
// use the build handler from routeEntry.handler.
|
|
727
|
+
const handler =
|
|
728
|
+
!context.build && routeEntry.liveHandler
|
|
729
|
+
? routeEntry.liveHandler
|
|
730
|
+
: routeEntry.handler;
|
|
695
731
|
if (!routeEntry.loading) {
|
|
696
|
-
const result = handleHandlerResult(await
|
|
732
|
+
const result = handleHandlerResult(await handler(context));
|
|
697
733
|
doneHandler();
|
|
698
734
|
return result;
|
|
699
735
|
}
|
|
700
736
|
if (!actionContext) {
|
|
701
|
-
const result = handleHandlerResult(
|
|
737
|
+
const result = handleHandlerResult(handler(context));
|
|
702
738
|
if (result instanceof Promise) {
|
|
703
739
|
result.finally(doneHandler).catch(() => {});
|
|
704
740
|
const tracked = deps.trackHandler(result, {
|
|
@@ -721,9 +757,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
721
757
|
debugLog("segment.action", "resolving action route with awaited value", {
|
|
722
758
|
entryId: entry.id,
|
|
723
759
|
});
|
|
724
|
-
const actionResult = handleHandlerResult(
|
|
725
|
-
await routeEntry.handler(context),
|
|
726
|
-
);
|
|
760
|
+
const actionResult = handleHandlerResult(await handler(context));
|
|
727
761
|
doneHandler();
|
|
728
762
|
return {
|
|
729
763
|
content: Promise.resolve(actionResult),
|
|
@@ -839,6 +873,7 @@ export async function resolveSegmentWithRevalidation<TEnv>(
|
|
|
839
873
|
deps,
|
|
840
874
|
actionContext,
|
|
841
875
|
stale,
|
|
876
|
+
entry,
|
|
842
877
|
);
|
|
843
878
|
segments.push(...orphanResult.segments);
|
|
844
879
|
matchedIds.push(...orphanResult.matchedIds);
|
|
@@ -950,6 +985,8 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
950
985
|
deps: SegmentResolutionDeps<TEnv>,
|
|
951
986
|
actionContext?: ActionContext,
|
|
952
987
|
stale?: boolean,
|
|
988
|
+
/** Parent route entry — its loaders are inherited so parallel slots can access them. */
|
|
989
|
+
parentRouteEntry?: EntryData,
|
|
953
990
|
): Promise<SegmentRevalidationResult> {
|
|
954
991
|
invariant(
|
|
955
992
|
orphan.type === "layout" || orphan.type === "cache",
|
|
@@ -977,6 +1014,37 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
977
1014
|
segments.push(...loaderResult.segments);
|
|
978
1015
|
matchedIds.push(...loaderResult.matchedIds);
|
|
979
1016
|
|
|
1017
|
+
// Inherit parent route's loaders so parallel slots inside this layout
|
|
1018
|
+
// can access them via useLoader(). See resolveOrphanLayout in fresh.ts.
|
|
1019
|
+
if (
|
|
1020
|
+
parentRouteEntry &&
|
|
1021
|
+
parentRouteEntry.loader &&
|
|
1022
|
+
parentRouteEntry.loader.length > 0 &&
|
|
1023
|
+
Object.keys(orphan.parallel).length > 0
|
|
1024
|
+
) {
|
|
1025
|
+
const inheritedResult = await resolveLoadersWithRevalidation(
|
|
1026
|
+
parentRouteEntry,
|
|
1027
|
+
context,
|
|
1028
|
+
belongsToRoute,
|
|
1029
|
+
clientSegmentIds,
|
|
1030
|
+
prevParams,
|
|
1031
|
+
request,
|
|
1032
|
+
prevUrl,
|
|
1033
|
+
nextUrl,
|
|
1034
|
+
routeKey,
|
|
1035
|
+
deps,
|
|
1036
|
+
actionContext,
|
|
1037
|
+
orphan.shortCode,
|
|
1038
|
+
stale,
|
|
1039
|
+
);
|
|
1040
|
+
// Tag as inherited so buildMatchResult can deduplicate when safe
|
|
1041
|
+
for (const s of inheritedResult.segments) {
|
|
1042
|
+
s._inherited = true;
|
|
1043
|
+
}
|
|
1044
|
+
segments.push(...inheritedResult.segments);
|
|
1045
|
+
matchedIds.push(...inheritedResult.matchedIds);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
980
1048
|
// Handler-first: resolve orphan layout handler before its parallels
|
|
981
1049
|
// so ctx.set() values are visible to parallel children.
|
|
982
1050
|
matchedIds.push(orphan.shortCode);
|
|
@@ -1063,6 +1131,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1063
1131
|
);
|
|
1064
1132
|
|
|
1065
1133
|
if (!resolvedParallelEntries.has(parallelEntry.id)) {
|
|
1134
|
+
// shortCodeOverride must match the parent layout, not the parallel entry.
|
|
1066
1135
|
const loaderResult = await resolveLoadersWithRevalidation(
|
|
1067
1136
|
parallelEntry,
|
|
1068
1137
|
context,
|
|
@@ -1075,7 +1144,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1075
1144
|
routeKey,
|
|
1076
1145
|
deps,
|
|
1077
1146
|
actionContext,
|
|
1078
|
-
|
|
1147
|
+
orphan.shortCode,
|
|
1079
1148
|
stale,
|
|
1080
1149
|
);
|
|
1081
1150
|
segments.push(...loaderResult.segments);
|
package/src/router/types.ts
CHANGED
|
@@ -96,6 +96,7 @@ export interface SegmentResolutionDeps<TEnv = any> {
|
|
|
96
96
|
findNearestNotFoundBoundary: (
|
|
97
97
|
entry: EntryData | null,
|
|
98
98
|
) => ReactNode | NotFoundBoundaryHandler | null;
|
|
99
|
+
notFoundComponent?: ReactNode | ((props: { pathname: string }) => ReactNode);
|
|
99
100
|
callOnError: (error: unknown, phase: ErrorPhase, context: any) => void;
|
|
100
101
|
}
|
|
101
102
|
|
package/src/router.ts
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
import MapRootLayout from "./server/root-layout.js";
|
|
20
20
|
import type { AllUseItems } from "./route-types.js";
|
|
21
21
|
import type { UrlPatterns } from "./urls.js";
|
|
22
|
+
import type { UrlBuilder } from "./urls/pattern-types.js";
|
|
23
|
+
import { urls } from "./urls.js";
|
|
22
24
|
import {
|
|
23
25
|
EntryData,
|
|
24
26
|
InterceptSelectorContext,
|
|
@@ -133,6 +135,7 @@ export function createRouter<TEnv = any>(
|
|
|
133
135
|
const {
|
|
134
136
|
id: userProvidedId,
|
|
135
137
|
$$id: injectedId,
|
|
138
|
+
basename: basenameOption,
|
|
136
139
|
debugPerformance = false,
|
|
137
140
|
document: documentOption,
|
|
138
141
|
defaultErrorBoundary,
|
|
@@ -158,6 +161,13 @@ export function createRouter<TEnv = any>(
|
|
|
158
161
|
originCheck: originCheckOption,
|
|
159
162
|
} = options;
|
|
160
163
|
|
|
164
|
+
// Normalize basename: ensure leading slash, strip trailing slash.
|
|
165
|
+
// A bare "/" is equivalent to no basename.
|
|
166
|
+
const basename =
|
|
167
|
+
basenameOption && basenameOption.replace(/^\/+|\/+$/g, "")
|
|
168
|
+
? "/" + basenameOption.replace(/^\/+|\/+$/g, "")
|
|
169
|
+
: undefined;
|
|
170
|
+
|
|
161
171
|
// Resolve telemetry sink (no-op when not configured)
|
|
162
172
|
const telemetry = resolveSink(telemetrySink);
|
|
163
173
|
|
|
@@ -526,6 +536,7 @@ export function createRouter<TEnv = any>(
|
|
|
526
536
|
trackHandler,
|
|
527
537
|
findNearestErrorBoundary,
|
|
528
538
|
findNearestNotFoundBoundary,
|
|
539
|
+
notFoundComponent: notFound,
|
|
529
540
|
callOnError,
|
|
530
541
|
};
|
|
531
542
|
|
|
@@ -614,6 +625,8 @@ export function createRouter<TEnv = any>(
|
|
|
614
625
|
params: Record<string, string>,
|
|
615
626
|
buildVars?: Record<string, any>,
|
|
616
627
|
isPassthroughRoute?: boolean,
|
|
628
|
+
buildEnv?: TEnv,
|
|
629
|
+
devMode?: boolean,
|
|
617
630
|
) {
|
|
618
631
|
return _matchForPrerender(
|
|
619
632
|
pathname,
|
|
@@ -621,6 +634,8 @@ export function createRouter<TEnv = any>(
|
|
|
621
634
|
prerenderDeps,
|
|
622
635
|
buildVars,
|
|
623
636
|
isPassthroughRoute,
|
|
637
|
+
buildEnv,
|
|
638
|
+
devMode,
|
|
624
639
|
);
|
|
625
640
|
}
|
|
626
641
|
|
|
@@ -628,12 +643,16 @@ export function createRouter<TEnv = any>(
|
|
|
628
643
|
handler: Function,
|
|
629
644
|
handlerId: string,
|
|
630
645
|
routeName?: string,
|
|
646
|
+
buildEnv?: TEnv,
|
|
647
|
+
devMode?: boolean,
|
|
631
648
|
) {
|
|
632
649
|
return _renderStaticSegment<TEnv>(
|
|
633
650
|
handler,
|
|
634
651
|
handlerId,
|
|
635
652
|
mergedRouteMap,
|
|
636
653
|
routeName,
|
|
654
|
+
buildEnv,
|
|
655
|
+
devMode,
|
|
637
656
|
);
|
|
638
657
|
}
|
|
639
658
|
|
|
@@ -658,8 +677,15 @@ export function createRouter<TEnv = any>(
|
|
|
658
677
|
const router: RSCRouterInternal<TEnv, {}> = {
|
|
659
678
|
__brand: RSC_ROUTER_BRAND,
|
|
660
679
|
id: routerId,
|
|
680
|
+
basename,
|
|
681
|
+
|
|
682
|
+
routes(patternsOrBuilder: UrlPatterns<TEnv> | UrlBuilder<TEnv>): any {
|
|
683
|
+
// Wrap builder functions in urls() automatically
|
|
684
|
+
const urlPatterns: UrlPatterns<TEnv> =
|
|
685
|
+
typeof patternsOrBuilder === "function"
|
|
686
|
+
? (urls(patternsOrBuilder) as UrlPatterns<TEnv>)
|
|
687
|
+
: patternsOrBuilder;
|
|
661
688
|
|
|
662
|
-
routes(urlPatterns: UrlPatterns<TEnv>): any {
|
|
663
689
|
// Store reference for runtime manifest generation
|
|
664
690
|
storedUrlPatterns = urlPatterns;
|
|
665
691
|
const currentMountIndex = mountIndex++;
|
|
@@ -707,6 +733,10 @@ export function createRouter<TEnv = any>(
|
|
|
707
733
|
counters: {},
|
|
708
734
|
mountIndex: currentMountIndex,
|
|
709
735
|
cacheProfiles: resolvedCacheProfiles,
|
|
736
|
+
// basename sets the initial URL prefix so all path() patterns
|
|
737
|
+
// are registered with the prefix (e.g. "/admin" + "/users" = "/admin/users").
|
|
738
|
+
// No namePrefix — route names stay unprefixed.
|
|
739
|
+
...(basename ? { urlPrefix: basename } : {}),
|
|
710
740
|
},
|
|
711
741
|
() => {
|
|
712
742
|
handlerResult = urlPatterns.handler() as AllUseItems[];
|
|
@@ -726,7 +756,7 @@ export function createRouter<TEnv = any>(
|
|
|
726
756
|
if (entry.type === "route" && entry.isPrerender) {
|
|
727
757
|
if (!prerenderRouteKeys) prerenderRouteKeys = new Set();
|
|
728
758
|
prerenderRouteKeys.add(name);
|
|
729
|
-
if (entry.
|
|
759
|
+
if (entry.isPassthrough === true) {
|
|
730
760
|
if (!passthroughRouteKeys) passthroughRouteKeys = new Set();
|
|
731
761
|
passthroughRouteKeys.add(name);
|
|
732
762
|
}
|
|
@@ -855,8 +885,18 @@ export function createRouter<TEnv = any>(
|
|
|
855
885
|
patternOrMiddleware: string | MiddlewareFn<TEnv>,
|
|
856
886
|
middleware?: MiddlewareFn<TEnv>,
|
|
857
887
|
): any {
|
|
858
|
-
//
|
|
859
|
-
|
|
888
|
+
// Auto-prefix pattern with basename so router-level middleware
|
|
889
|
+
// patterns are router-relative (e.g. "/users/*" matches "/app/users/*").
|
|
890
|
+
if (basename && typeof patternOrMiddleware === "string") {
|
|
891
|
+
const pattern = patternOrMiddleware;
|
|
892
|
+
const prefixed =
|
|
893
|
+
pattern === "/*" || pattern === "*"
|
|
894
|
+
? `${basename}/*`
|
|
895
|
+
: `${basename}${pattern}`;
|
|
896
|
+
addMiddleware(prefixed, middleware, null);
|
|
897
|
+
} else {
|
|
898
|
+
addMiddleware(patternOrMiddleware, middleware, null);
|
|
899
|
+
}
|
|
860
900
|
return router;
|
|
861
901
|
},
|
|
862
902
|
|
|
@@ -957,6 +997,9 @@ export function createRouter<TEnv = any>(
|
|
|
957
997
|
// Expose source file for per-router type generation
|
|
958
998
|
__sourceFile,
|
|
959
999
|
|
|
1000
|
+
// Expose basename for runtime manifest generation
|
|
1001
|
+
__basename: basename,
|
|
1002
|
+
|
|
960
1003
|
// RSC request handler (lazily created on first call)
|
|
961
1004
|
fetch: (() => {
|
|
962
1005
|
// Handler is created on first call and reused
|
|
@@ -990,6 +1033,10 @@ export function createRouter<TEnv = any>(
|
|
|
990
1033
|
};
|
|
991
1034
|
})(),
|
|
992
1035
|
|
|
1036
|
+
// Low-level route matching for request classification
|
|
1037
|
+
findMatch: (pathname: string, metricsStore?: any) =>
|
|
1038
|
+
findMatch(pathname, metricsStore),
|
|
1039
|
+
|
|
993
1040
|
// Debug utility for manifest inspection
|
|
994
1041
|
debugManifest: () => buildDebugManifest<TEnv>(routesEntries),
|
|
995
1042
|
};
|
|
@@ -998,7 +1045,9 @@ export function createRouter<TEnv = any>(
|
|
|
998
1045
|
RouterRegistry.set(routerId, router);
|
|
999
1046
|
|
|
1000
1047
|
// If urls option was provided, auto-register them
|
|
1001
|
-
if (urlsOption) {
|
|
1048
|
+
if (typeof urlsOption === "function") {
|
|
1049
|
+
return router.routes(urlsOption) as RSCRouter<TEnv, {}>;
|
|
1050
|
+
} else if (urlsOption) {
|
|
1002
1051
|
return router.routes(urlsOption) as RSCRouter<TEnv, {}>;
|
|
1003
1052
|
}
|
|
1004
1053
|
|