@rangojs/router 0.0.0-experimental.57 → 0.0.0-experimental.57005a2b
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 +2 -1
- package/dist/vite/index.js +507 -192
- package/dist/vite/index.js.bak +5448 -0
- 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/middleware/SKILL.md +32 -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/src/__internal.ts +1 -1
- package/src/browser/navigation-bridge.ts +21 -2
- package/src/browser/navigation-client.ts +34 -6
- package/src/browser/partial-update.ts +14 -2
- package/src/browser/prefetch/cache.ts +16 -6
- package/src/browser/prefetch/fetch.ts +60 -4
- package/src/browser/react/Link.tsx +25 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/scroll-restoration.ts +10 -8
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/build/generate-manifest.ts +3 -6
- package/src/build/route-trie.ts +50 -24
- 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 +61 -14
- package/src/route-definition/index.ts +3 -0
- 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 +46 -6
- package/src/router/lazy-includes.ts +5 -5
- package/src/router/loader-resolution.ts +147 -19
- package/src/router/manifest.ts +12 -7
- package/src/router/match-api.ts +124 -189
- package/src/router/match-middleware/cache-lookup.ts +24 -7
- package/src/router/match-middleware/segment-resolution.ts +53 -0
- package/src/router/match-result.ts +82 -4
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/prerender-match.ts +108 -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 +59 -2
- package/src/router/segment-resolution/revalidation.ts +79 -6
- package/src/router.ts +13 -1
- package/src/rsc/handler.ts +468 -377
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/progressive-enhancement.ts +10 -2
- package/src/rsc/rsc-rendering.ts +5 -1
- package/src/rsc/server-action.ts +6 -0
- package/src/rsc/ssr-setup.ts +1 -1
- package/src/rsc/types.ts +1 -0
- 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 +40 -4
- package/src/server/handle-store.ts +19 -0
- package/src/server/request-context.ts +125 -3
- package/src/static-handler.ts +18 -6
- package/src/types/handler-context.ts +12 -2
- package/src/types/loader-types.ts +32 -4
- 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/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/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/refresh-cmd.ts +88 -26
- package/src/vite/rango.ts +2 -1
- package/src/vite/router-discovery.ts +178 -37
- package/src/vite/utils/prerender-utils.ts +37 -5
|
@@ -26,7 +26,12 @@ import {
|
|
|
26
26
|
contextSet,
|
|
27
27
|
isNonCacheable,
|
|
28
28
|
} from "../context-var.js";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
createHandleStore,
|
|
31
|
+
buildHandleSnapshot,
|
|
32
|
+
type HandleStore,
|
|
33
|
+
type HandleData,
|
|
34
|
+
} from "./handle-store.js";
|
|
30
35
|
import { isHandle } from "../handle.js";
|
|
31
36
|
import { track, type MetricsStore } from "./context.js";
|
|
32
37
|
import { getFetchableLoader } from "./fetchable-loader-store.js";
|
|
@@ -271,6 +276,54 @@ export interface RequestContext<
|
|
|
271
276
|
/** @internal Previous route key (from the navigation source), used for revalidation */
|
|
272
277
|
_prevRouteKey?: string;
|
|
273
278
|
|
|
279
|
+
/**
|
|
280
|
+
* @internal Render barrier for experimental `rendered()` API.
|
|
281
|
+
* Resolves when all non-loader segments have settled and handle data
|
|
282
|
+
* is available. Used by DSL loaders that call `ctx.rendered()`.
|
|
283
|
+
*/
|
|
284
|
+
_renderBarrier: Promise<void>;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* @internal Resolve the render barrier. Accepts resolved segments, filters
|
|
288
|
+
* out loaders, and captures non-loader segment IDs as the handle ordering.
|
|
289
|
+
* Called after segment resolution (fresh) or handle replay (cache/prerender).
|
|
290
|
+
*/
|
|
291
|
+
_resolveRenderBarrier: (
|
|
292
|
+
segments: Array<{ type: string; id: string }>,
|
|
293
|
+
) => void;
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* @internal Segment order at barrier resolution time, used by loader
|
|
297
|
+
* ctx.use(handle) to collect handle data in correct order.
|
|
298
|
+
*/
|
|
299
|
+
_renderBarrierSegmentOrder?: string[];
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @internal Set to true when the matched entry tree contains any `loading()`
|
|
303
|
+
* entries (streaming). Used by rendered() to fail fast.
|
|
304
|
+
*/
|
|
305
|
+
_treeHasStreaming?: boolean;
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* @internal Loader IDs that have called rendered() and are waiting for the
|
|
309
|
+
* barrier. Used to detect deadlocks when a handler tries to await the same
|
|
310
|
+
* loader via ctx.use(Loader).
|
|
311
|
+
*/
|
|
312
|
+
_renderBarrierWaiters?: Set<string>;
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* @internal Loader IDs that handlers have started awaiting via ctx.use().
|
|
316
|
+
* Used for bidirectional deadlock detection: if a loader later calls
|
|
317
|
+
* rendered() and a handler already awaits it, we can detect the deadlock.
|
|
318
|
+
*/
|
|
319
|
+
_handlerLoaderDeps?: Set<string>;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* @internal Cached HandleData snapshot built at barrier resolution time.
|
|
323
|
+
* Avoids rebuilding the snapshot on every loader ctx.use(handle) call.
|
|
324
|
+
*/
|
|
325
|
+
_renderBarrierHandleSnapshot?: HandleData;
|
|
326
|
+
|
|
274
327
|
/** @internal Per-request error dedup set for onError reporting */
|
|
275
328
|
_reportedErrors: WeakSet<object>;
|
|
276
329
|
|
|
@@ -290,6 +343,12 @@ export interface RequestContext<
|
|
|
290
343
|
|
|
291
344
|
/** @internal Router basename for this request (used by redirect()) */
|
|
292
345
|
_basename?: string;
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* @internal RouteSnapshot from classifyRequest, reused by match/matchPartial
|
|
349
|
+
* to avoid a second resolveRoute call. Cleared on HMR invalidation.
|
|
350
|
+
*/
|
|
351
|
+
_classifiedRoute?: import("../router/route-snapshot.js").RouteSnapshot;
|
|
293
352
|
}
|
|
294
353
|
|
|
295
354
|
/**
|
|
@@ -316,12 +375,20 @@ export type PublicRequestContext<
|
|
|
316
375
|
| "_routeName"
|
|
317
376
|
| "_prevRouteKey"
|
|
318
377
|
| "_reportedErrors"
|
|
378
|
+
| "_renderBarrier"
|
|
379
|
+
| "_resolveRenderBarrier"
|
|
380
|
+
| "_renderBarrierSegmentOrder"
|
|
381
|
+
| "_treeHasStreaming"
|
|
382
|
+
| "_renderBarrierWaiters"
|
|
383
|
+
| "_handlerLoaderDeps"
|
|
384
|
+
| "_renderBarrierHandleSnapshot"
|
|
319
385
|
| "_reportBackgroundError"
|
|
320
386
|
| "_debugPerformance"
|
|
321
387
|
| "_metricsStore"
|
|
322
388
|
| "_basename"
|
|
323
389
|
| "_setStatus"
|
|
324
390
|
| "_variables"
|
|
391
|
+
| "_classifiedRoute"
|
|
325
392
|
| "res"
|
|
326
393
|
>;
|
|
327
394
|
|
|
@@ -743,9 +810,58 @@ export function createRequestContext<TEnv>(
|
|
|
743
810
|
_reportedErrors: new WeakSet<object>(),
|
|
744
811
|
_metricsStore: undefined,
|
|
745
812
|
|
|
813
|
+
// Render barrier: deferred promise resolved after non-loader segments settle.
|
|
814
|
+
_renderBarrier: null as any, // set below
|
|
815
|
+
_resolveRenderBarrier: null as any, // set below
|
|
816
|
+
_renderBarrierSegmentOrder: undefined,
|
|
817
|
+
|
|
746
818
|
reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
|
|
747
819
|
};
|
|
748
820
|
|
|
821
|
+
// Lazy render barrier: only allocate the Promise when a loader actually
|
|
822
|
+
// calls rendered(). Requests that don't use rendered() pay zero cost.
|
|
823
|
+
let barrierResolved = false;
|
|
824
|
+
let resolveBarrier: (() => void) | undefined;
|
|
825
|
+
ctx._renderBarrier = null as any; // lazy — created on first access
|
|
826
|
+
ctx._resolveRenderBarrier = (
|
|
827
|
+
segments: Array<{ type: string; id: string }>,
|
|
828
|
+
) => {
|
|
829
|
+
if (barrierResolved) return;
|
|
830
|
+
barrierResolved = true;
|
|
831
|
+
const segOrder = segments
|
|
832
|
+
.filter((s) => s.type !== "loader")
|
|
833
|
+
.map((s) => s.id);
|
|
834
|
+
ctx._renderBarrierSegmentOrder = segOrder;
|
|
835
|
+
// Build and cache handle snapshot so loader ctx.use(handle) calls
|
|
836
|
+
// don't rebuild it on every invocation.
|
|
837
|
+
ctx._renderBarrierHandleSnapshot = buildHandleSnapshot(
|
|
838
|
+
handleStore,
|
|
839
|
+
segOrder,
|
|
840
|
+
);
|
|
841
|
+
ctx._renderBarrierWaiters = undefined;
|
|
842
|
+
ctx._handlerLoaderDeps = undefined;
|
|
843
|
+
if (resolveBarrier) resolveBarrier();
|
|
844
|
+
};
|
|
845
|
+
Object.defineProperty(ctx, "_renderBarrier", {
|
|
846
|
+
get() {
|
|
847
|
+
// Barrier already resolved (cache/prerender hit) or first lazy access.
|
|
848
|
+
// Either way, replace the getter with a concrete value to avoid
|
|
849
|
+
// repeated Promise.resolve() allocations on subsequent reads.
|
|
850
|
+
const p = barrierResolved
|
|
851
|
+
? Promise.resolve()
|
|
852
|
+
: new Promise<void>((resolve) => {
|
|
853
|
+
resolveBarrier = resolve;
|
|
854
|
+
});
|
|
855
|
+
Object.defineProperty(ctx, "_renderBarrier", {
|
|
856
|
+
value: p,
|
|
857
|
+
writable: false,
|
|
858
|
+
configurable: false,
|
|
859
|
+
});
|
|
860
|
+
return p;
|
|
861
|
+
},
|
|
862
|
+
configurable: true,
|
|
863
|
+
});
|
|
864
|
+
|
|
749
865
|
// Now create use() with access to ctx
|
|
750
866
|
ctx.use = createUseFunction({
|
|
751
867
|
handleStore,
|
|
@@ -929,12 +1045,12 @@ export function createUseFunction<TEnv>(
|
|
|
929
1045
|
url: ctx.url,
|
|
930
1046
|
env: ctx.env as any,
|
|
931
1047
|
get: ctx.get as any,
|
|
932
|
-
use: <TDep, TDepParams = any>(
|
|
1048
|
+
use: (<TDep, TDepParams = any>(
|
|
933
1049
|
dep: LoaderDefinition<TDep, TDepParams>,
|
|
934
1050
|
): Promise<TDep> => {
|
|
935
1051
|
// Recursive call - will start dep loader if not already started
|
|
936
1052
|
return ctx.use(dep);
|
|
937
|
-
},
|
|
1053
|
+
}) as LoaderContext["use"],
|
|
938
1054
|
method: "GET",
|
|
939
1055
|
body: undefined,
|
|
940
1056
|
reverse: createReverseFunction(
|
|
@@ -943,6 +1059,12 @@ export function createUseFunction<TEnv>(
|
|
|
943
1059
|
ctx.params as Record<string, string>,
|
|
944
1060
|
ctx._routeName ? isRouteRootScoped(ctx._routeName) : undefined,
|
|
945
1061
|
),
|
|
1062
|
+
rendered: () => {
|
|
1063
|
+
throw new Error(
|
|
1064
|
+
`ctx.rendered() is only available in DSL loaders (registered via loader() in urls()). ` +
|
|
1065
|
+
`It cannot be used from request-context loaders or server actions.`,
|
|
1066
|
+
);
|
|
1067
|
+
},
|
|
946
1068
|
};
|
|
947
1069
|
|
|
948
1070
|
const doneLoader = track(`loader:${loader.$$id}`, 2);
|
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
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ContextVar } from "../context-var.js";
|
|
2
|
+
import type { Handle } from "../handle.js";
|
|
2
3
|
import type { MiddlewareFn } from "../router/middleware.js";
|
|
3
4
|
import type { ScopedReverseFunction } from "../reverse.js";
|
|
4
5
|
import type { SearchSchema, ResolveSearchSchema } from "../search-params.js";
|
|
@@ -57,11 +58,38 @@ export type LoaderContext<
|
|
|
57
58
|
<T>(contextVar: ContextVar<T>): T | undefined;
|
|
58
59
|
} & (<K extends keyof DefaultVars>(key: K) => DefaultVars[K]);
|
|
59
60
|
/**
|
|
60
|
-
* Access another loader's data
|
|
61
|
+
* Access another loader's data, or read handle data after rendered().
|
|
62
|
+
*
|
|
63
|
+
* For loaders: returns a promise (loaders run in parallel).
|
|
64
|
+
* For handles: returns collected data (only after `await ctx.rendered()`).
|
|
61
65
|
*/
|
|
62
|
-
use:
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
use: {
|
|
67
|
+
<T, TLoaderParams = any>(
|
|
68
|
+
loader: LoaderDefinition<T, TLoaderParams>,
|
|
69
|
+
): Promise<T>;
|
|
70
|
+
<TData, TAccumulated = TData[]>(
|
|
71
|
+
handle: Handle<TData, TAccumulated>,
|
|
72
|
+
): TAccumulated;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* **Experimental.** Wait for all non-loader segments to settle.
|
|
76
|
+
*
|
|
77
|
+
* After the returned promise resolves, handle data is available via
|
|
78
|
+
* `ctx.use(handle)`. Only supported in DSL loaders on non-streaming
|
|
79
|
+
* trees (no `loading()`). Throws if called from a handler-invoked
|
|
80
|
+
* loader or when the tree uses streaming.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* const PricesLoader = createLoader(async (ctx) => {
|
|
85
|
+
* "use server";
|
|
86
|
+
* await ctx.rendered();
|
|
87
|
+
* const products = ctx.use(Products); // reads handle data
|
|
88
|
+
* return pricing.getLive(products.map(p => p.id));
|
|
89
|
+
* });
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
rendered: () => Promise<void>;
|
|
65
93
|
/**
|
|
66
94
|
* HTTP method (GET, POST, PUT, PATCH, DELETE)
|
|
67
95
|
* Available when loader is called via load({ method: "POST", ... })
|
package/src/types/route-entry.ts
CHANGED
|
@@ -8,10 +8,21 @@ export interface LazyIncludeContext {
|
|
|
8
8
|
urlPrefix: string;
|
|
9
9
|
namePrefix: string | undefined;
|
|
10
10
|
parent: unknown; // EntryData - avoid circular import
|
|
11
|
+
/** Counter snapshot from pattern extraction for consistent shortCode indices */
|
|
12
|
+
counters?: Record<string, number>;
|
|
11
13
|
cacheProfiles?: Record<
|
|
12
14
|
string,
|
|
13
15
|
import("../cache/profile-registry.js").CacheProfile
|
|
14
16
|
>;
|
|
17
|
+
/** Root scope flag for dot-local reverse resolution */
|
|
18
|
+
rootScoped?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Positional include scope token composed from the parent scope plus this
|
|
21
|
+
* include's sibling index (`${parentScope}I${idx}`). Applied to direct-
|
|
22
|
+
* descendant shortCodes during lazy evaluation so routes inside the
|
|
23
|
+
* include cannot collide with siblings declared outside it.
|
|
24
|
+
*/
|
|
25
|
+
includeScope?: string;
|
|
15
26
|
}
|
|
16
27
|
|
|
17
28
|
/**
|
|
@@ -69,7 +80,7 @@ export interface RouteEntry<TEnv = any> {
|
|
|
69
80
|
prerenderRouteKeys?: Set<string>;
|
|
70
81
|
|
|
71
82
|
/**
|
|
72
|
-
* Route keys in this entry that
|
|
83
|
+
* Route keys in this entry that are wrapped with `Passthrough()`.
|
|
73
84
|
* Used by the non-trie match path to set the `pt` flag.
|
|
74
85
|
*/
|
|
75
86
|
passthroughRouteKeys?: Set<string>;
|
package/src/types/segments.ts
CHANGED
|
@@ -50,12 +50,12 @@ export interface ResolvedSegment {
|
|
|
50
50
|
parallelName?: string; // For parallels: the parallel group name (used to match with revalidations)
|
|
51
51
|
// Loader-specific fields
|
|
52
52
|
loaderId?: string; // For loaders: the loader $$id identifier
|
|
53
|
+
_inherited?: boolean; // For inherited loaders: dedup marker for buildMatchResult
|
|
53
54
|
loaderData?: any; // For loaders: the resolved data from loader execution
|
|
54
55
|
parallelLoading?: ReactNode; // For parallel-owned loaders: the parallel's loading fallback
|
|
55
56
|
// Intercept loader fields (for streaming loader data in parallel segments)
|
|
56
57
|
loaderDataPromise?: Promise<any[]> | any[]; // Loader data promise or resolved array
|
|
57
58
|
loaderIds?: string[]; // IDs ($$id) of loaders for this segment
|
|
58
|
-
parallelLoaderSources?: any[]; // Internal: preserves stable aggregate promise across renders
|
|
59
59
|
// Error-specific fields
|
|
60
60
|
error?: ErrorInfo; // For error segments: the error information
|
|
61
61
|
// NotFound-specific fields
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
runWithPrefixes,
|
|
5
5
|
getUrlPrefix,
|
|
6
6
|
getNamePrefix,
|
|
7
|
-
getRootScoped,
|
|
8
7
|
} from "../server/context";
|
|
9
8
|
import {
|
|
10
9
|
INTERNAL_INCLUDE_SCOPE_PREFIX,
|
|
@@ -149,22 +148,32 @@ export function createIncludeHelper<TEnv>(): IncludeFn<TEnv> {
|
|
|
149
148
|
});
|
|
150
149
|
}
|
|
151
150
|
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
// and
|
|
161
|
-
|
|
151
|
+
// Allocate an include-scope token for this include() call. The token is
|
|
152
|
+
// appended to the parent's shortCode prefix whenever the include's
|
|
153
|
+
// direct-descendant shortCodes are generated (see getShortCode in
|
|
154
|
+
// context.ts), partitioning the parent's counter namespace so routes
|
|
155
|
+
// inside an include cannot collide with siblings declared outside it.
|
|
156
|
+
//
|
|
157
|
+
// Scopes compose: a nested include inside an outer include with scope
|
|
158
|
+
// "I0" allocates against the `${parent.shortCode}I0_include` counter
|
|
159
|
+
// and produces scope "I0I0", "I0I1", etc.
|
|
160
|
+
const parentScope = ctx.includeScope ?? "";
|
|
161
|
+
let includeScope = parentScope;
|
|
162
162
|
if (capturedParent?.shortCode) {
|
|
163
|
-
const
|
|
164
|
-
ctx.counters[
|
|
165
|
-
ctx.counters[
|
|
163
|
+
const includeCounterKey = `${capturedParent.shortCode}${parentScope}_include`;
|
|
164
|
+
ctx.counters[includeCounterKey] ??= 0;
|
|
165
|
+
const includeIdx = ctx.counters[includeCounterKey];
|
|
166
|
+
ctx.counters[includeCounterKey] = includeIdx + 1;
|
|
167
|
+
includeScope = `${parentScope}I${includeIdx}`;
|
|
166
168
|
}
|
|
167
169
|
|
|
170
|
+
// Snapshot parent's counters AFTER allocating the include scope so lazy
|
|
171
|
+
// manifest generation starts with the same counter state this include
|
|
172
|
+
// observed — its descendants still get fresh per-scope counters because
|
|
173
|
+
// they key off `${parent.shortCode}${includeScope}_*` (not shared with
|
|
174
|
+
// siblings outside the include).
|
|
175
|
+
const capturedCounters = { ...ctx.counters };
|
|
176
|
+
|
|
168
177
|
// Compute rootScoped at capture time, mirroring the logic in runWithPrefixes.
|
|
169
178
|
// This ensures lazy evaluation restores the correct scope state.
|
|
170
179
|
const parentRootScoped = ctx.rootScoped;
|
|
@@ -191,6 +200,7 @@ export function createIncludeHelper<TEnv>(): IncludeFn<TEnv> {
|
|
|
191
200
|
counters: capturedCounters,
|
|
192
201
|
cacheProfiles: ctx.cacheProfiles,
|
|
193
202
|
rootScoped: capturedRootScoped,
|
|
203
|
+
includeScope,
|
|
194
204
|
},
|
|
195
205
|
} as IncludeItem;
|
|
196
206
|
};
|
|
@@ -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>,
|
|
@@ -229,12 +233,27 @@ export type PathHelpers<TEnv> = {
|
|
|
229
233
|
include: IncludeFn<TEnv>;
|
|
230
234
|
|
|
231
235
|
/**
|
|
232
|
-
* Define parallel routes that render simultaneously in named slots
|
|
236
|
+
* Define parallel routes that render simultaneously in named slots.
|
|
237
|
+
*
|
|
238
|
+
* A slot value can be a Handler / ReactNode / StaticHandlerDefinition
|
|
239
|
+
* (legacy form, broadcast use applies to every slot) or a slot descriptor
|
|
240
|
+
* `{ handler, use? }` whose `use` is scoped to that slot only. Per-slot
|
|
241
|
+
* merge order is `handler.use` → shared `use` → slot-local `use`, with
|
|
242
|
+
* narrowest scope winning for last-write-wins items like `loading()`.
|
|
233
243
|
*/
|
|
234
244
|
parallel: <
|
|
235
245
|
TSlots extends Record<
|
|
236
246
|
`@${string}`,
|
|
237
|
-
Handler<any, any, TEnv>
|
|
247
|
+
| Handler<any, any, TEnv>
|
|
248
|
+
| ReactNode
|
|
249
|
+
| StaticHandlerDefinition
|
|
250
|
+
| {
|
|
251
|
+
handler:
|
|
252
|
+
| Handler<any, any, TEnv>
|
|
253
|
+
| ReactNode
|
|
254
|
+
| StaticHandlerDefinition;
|
|
255
|
+
use?: () => ParallelUseItem[];
|
|
256
|
+
}
|
|
238
257
|
>,
|
|
239
258
|
>(
|
|
240
259
|
slots: TSlots,
|
|
@@ -260,9 +279,20 @@ export type PathHelpers<TEnv> = {
|
|
|
260
279
|
) => InterceptItem;
|
|
261
280
|
|
|
262
281
|
/**
|
|
263
|
-
* Attach middleware to the current route/layout
|
|
282
|
+
* Attach middleware to the current route/layout, or wrap child segments
|
|
264
283
|
*/
|
|
265
|
-
middleware:
|
|
284
|
+
middleware: {
|
|
285
|
+
(fn: MiddlewareFn<TEnv>): MiddlewareItem;
|
|
286
|
+
(
|
|
287
|
+
fn: MiddlewareFn<TEnv>,
|
|
288
|
+
children: () => UseItems<LayoutUseItem>,
|
|
289
|
+
): MiddlewareItem;
|
|
290
|
+
(fns: MiddlewareFn<TEnv>[]): MiddlewareItem;
|
|
291
|
+
(
|
|
292
|
+
fns: MiddlewareFn<TEnv>[],
|
|
293
|
+
children: () => UseItems<LayoutUseItem>,
|
|
294
|
+
): MiddlewareItem;
|
|
295
|
+
};
|
|
266
296
|
|
|
267
297
|
/**
|
|
268
298
|
* Control when a segment should revalidate during navigation
|
|
@@ -280,7 +310,10 @@ export type PathHelpers<TEnv> = {
|
|
|
280
310
|
/**
|
|
281
311
|
* Attach a loading component to the current route/layout
|
|
282
312
|
*/
|
|
283
|
-
loading: (
|
|
313
|
+
loading: (
|
|
314
|
+
component: ReactNode | (() => ReactNode),
|
|
315
|
+
options?: { ssr?: boolean },
|
|
316
|
+
) => LoadingItem;
|
|
284
317
|
|
|
285
318
|
/**
|
|
286
319
|
* 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}]`,
|