@rangojs/router 0.0.0-experimental.56cb65a7 → 0.0.0-experimental.57
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/dist/bin/rango.js +128 -46
- package/dist/vite/index.js +211 -47
- package/package.json +2 -2
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +8 -0
- package/skills/links/SKILL.md +3 -1
- package/skills/loader/SKILL.md +53 -43
- package/skills/middleware/SKILL.md +2 -0
- package/skills/parallel/SKILL.md +67 -0
- package/skills/route/SKILL.md +31 -0
- package/skills/router-setup/SKILL.md +87 -2
- package/skills/typesafety/SKILL.md +10 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/navigation-bridge.ts +16 -3
- package/src/browser/navigation-client.ts +64 -40
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/partial-update.ts +37 -4
- package/src/browser/prefetch/fetch.ts +8 -2
- package/src/browser/prefetch/queue.ts +61 -29
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +44 -8
- package/src/browser/react/NavigationProvider.tsx +13 -4
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-router.ts +21 -8
- package/src/browser/rsc-router.tsx +26 -3
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +27 -5
- package/src/build/generate-manifest.ts +3 -0
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +211 -72
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +46 -5
- package/src/cache/taint.ts +55 -0
- package/src/context-var.ts +72 -2
- package/src/route-definition/helpers-types.ts +6 -5
- package/src/route-definition/redirect.ts +9 -1
- package/src/router/handler-context.ts +36 -17
- package/src/router/intercept-resolution.ts +9 -4
- package/src/router/loader-resolution.ts +9 -2
- package/src/router/match-middleware/background-revalidation.ts +12 -1
- package/src/router/match-middleware/cache-lookup.ts +50 -7
- package/src/router/match-middleware/cache-store.ts +21 -4
- package/src/router/match-result.ts +11 -5
- package/src/router/middleware-types.ts +6 -8
- package/src/router/middleware.ts +2 -5
- package/src/router/prerender-match.ts +2 -2
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +25 -4
- package/src/router/router-options.ts +37 -11
- package/src/router/segment-resolution/fresh.ts +47 -16
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/revalidation.ts +50 -21
- package/src/router/types.ts +1 -0
- package/src/router.ts +41 -4
- package/src/rsc/handler.ts +11 -2
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/rsc-rendering.ts +5 -0
- package/src/rsc/server-action.ts +2 -0
- package/src/rsc/ssr-setup.ts +1 -1
- package/src/rsc/types.ts +8 -1
- package/src/server/context.ts +36 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +50 -12
- package/src/ssr/index.tsx +3 -0
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +125 -31
- package/src/types/loader-types.ts +4 -5
- package/src/urls/pattern-types.ts +12 -0
- package/src/vite/discovery/discover-routers.ts +5 -1
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/rango.ts +17 -1
- package/src/vite/utils/shared-utils.ts +3 -2
package/src/server/context.ts
CHANGED
|
@@ -273,6 +273,9 @@ interface HelperContext {
|
|
|
273
273
|
string,
|
|
274
274
|
import("../cache/profile-registry.js").CacheProfile
|
|
275
275
|
>;
|
|
276
|
+
/** True when resolving handlers inside a cache() DSL boundary.
|
|
277
|
+
* Read by ctx.get() to guard non-cacheable variable reads. */
|
|
278
|
+
insideCacheScope?: boolean;
|
|
276
279
|
}
|
|
277
280
|
// Use a global symbol key so the AsyncLocalStorage instance survives HMR
|
|
278
281
|
// module re-evaluation. Without this, Vite's RSC module runner may create
|
|
@@ -666,3 +669,36 @@ export function track(label: string, depth?: number): () => void {
|
|
|
666
669
|
});
|
|
667
670
|
};
|
|
668
671
|
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Separate ALS for tracking loader execution scope.
|
|
675
|
+
* Uses a dedicated ALS (not RSCRouterContext) to avoid issues with
|
|
676
|
+
* nested RSCRouterContext.run() calls in Vite's module runner.
|
|
677
|
+
*/
|
|
678
|
+
const LOADER_SCOPE_KEY = Symbol.for("rangojs-router:loader-scope");
|
|
679
|
+
const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
|
|
680
|
+
globalThis as any
|
|
681
|
+
)[LOADER_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Check if the current execution is inside a cache() DSL boundary.
|
|
685
|
+
* Returns false inside loader execution — loaders are always fresh
|
|
686
|
+
* (never cached), so non-cacheable reads are safe.
|
|
687
|
+
*/
|
|
688
|
+
export function isInsideCacheScope(): boolean {
|
|
689
|
+
if (RSCRouterContext.getStore()?.insideCacheScope !== true) return false;
|
|
690
|
+
// Loaders are always fresh — even inside a cache() boundary, the loader
|
|
691
|
+
// function re-executes on every request. Skip the guard when running
|
|
692
|
+
// inside a loader.
|
|
693
|
+
if (loaderScopeALS.getStore()?.active) return false;
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Run `fn` inside a loader scope. While active, cache-scope guards
|
|
699
|
+
* are bypassed because loaders are always fresh (never cached) and
|
|
700
|
+
* their side effects (setCookie, header, etc.) are safe.
|
|
701
|
+
*/
|
|
702
|
+
export function runInsideLoaderScope<T>(fn: () => T): T {
|
|
703
|
+
return loaderScopeALS.run({ active: true }, fn);
|
|
704
|
+
}
|
|
@@ -44,20 +44,21 @@ export function setLoaderImports(
|
|
|
44
44
|
export async function getLoaderLazy(
|
|
45
45
|
id: string,
|
|
46
46
|
): Promise<LoaderRegistryEntry | undefined> {
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return existing;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Check the fetchable loader registry (populated by createLoader)
|
|
47
|
+
// Always check fetchableLoaderRegistry first — it's the source of truth.
|
|
48
|
+
// createLoader() updates it during module re-evaluation (HMR), so checking
|
|
49
|
+
// here ensures we pick up the fresh function after a loader file change.
|
|
54
50
|
const fetchable = getFetchableLoader(id);
|
|
55
51
|
if (fetchable) {
|
|
56
|
-
// Cache in main registry for future requests
|
|
57
52
|
loaderRegistry.set(id, fetchable);
|
|
58
53
|
return fetchable;
|
|
59
54
|
}
|
|
60
55
|
|
|
56
|
+
// Fall back to local cache (populated by previous lazy imports in production)
|
|
57
|
+
const existing = loaderRegistry.get(id);
|
|
58
|
+
if (existing) {
|
|
59
|
+
return existing;
|
|
60
|
+
}
|
|
61
|
+
|
|
61
62
|
// Try to lazy load from the import map (production mode)
|
|
62
63
|
if (lazyLoaderImports && lazyLoaderImports.size > 0) {
|
|
63
64
|
const lazyImport = lazyLoaderImports.get(id);
|
|
@@ -20,7 +20,12 @@ import type {
|
|
|
20
20
|
DefaultRouteName,
|
|
21
21
|
} from "../types/global-namespace.js";
|
|
22
22
|
import type { Handle } from "../handle.js";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
type ContextVar,
|
|
25
|
+
contextGet,
|
|
26
|
+
contextSet,
|
|
27
|
+
isNonCacheable,
|
|
28
|
+
} from "../context-var.js";
|
|
24
29
|
import { createHandleStore, type HandleStore } from "./handle-store.js";
|
|
25
30
|
import { isHandle } from "../handle.js";
|
|
26
31
|
import { track, type MetricsStore } from "./context.js";
|
|
@@ -30,6 +35,7 @@ import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
|
|
|
30
35
|
import { THEME_COOKIE } from "../theme/constants.js";
|
|
31
36
|
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
32
37
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
38
|
+
import { isInsideCacheScope } from "./context.js";
|
|
33
39
|
import {
|
|
34
40
|
createReverseFunction,
|
|
35
41
|
stripInternalParams,
|
|
@@ -63,8 +69,8 @@ export interface RequestContext<
|
|
|
63
69
|
pathname: string;
|
|
64
70
|
/** URL search params (with internal `_rsc*` params stripped, same as `url.searchParams`) */
|
|
65
71
|
searchParams: URLSearchParams;
|
|
66
|
-
/**
|
|
67
|
-
|
|
72
|
+
/** @internal Shared variable backing store for ctx.get()/ctx.set(). */
|
|
73
|
+
_variables: Record<string, any>;
|
|
68
74
|
/** Get a variable set by middleware */
|
|
69
75
|
get: {
|
|
70
76
|
<T>(contextVar: ContextVar<T>): T | undefined;
|
|
@@ -72,8 +78,12 @@ export interface RequestContext<
|
|
|
72
78
|
};
|
|
73
79
|
/** Set a variable (shared with middleware and handlers) */
|
|
74
80
|
set: {
|
|
75
|
-
<T>(
|
|
76
|
-
|
|
81
|
+
<T>(
|
|
82
|
+
contextVar: ContextVar<T>,
|
|
83
|
+
value: T,
|
|
84
|
+
options?: { cache?: boolean },
|
|
85
|
+
): void;
|
|
86
|
+
<K extends string>(key: K, value: any, options?: { cache?: boolean }): void;
|
|
77
87
|
};
|
|
78
88
|
/**
|
|
79
89
|
* Route params (populated after route matching)
|
|
@@ -277,6 +287,9 @@ export interface RequestContext<
|
|
|
277
287
|
|
|
278
288
|
/** @internal Request-scoped performance metrics store */
|
|
279
289
|
_metricsStore?: MetricsStore;
|
|
290
|
+
|
|
291
|
+
/** @internal Router basename for this request (used by redirect()) */
|
|
292
|
+
_basename?: string;
|
|
280
293
|
}
|
|
281
294
|
|
|
282
295
|
/**
|
|
@@ -306,7 +319,9 @@ export type PublicRequestContext<
|
|
|
306
319
|
| "_reportBackgroundError"
|
|
307
320
|
| "_debugPerformance"
|
|
308
321
|
| "_metricsStore"
|
|
322
|
+
| "_basename"
|
|
309
323
|
| "_setStatus"
|
|
324
|
+
| "_variables"
|
|
310
325
|
| "res"
|
|
311
326
|
>;
|
|
312
327
|
|
|
@@ -506,6 +521,18 @@ export function createRequestContext<TEnv>(
|
|
|
506
521
|
responseCookieCache = null;
|
|
507
522
|
};
|
|
508
523
|
|
|
524
|
+
// Guard: throw if a response-level side effect is called inside a cache() scope.
|
|
525
|
+
// Uses ALS to detect the scope (set during segment resolution).
|
|
526
|
+
function assertNotInsideCacheScopeALS(methodName: string): void {
|
|
527
|
+
if (isInsideCacheScope()) {
|
|
528
|
+
throw new Error(
|
|
529
|
+
`ctx.${methodName}() cannot be called inside a cache() boundary. ` +
|
|
530
|
+
`On cache hit the handler is skipped, so this side effect would be lost. ` +
|
|
531
|
+
`Move ctx.${methodName}() to a middleware or layout outside the cache() scope.`,
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
509
536
|
// Effective cookie read: response stub Set-Cookie wins, then original header.
|
|
510
537
|
// The stub IS the source of truth for same-request mutations.
|
|
511
538
|
const effectiveCookie = (name: string): string | undefined => {
|
|
@@ -569,12 +596,20 @@ export function createRequestContext<TEnv>(
|
|
|
569
596
|
originalUrl: new URL(request.url),
|
|
570
597
|
pathname: url.pathname,
|
|
571
598
|
searchParams: cleanUrl.searchParams,
|
|
572
|
-
|
|
573
|
-
get: ((keyOrVar: any) =>
|
|
574
|
-
|
|
575
|
-
|
|
599
|
+
_variables: variables,
|
|
600
|
+
get: ((keyOrVar: any) => {
|
|
601
|
+
if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
|
|
602
|
+
throw new Error(
|
|
603
|
+
`ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
|
|
604
|
+
`The variable was created with { cache: false } or set with { cache: false }, ` +
|
|
605
|
+
`and its value would be stale on cache hit. Move the read outside the cached scope.`,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
return contextGet(variables, keyOrVar);
|
|
609
|
+
}) as RequestContext<TEnv>["get"],
|
|
610
|
+
set: ((keyOrVar: any, value: any, options?: any) => {
|
|
576
611
|
assertNotInsideCacheExec(ctx, "set");
|
|
577
|
-
contextSet(variables, keyOrVar, value);
|
|
612
|
+
contextSet(variables, keyOrVar, value, options);
|
|
578
613
|
}) as RequestContext<TEnv>["set"],
|
|
579
614
|
params: {} as Record<string, string>,
|
|
580
615
|
|
|
@@ -612,6 +647,7 @@ export function createRequestContext<TEnv>(
|
|
|
612
647
|
|
|
613
648
|
setCookie(name: string, value: string, options?: CookieOptions): void {
|
|
614
649
|
assertNotInsideCacheExec(ctx, "setCookie");
|
|
650
|
+
assertNotInsideCacheScopeALS("setCookie");
|
|
615
651
|
stubResponse.headers.append(
|
|
616
652
|
"Set-Cookie",
|
|
617
653
|
serializeCookieValue(name, value, options),
|
|
@@ -624,6 +660,7 @@ export function createRequestContext<TEnv>(
|
|
|
624
660
|
options?: Pick<CookieOptions, "domain" | "path">,
|
|
625
661
|
): void {
|
|
626
662
|
assertNotInsideCacheExec(ctx, "deleteCookie");
|
|
663
|
+
assertNotInsideCacheScopeALS("deleteCookie");
|
|
627
664
|
stubResponse.headers.append(
|
|
628
665
|
"Set-Cookie",
|
|
629
666
|
serializeCookieValue(name, "", { ...options, maxAge: 0 }),
|
|
@@ -633,11 +670,13 @@ export function createRequestContext<TEnv>(
|
|
|
633
670
|
|
|
634
671
|
header(name: string, value: string): void {
|
|
635
672
|
assertNotInsideCacheExec(ctx, "header");
|
|
673
|
+
assertNotInsideCacheScopeALS("header");
|
|
636
674
|
stubResponse.headers.set(name, value);
|
|
637
675
|
},
|
|
638
676
|
|
|
639
677
|
setStatus(status: number): void {
|
|
640
678
|
assertNotInsideCacheExec(ctx, "setStatus");
|
|
679
|
+
assertNotInsideCacheScopeALS("setStatus");
|
|
641
680
|
stubResponse = new Response(null, {
|
|
642
681
|
status,
|
|
643
682
|
headers: stubResponse.headers,
|
|
@@ -676,6 +715,7 @@ export function createRequestContext<TEnv>(
|
|
|
676
715
|
|
|
677
716
|
onResponse(callback: (response: Response) => Response): void {
|
|
678
717
|
assertNotInsideCacheExec(ctx, "onResponse");
|
|
718
|
+
assertNotInsideCacheScopeALS("onResponse");
|
|
679
719
|
this._onResponseCallbacks.push(callback);
|
|
680
720
|
},
|
|
681
721
|
|
|
@@ -888,7 +928,6 @@ export function createUseFunction<TEnv>(
|
|
|
888
928
|
pathname: ctx.pathname,
|
|
889
929
|
url: ctx.url,
|
|
890
930
|
env: ctx.env as any,
|
|
891
|
-
var: ctx.var as any,
|
|
892
931
|
get: ctx.get as any,
|
|
893
932
|
use: <TDep, TDepParams = any>(
|
|
894
933
|
dep: LoaderDefinition<TDep, TDepParams>,
|
|
@@ -906,7 +945,6 @@ export function createUseFunction<TEnv>(
|
|
|
906
945
|
),
|
|
907
946
|
};
|
|
908
947
|
|
|
909
|
-
// Start loader execution with tracking
|
|
910
948
|
const doneLoader = track(`loader:${loader.$$id}`, 2);
|
|
911
949
|
const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
|
|
912
950
|
doneLoader();
|
package/src/ssr/index.tsx
CHANGED
|
@@ -129,6 +129,7 @@ interface RscPayload {
|
|
|
129
129
|
matched?: string[];
|
|
130
130
|
pathname?: string;
|
|
131
131
|
params?: Record<string, string>;
|
|
132
|
+
basename?: string;
|
|
132
133
|
themeConfig?: ResolvedThemeConfig | null;
|
|
133
134
|
initialTheme?: Theme;
|
|
134
135
|
version?: string;
|
|
@@ -261,6 +262,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
261
262
|
function SsrRoot() {
|
|
262
263
|
payload ??= createFromReadableStream<RscPayload>(rscStream1);
|
|
263
264
|
const resolved = React.use(payload);
|
|
265
|
+
|
|
264
266
|
const themeConfig = resolved.metadata?.themeConfig ?? null;
|
|
265
267
|
const pathname = resolved.metadata?.pathname ?? "/";
|
|
266
268
|
|
|
@@ -286,6 +288,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
286
288
|
navigate: async () => {},
|
|
287
289
|
refresh: async () => {},
|
|
288
290
|
version: resolved.metadata?.version,
|
|
291
|
+
basename: resolved.metadata?.basename,
|
|
289
292
|
};
|
|
290
293
|
|
|
291
294
|
// Build content tree from segments.
|
package/src/types/cache-types.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* during cache key generation (before middleware runs).
|
|
6
6
|
*
|
|
7
7
|
* Note: While the full RequestContext is passed, middleware-set variables
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* read via `ctx.get()` may not be populated yet since cache lookup happens
|
|
9
|
+
* before middleware execution.
|
|
10
10
|
*/
|
|
11
11
|
export type { RequestContext as CacheContext } from "../server/request-context.js";
|
|
12
12
|
|
|
@@ -101,7 +101,7 @@ export interface CacheOptions<TEnv = unknown> {
|
|
|
101
101
|
* Return false to skip cache for this request (always fetch fresh).
|
|
102
102
|
*
|
|
103
103
|
* Has access to full RequestContext including env, request, params, cookies, etc.
|
|
104
|
-
* Note: Middleware-set variables
|
|
104
|
+
* Note: Middleware-set variables read via `ctx.get()` may not be populated yet.
|
|
105
105
|
*
|
|
106
106
|
* @example
|
|
107
107
|
* ```typescript
|
|
@@ -123,7 +123,7 @@ export interface CacheOptions<TEnv = unknown> {
|
|
|
123
123
|
* Bypasses default key generation AND store's keyGenerator.
|
|
124
124
|
*
|
|
125
125
|
* Has access to full RequestContext including env, request, params, cookies, etc.
|
|
126
|
-
* Note: Middleware-set variables
|
|
126
|
+
* Note: Middleware-set variables read via `ctx.get()` may not be populated yet.
|
|
127
127
|
*
|
|
128
128
|
* @example
|
|
129
129
|
* ```typescript
|
|
@@ -170,7 +170,7 @@ export type Handler<
|
|
|
170
170
|
* - Cleaned route URL (`url`, `searchParams`, `pathname` — no `_rsc*` params)
|
|
171
171
|
* - Original request (`request` — raw transport URL, headers, method, body)
|
|
172
172
|
* - Platform bindings (env.DB, env.KV, env.SECRETS)
|
|
173
|
-
* - Middleware variables (
|
|
173
|
+
* - Middleware variables (`get("user")`, `get("permissions")`)
|
|
174
174
|
* - Getter/setter for variables (get('user'), set('user', ...))
|
|
175
175
|
*
|
|
176
176
|
* @example
|
|
@@ -178,8 +178,7 @@ export type Handler<
|
|
|
178
178
|
* const handler = (ctx: HandlerContext<{ slug: string }, AppEnv>) => {
|
|
179
179
|
* ctx.params.slug // Route param (string)
|
|
180
180
|
* ctx.env.DB // Binding (D1Database)
|
|
181
|
-
* ctx.
|
|
182
|
-
* ctx.get('user') // Alternative getter
|
|
181
|
+
* ctx.get('user') // Variable (User | undefined)
|
|
183
182
|
* ctx.set('user', {...}) // Setter
|
|
184
183
|
* ctx.url // Clean URL (no _rsc* params)
|
|
185
184
|
* ctx.searchParams // Clean params (no _rsc* params)
|
|
@@ -244,14 +243,9 @@ export type HandlerContext<
|
|
|
244
243
|
* Access resources like `ctx.env.DB`, `ctx.env.KV`.
|
|
245
244
|
*/
|
|
246
245
|
env: TEnv;
|
|
247
|
-
/**
|
|
248
|
-
* Middleware-injected variables.
|
|
249
|
-
* Access values like `ctx.var.user`, `ctx.var.permissions`.
|
|
250
|
-
*/
|
|
251
|
-
var: DefaultVars;
|
|
252
246
|
/**
|
|
253
247
|
* Type-safe getter for middleware variables.
|
|
254
|
-
*
|
|
248
|
+
* Preferred way to read middleware-injected variables.
|
|
255
249
|
*
|
|
256
250
|
* @example
|
|
257
251
|
* ```typescript
|
|
@@ -272,8 +266,16 @@ export type HandlerContext<
|
|
|
272
266
|
* ```
|
|
273
267
|
*/
|
|
274
268
|
set: {
|
|
275
|
-
<T>(
|
|
276
|
-
|
|
269
|
+
<T>(
|
|
270
|
+
contextVar: ContextVar<T>,
|
|
271
|
+
value: T,
|
|
272
|
+
options?: { cache?: boolean },
|
|
273
|
+
): void;
|
|
274
|
+
} & (<K extends keyof DefaultVars>(
|
|
275
|
+
key: K,
|
|
276
|
+
value: DefaultVars[K],
|
|
277
|
+
options?: { cache?: boolean },
|
|
278
|
+
) => void);
|
|
277
279
|
/**
|
|
278
280
|
* Response headers. Headers set here are merged into the final response.
|
|
279
281
|
*
|
|
@@ -289,8 +291,15 @@ export type HandlerContext<
|
|
|
289
291
|
/**
|
|
290
292
|
* Access loader data or push handle data.
|
|
291
293
|
*
|
|
294
|
+
* Available in route handlers, layout handlers, middleware, server actions,
|
|
295
|
+
* and server components rendered within the request context.
|
|
296
|
+
*
|
|
292
297
|
* For loaders: Returns a promise that resolves to the loader data.
|
|
293
298
|
* Loaders are executed in parallel and memoized per request.
|
|
299
|
+
* Prefer DSL `loader()` + client `useLoader()` over `ctx.use(Loader)` —
|
|
300
|
+
* DSL loaders are always fresh and cache-safe. Use `ctx.use(Loader)` only
|
|
301
|
+
* when you need loader data in the handler itself (e.g., to set context
|
|
302
|
+
* variables or make routing decisions).
|
|
294
303
|
*
|
|
295
304
|
* For handles: Returns a push function to add data for this segment.
|
|
296
305
|
* Handle data accumulates across all matched route segments.
|
|
@@ -298,10 +307,11 @@ export type HandlerContext<
|
|
|
298
307
|
*
|
|
299
308
|
* @example
|
|
300
309
|
* ```typescript
|
|
301
|
-
* // Loader
|
|
302
|
-
* route("
|
|
303
|
-
* const
|
|
304
|
-
*
|
|
310
|
+
* // Loader escape hatch — use when handler needs the data directly
|
|
311
|
+
* route("product", async (ctx) => {
|
|
312
|
+
* const { product } = await ctx.use(ProductLoader);
|
|
313
|
+
* ctx.set(Product, product); // make available to children
|
|
314
|
+
* return <ProductPage />;
|
|
305
315
|
* });
|
|
306
316
|
*
|
|
307
317
|
* // Handle usage - direct value
|
|
@@ -432,6 +442,8 @@ export type InternalHandlerContext<
|
|
|
432
442
|
> = HandlerContext<TParams, TEnv, TSearch> & {
|
|
433
443
|
/** @internal Stub response for collecting headers/cookies. */
|
|
434
444
|
res: Response;
|
|
445
|
+
/** @internal Shared variable backing store for ctx.get()/ctx.set(). */
|
|
446
|
+
_variables: Record<string, any>;
|
|
435
447
|
/** Prerender-only control flow helper, attached when the runtime context supports it. */
|
|
436
448
|
passthrough?: () => unknown;
|
|
437
449
|
/** Current segment ID for handle data attribution. */
|
|
@@ -519,30 +531,112 @@ export type RevalidateParams<TParams = GenericParams, TEnv = any> = Parameters<
|
|
|
519
531
|
* })
|
|
520
532
|
* ```
|
|
521
533
|
*/
|
|
534
|
+
/**
|
|
535
|
+
* Revalidation function called during client-side navigation to decide whether
|
|
536
|
+
* a segment (layout, route, parallel slot, or loader) should be re-rendered.
|
|
537
|
+
*
|
|
538
|
+
* Return `true` to re-render, `false` to skip (keep client's current version),
|
|
539
|
+
* or `{ defaultShouldRevalidate: boolean }` to override the default for
|
|
540
|
+
* downstream segments.
|
|
541
|
+
*
|
|
542
|
+
* @example
|
|
543
|
+
* ```ts
|
|
544
|
+
* // Re-render only when a cart action happened or browser signals staleness
|
|
545
|
+
* revalidate(({ actionId, stale }) =>
|
|
546
|
+
* actionId?.includes("cart") || stale || false
|
|
547
|
+
* )
|
|
548
|
+
*
|
|
549
|
+
* // Always re-render when params change (default behavior made explicit)
|
|
550
|
+
* revalidate(({ defaultShouldRevalidate }) => defaultShouldRevalidate)
|
|
551
|
+
* ```
|
|
552
|
+
*/
|
|
522
553
|
export type ShouldRevalidateFn<TParams = GenericParams, TEnv = any> = (args: {
|
|
554
|
+
/** Route params from the page being navigated away from. */
|
|
523
555
|
currentParams: TParams;
|
|
556
|
+
/** Full URL of the page being navigated away from. */
|
|
524
557
|
currentUrl: URL;
|
|
558
|
+
/** Route params for the navigation target. */
|
|
525
559
|
nextParams: TParams;
|
|
560
|
+
/** Full URL of the navigation target. */
|
|
526
561
|
nextUrl: URL;
|
|
562
|
+
/**
|
|
563
|
+
* The router's default revalidation decision for this segment.
|
|
564
|
+
* `true` when params changed or the segment is new to the client.
|
|
565
|
+
* Return this when you want default behavior plus your own conditions.
|
|
566
|
+
*/
|
|
527
567
|
defaultShouldRevalidate: boolean;
|
|
568
|
+
/** Full handler context — access to `ctx.use()`, `ctx.env`, `ctx.params`, etc. */
|
|
528
569
|
context: HandlerContext<TParams, TEnv>;
|
|
529
|
-
|
|
570
|
+
|
|
571
|
+
// ── Segment metadata (which segment is being evaluated) ──────────────
|
|
572
|
+
|
|
573
|
+
/** The type of segment being revalidated. */
|
|
530
574
|
segmentType: "layout" | "route" | "parallel";
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
575
|
+
/** Layout name (e.g., `"root"`, `"shop"`, `"auth"`). Only set for layout segments. */
|
|
576
|
+
layoutName?: string;
|
|
577
|
+
/** Slot name (e.g., `"@sidebar"`, `"@modal"`). Only set for parallel segments. */
|
|
578
|
+
slotName?: string;
|
|
579
|
+
|
|
580
|
+
// ── Action context (populated when revalidation is triggered by a server action) ──
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Identifier of the server action that triggered revalidation.
|
|
584
|
+
* `undefined` during normal navigation (no action involved).
|
|
585
|
+
*
|
|
586
|
+
* Format: `"src/<path>#<exportName>"` — the file path is the source path
|
|
587
|
+
* relative to the project root, followed by `#` and the exported function name.
|
|
588
|
+
*
|
|
589
|
+
* This is stable and can be used for path-based matching to revalidate
|
|
590
|
+
* when any action in a module or directory fires:
|
|
591
|
+
*
|
|
592
|
+
* @example
|
|
593
|
+
* ```ts
|
|
594
|
+
* // Match a specific action
|
|
595
|
+
* revalidate(({ actionId }) => actionId === "src/actions/cart.ts#addToCart")
|
|
596
|
+
*
|
|
597
|
+
* // Match any action in the cart module
|
|
598
|
+
* revalidate(({ actionId }) => actionId?.includes("cart") ?? false)
|
|
599
|
+
*
|
|
600
|
+
* // Match any action under src/apps/store/actions/
|
|
601
|
+
* revalidate(({ actionId }) => actionId?.startsWith("src/apps/store/actions/") ?? false)
|
|
602
|
+
* ```
|
|
603
|
+
*/
|
|
604
|
+
actionId?: string;
|
|
605
|
+
/** URL where the action was executed (the page the user was on when they triggered the action). */
|
|
606
|
+
actionUrl?: URL;
|
|
607
|
+
/** Return value from the action execution. Can be used to conditionally revalidate based on the action's outcome. */
|
|
608
|
+
actionResult?: any;
|
|
609
|
+
/** FormData from the action request body. Only set for form-based actions (not inline `"use server"` actions). */
|
|
610
|
+
formData?: FormData;
|
|
611
|
+
/** HTTP method: `"GET"` for navigation, `"POST"` for server actions. */
|
|
612
|
+
method?: string;
|
|
613
|
+
|
|
614
|
+
// ── Route identity ───────────────────────────────────────────────────
|
|
615
|
+
|
|
616
|
+
/** Route name of the navigation target. Alias for `toRouteName`. */
|
|
617
|
+
routeName?: DefaultRouteName;
|
|
618
|
+
/**
|
|
619
|
+
* Route name being navigated away from.
|
|
620
|
+
* `undefined` for unnamed internal routes (those without a `name` option).
|
|
621
|
+
*/
|
|
622
|
+
fromRouteName?: DefaultRouteName;
|
|
623
|
+
/**
|
|
624
|
+
* Route name being navigated to.
|
|
625
|
+
* `undefined` for unnamed internal routes (those without a `name` option).
|
|
626
|
+
*/
|
|
627
|
+
toRouteName?: DefaultRouteName;
|
|
628
|
+
|
|
629
|
+
// ── Staleness signal ─────────────────────────────────────────────────
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* `true` when the browser signals that data may be stale — typically because
|
|
633
|
+
* a server action was executed in this or another tab (`_rsc_stale` header).
|
|
634
|
+
*
|
|
635
|
+
* This is NOT segment cache staleness (loaders are never segment-cached).
|
|
636
|
+
* Use this to decide whether loader data should be re-fetched after an
|
|
637
|
+
* action that may have mutated backend state.
|
|
638
|
+
*/
|
|
639
|
+
stale?: boolean;
|
|
546
640
|
}) => boolean | { defaultShouldRevalidate: boolean };
|
|
547
641
|
|
|
548
642
|
// MiddlewareFn is imported from "../router/middleware.js" and re-exported
|
|
@@ -53,7 +53,6 @@ export type LoaderContext<
|
|
|
53
53
|
pathname: string;
|
|
54
54
|
url: URL;
|
|
55
55
|
env: TEnv;
|
|
56
|
-
var: DefaultVars;
|
|
57
56
|
get: {
|
|
58
57
|
<T>(contextVar: ContextVar<T>): T | undefined;
|
|
59
58
|
} & (<K extends keyof DefaultVars>(key: K) => DefaultVars[K]);
|
|
@@ -166,11 +165,11 @@ export type LoadOptions =
|
|
|
166
165
|
* return await db.products.findBySlug(slug);
|
|
167
166
|
* });
|
|
168
167
|
*
|
|
169
|
-
* //
|
|
170
|
-
* const
|
|
168
|
+
* // Client usage (preferred — cache-safe, always fresh)
|
|
169
|
+
* const { data } = useLoader(CartLoader);
|
|
171
170
|
*
|
|
172
|
-
* //
|
|
173
|
-
* const cart =
|
|
171
|
+
* // Server escape hatch (handler needs data directly)
|
|
172
|
+
* const cart = await ctx.use(CartLoader);
|
|
174
173
|
* ```
|
|
175
174
|
*/
|
|
176
175
|
export type LoaderDefinition<
|
|
@@ -7,6 +7,18 @@ import type {
|
|
|
7
7
|
} from "../route-types.js";
|
|
8
8
|
import type { SearchSchema } from "../search-params.js";
|
|
9
9
|
import { RESPONSE_TYPE } from "./response-types.js";
|
|
10
|
+
import type { DefaultEnv } from "../types.js";
|
|
11
|
+
import type { PathHelpers } from "./path-helper-types.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Builder function accepted by urls() and as a shorthand for routes()/urls option.
|
|
15
|
+
* When passed directly to routes() or createRouter({ urls }), it is wrapped in urls() automatically.
|
|
16
|
+
*/
|
|
17
|
+
export type UrlBuilder<
|
|
18
|
+
TEnv = DefaultEnv,
|
|
19
|
+
TItems extends readonly (AllUseItems | readonly AllUseItems[])[] =
|
|
20
|
+
readonly AllUseItems[],
|
|
21
|
+
> = (helpers: PathHelpers<TEnv>) => TItems;
|
|
10
22
|
|
|
11
23
|
/**
|
|
12
24
|
* Sentinel type for unnamed routes.
|
|
@@ -135,7 +135,11 @@ export async function discoverRouters(
|
|
|
135
135
|
continue;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
const manifest = generateManifestFull(
|
|
138
|
+
const manifest = generateManifestFull(
|
|
139
|
+
router.urlpatterns,
|
|
140
|
+
routerMountIndex,
|
|
141
|
+
router.__basename ? { urlPrefix: router.__basename } : undefined,
|
|
142
|
+
);
|
|
139
143
|
routerMountIndex++;
|
|
140
144
|
allManifests.push({ id, manifest });
|
|
141
145
|
const routeCount = Object.keys(manifest.routeManifest).length;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Performance Tracks — RSDW client patch
|
|
3
|
+
*
|
|
4
|
+
* Patches the RSDW client so _debugInfo recovery works for plain-object
|
|
5
|
+
* payloads (our RscPayload shape). Without this, the Server Components
|
|
6
|
+
* track in Chrome DevTools stays empty.
|
|
7
|
+
*
|
|
8
|
+
* React's flushComponentPerformance uses splice(0) to empty _debugInfo
|
|
9
|
+
* after resolution, then recovers it from the resolved value — but only
|
|
10
|
+
* for arrays, async iterables, React elements, and lazy types. Since our
|
|
11
|
+
* RscPayload is a plain object, _debugInfo is lost. This patch relaxes
|
|
12
|
+
* the check so _debugInfo is recovered from any object.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Plugin } from "vite";
|
|
16
|
+
import { readFile } from "node:fs/promises";
|
|
17
|
+
|
|
18
|
+
const RSDW_PATCH_RE =
|
|
19
|
+
/((?:var|let|const)\s+\w+\s*=\s*root\._children\s*,\s*(\w+)\s*=\s*root\._debugInfo\s*[;,])/;
|
|
20
|
+
|
|
21
|
+
function buildPatchReplacement(match: string, debugInfoVar: string): string {
|
|
22
|
+
return `${match}
|
|
23
|
+
if (${debugInfoVar} && 0 === ${debugInfoVar}.length && "fulfilled" === root.status) {
|
|
24
|
+
var _resolved = "function" === typeof resolveLazy ? resolveLazy(root.value) : root.value;
|
|
25
|
+
if ("object" === typeof _resolved && null !== _resolved && isArrayImpl(_resolved._debugInfo)) {
|
|
26
|
+
${debugInfoVar} = _resolved._debugInfo;
|
|
27
|
+
}
|
|
28
|
+
}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function patchRsdwClientDebugInfoRecovery(code: string): {
|
|
32
|
+
code: string;
|
|
33
|
+
debugInfoVar: string | null;
|
|
34
|
+
} {
|
|
35
|
+
const match = code.match(RSDW_PATCH_RE);
|
|
36
|
+
if (!match) {
|
|
37
|
+
return { code, debugInfoVar: null };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
code: code.replace(match[1]!, buildPatchReplacement(match[1]!, match[2]!)),
|
|
42
|
+
debugInfoVar: match[2]!,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function performanceTracksOptimizeDepsPlugin(): {
|
|
47
|
+
name: string;
|
|
48
|
+
setup(build: any): void;
|
|
49
|
+
} {
|
|
50
|
+
return {
|
|
51
|
+
name: "@rangojs/router:performance-tracks-optimize-deps",
|
|
52
|
+
setup(build: any): void {
|
|
53
|
+
build.onLoad(
|
|
54
|
+
{
|
|
55
|
+
filter:
|
|
56
|
+
/react-server-dom-webpack-client\.browser\.(development|production)\.js$/,
|
|
57
|
+
},
|
|
58
|
+
async (args: { path: string }) => {
|
|
59
|
+
const code = await readFile(args.path, "utf8");
|
|
60
|
+
const patched = patchRsdwClientDebugInfoRecovery(code);
|
|
61
|
+
return {
|
|
62
|
+
contents: patched.code,
|
|
63
|
+
loader: "js",
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function performanceTracksPlugin(): Plugin {
|
|
72
|
+
return {
|
|
73
|
+
name: "@rangojs/router:performance-tracks",
|
|
74
|
+
|
|
75
|
+
transform(code, id) {
|
|
76
|
+
if (!id.includes("react-server-dom") || !id.includes("client")) return;
|
|
77
|
+
const patched = patchRsdwClientDebugInfoRecovery(code);
|
|
78
|
+
if (!patched.debugInfoVar) return;
|
|
79
|
+
if (process.env.INTERNAL_RANGO_DEBUG)
|
|
80
|
+
console.log(
|
|
81
|
+
"[perf-tracks] patched RSDW client (var:",
|
|
82
|
+
patched.debugInfoVar,
|
|
83
|
+
")",
|
|
84
|
+
);
|
|
85
|
+
return patched.code;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|