@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26
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 +294 -28
- package/dist/bin/rango.js +355 -47
- package/dist/vite/index.js +1658 -1239
- package/package.json +3 -3
- package/skills/cache-guide/SKILL.md +9 -5
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +229 -15
- package/skills/middleware/SKILL.md +109 -30
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +189 -19
- package/skills/rango/SKILL.md +1 -2
- package/skills/response-routes/SKILL.md +3 -3
- package/skills/route/SKILL.md +44 -3
- package/skills/router-setup/SKILL.md +80 -3
- package/skills/theme/SKILL.md +5 -4
- package/skills/typesafety/SKILL.md +59 -16
- package/skills/use-cache/SKILL.md +16 -2
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +56 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +29 -48
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +19 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +66 -443
- package/src/browser/navigation-client.ts +34 -62
- package/src/browser/navigation-store.ts +4 -33
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/partial-update.ts +103 -151
- package/src/browser/prefetch/cache.ts +67 -0
- package/src/browser/prefetch/fetch.ts +137 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +154 -44
- package/src/browser/react/NavigationProvider.tsx +32 -0
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +29 -11
- package/src/browser/react/location-state.ts +6 -4
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +23 -45
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +21 -64
- package/src/browser/react/use-navigation.ts +7 -32
- package/src/browser/react/use-params.ts +5 -34
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +3 -6
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +75 -114
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +46 -22
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +458 -405
- package/src/browser/types.ts +21 -35
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +38 -13
- package/src/build/generate-route-types.ts +4 -0
- package/src/build/index.ts +1 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +170 -18
- package/src/build/runtime-discovery.ts +13 -1
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +136 -123
- package/src/cache/cache-scope.ts +76 -83
- package/src/cache/cf/cf-cache-store.ts +12 -7
- package/src/cache/document-cache.ts +93 -69
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +43 -69
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +140 -117
- package/src/cache/taint.ts +30 -3
- package/src/cache/types.ts +1 -115
- package/src/client.rsc.tsx +0 -1
- package/src/client.tsx +53 -76
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/index.ts +0 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +53 -10
- package/src/index.ts +73 -43
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +60 -18
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/index.ts +0 -3
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +96 -17
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +6 -11
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +62 -54
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +78 -10
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/match-result.ts +0 -9
- package/src/router/metrics.ts +233 -13
- package/src/router/middleware-types.ts +34 -39
- package/src/router/middleware.ts +290 -130
- package/src/router/pattern-matching.ts +61 -10
- package/src/router/prerender-match.ts +36 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +158 -40
- package/src/router/router-options.ts +223 -1
- package/src/router/router-registry.ts +5 -2
- package/src/router/segment-resolution/fresh.ts +165 -242
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +102 -98
- package/src/router/segment-resolution/revalidation.ts +394 -272
- package/src/router/segment-resolution/static-store.ts +2 -2
- package/src/router/segment-resolution.ts +1 -3
- package/src/router/segment-wrappers.ts +3 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +20 -2
- package/src/router/types.ts +7 -1
- package/src/router.ts +203 -18
- package/src/rsc/handler-context.ts +13 -2
- package/src/rsc/handler.ts +489 -438
- package/src/rsc/helpers.ts +125 -5
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/manifest-init.ts +3 -2
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +245 -19
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +47 -43
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +166 -66
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +20 -2
- package/src/search-params.ts +38 -23
- package/src/server/context.ts +61 -7
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +84 -12
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +275 -49
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +67 -28
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +4 -18
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +6 -1
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +22 -0
- package/src/types/handler-context.ts +103 -16
- package/src/types/index.ts +1 -1
- package/src/types/loader-types.ts +9 -6
- package/src/types/route-config.ts +17 -26
- package/src/types/route-entry.ts +28 -0
- package/src/types/segments.ts +0 -5
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +29 -7
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +27 -9
- package/src/vite/discovery/bundle-postprocess.ts +32 -52
- package/src/vite/discovery/discover-routers.ts +52 -26
- package/src/vite/discovery/prerender-collection.ts +58 -41
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/state.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/index.ts +10 -51
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/expose-internal-ids.ts +4 -3
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/plugins/version-plugin.ts +188 -18
- package/src/vite/rango.ts +61 -36
- package/src/vite/router-discovery.ts +173 -100
- package/src/vite/utils/prerender-utils.ts +81 -0
- package/src/vite/utils/shared-utils.ts +19 -9
- package/skills/testing/SKILL.md +0 -226
- package/src/browser/lru-cache.ts +0 -61
- package/src/browser/react/prefetch.ts +0 -27
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/route-definition/route-function.ts +0 -119
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/{CLAUDE.md → AGENTS.md} +0 -0
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "../server/context";
|
|
17
17
|
import { invariant } from "../errors";
|
|
18
18
|
import { isCachedFunction } from "../cache/taint.js";
|
|
19
|
-
import {
|
|
19
|
+
import { RSCRouterContext } from "../server/context";
|
|
20
20
|
import { isStaticHandler } from "../static-handler.js";
|
|
21
21
|
import RootLayout from "../server/root-layout";
|
|
22
22
|
import type {
|
|
@@ -227,7 +227,9 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
227
227
|
children = undefined;
|
|
228
228
|
} else if (typeof optionsOrChildren === "string") {
|
|
229
229
|
// cache('profileName') or cache('profileName', () => [...])
|
|
230
|
-
|
|
230
|
+
// Resolve from context-scoped profiles (set per-router via HelperContext).
|
|
231
|
+
const ctxStore = RSCRouterContext.getStore();
|
|
232
|
+
const profile = ctxStore?.cacheProfiles?.[optionsOrChildren];
|
|
231
233
|
invariant(
|
|
232
234
|
profile,
|
|
233
235
|
`cache("${optionsOrChildren}"): unknown cache profile. ` +
|
|
@@ -245,7 +247,9 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
245
247
|
children = maybeChildren;
|
|
246
248
|
}
|
|
247
249
|
|
|
248
|
-
|
|
250
|
+
// Allocate a single index for this cache() call (used in all paths)
|
|
251
|
+
const cacheIndex = store.getNextIndex("cache");
|
|
252
|
+
const name = `$${cacheIndex}`;
|
|
249
253
|
const cacheConfig = { options };
|
|
250
254
|
|
|
251
255
|
// If no children, create an orphan cache entry (like orphan layouts)
|
|
@@ -262,7 +266,7 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
262
266
|
|
|
263
267
|
// Create orphan cache entry (like orphan layout)
|
|
264
268
|
// Subsequent siblings in the same array will attach to this entry
|
|
265
|
-
const namespace = `${ctx.namespace}.${
|
|
269
|
+
const namespace = `${ctx.namespace}.${cacheIndex}`;
|
|
266
270
|
const cacheUrlPrefix = getUrlPrefix();
|
|
267
271
|
|
|
268
272
|
const entry = {
|
|
@@ -297,8 +301,7 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
297
301
|
}
|
|
298
302
|
|
|
299
303
|
// With children: create a cache entry (like layout with caching semantics)
|
|
300
|
-
const
|
|
301
|
-
const namespace = `${ctx.namespace}.${cacheNextIndex}`;
|
|
304
|
+
const namespace = `${ctx.namespace}.${cacheIndex}`;
|
|
302
305
|
const cacheShortCode = store.getShortCode("cache");
|
|
303
306
|
|
|
304
307
|
const cacheUrlPrefix2 = getUrlPrefix();
|
|
@@ -43,11 +43,16 @@ import {
|
|
|
43
43
|
export function redirect(url: string, status?: number): Response;
|
|
44
44
|
export function redirect(
|
|
45
45
|
url: string,
|
|
46
|
-
options: {
|
|
46
|
+
options: {
|
|
47
|
+
status?: number;
|
|
48
|
+
state?: LocationStateEntry | LocationStateEntry[];
|
|
49
|
+
},
|
|
47
50
|
): Response;
|
|
48
51
|
export function redirect(
|
|
49
52
|
url: string,
|
|
50
|
-
statusOrOptions?:
|
|
53
|
+
statusOrOptions?:
|
|
54
|
+
| number
|
|
55
|
+
| { status?: number; state?: LocationStateEntry | LocationStateEntry[] },
|
|
51
56
|
): Response {
|
|
52
57
|
const status =
|
|
53
58
|
typeof statusOrOptions === "number"
|
|
@@ -62,7 +67,14 @@ export function redirect(
|
|
|
62
67
|
|
|
63
68
|
if (process.env.NODE_ENV !== "production") {
|
|
64
69
|
const reqCtx = getRequestContext();
|
|
65
|
-
|
|
70
|
+
// Warn only on true full-page SSR loads. SPA partial requests and server
|
|
71
|
+
// actions both deliver state through Flight payloads, so suppress for those.
|
|
72
|
+
if (
|
|
73
|
+
reqCtx &&
|
|
74
|
+
!reqCtx.url.searchParams.has("_rsc_partial") &&
|
|
75
|
+
!reqCtx.request.headers.has("rsc-action") &&
|
|
76
|
+
!reqCtx.url.searchParams.has("_rsc_action")
|
|
77
|
+
) {
|
|
66
78
|
console.warn(
|
|
67
79
|
`[Router] redirect() with state during a full-page (SSR) request to "${url}". ` +
|
|
68
80
|
"Location state is only delivered during SPA navigations and will be lost on this request.",
|
package/src/route-map-builder.ts
CHANGED
|
@@ -128,15 +128,23 @@ const perRouterPrecomputedEntriesMap: Map<
|
|
|
128
128
|
> = new Map();
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
|
-
* Clear all
|
|
131
|
+
* Clear all cached route data (global and per-router).
|
|
132
132
|
* Called during HMR when route definitions change so the handler rebuilds
|
|
133
133
|
* the trie from the updated router.urlpatterns on the next request.
|
|
134
|
+
*
|
|
135
|
+
* The virtual module calls this before repopulating with fresh data,
|
|
136
|
+
* preventing stale entries from removed routes from accumulating.
|
|
134
137
|
*/
|
|
135
138
|
export function clearAllRouterData(): void {
|
|
139
|
+
globalRouteMap = {};
|
|
140
|
+
cachedManifest = null;
|
|
141
|
+
cachedPrecomputedEntries = null;
|
|
142
|
+
cachedRouteTrie = null;
|
|
143
|
+
rootScopeRoutes.clear();
|
|
144
|
+
globalSearchSchemas.clear();
|
|
136
145
|
perRouterManifestMap.clear();
|
|
137
146
|
perRouterTrieMap.clear();
|
|
138
147
|
perRouterPrecomputedEntriesMap.clear();
|
|
139
|
-
cachedRouteTrie = null;
|
|
140
148
|
}
|
|
141
149
|
|
|
142
150
|
export function setRouterManifest(
|
|
@@ -217,6 +225,34 @@ export function waitForManifestReady(): Promise<void> | null {
|
|
|
217
225
|
return manifestReadyPromise;
|
|
218
226
|
}
|
|
219
227
|
|
|
228
|
+
// ============================================================================
|
|
229
|
+
// Route Scope Registry
|
|
230
|
+
// ============================================================================
|
|
231
|
+
|
|
232
|
+
// Tracks whether each route is at root scope (no named include boundary above).
|
|
233
|
+
// Used by dot-local reverse resolution to decide whether bare-name fallback
|
|
234
|
+
// is allowed after scoped lookups are exhausted.
|
|
235
|
+
const rootScopeRoutes: Map<string, boolean> = new Map();
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Register whether a route is at root scope.
|
|
239
|
+
* Called by path() during route evaluation.
|
|
240
|
+
*/
|
|
241
|
+
export function registerRouteRootScope(
|
|
242
|
+
routeName: string,
|
|
243
|
+
rootScoped: boolean,
|
|
244
|
+
): void {
|
|
245
|
+
rootScopeRoutes.set(routeName, rootScoped);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Check if a route is at root scope.
|
|
250
|
+
* Returns undefined if the route has not been registered (e.g. in unit tests).
|
|
251
|
+
*/
|
|
252
|
+
export function isRouteRootScoped(routeName: string): boolean | undefined {
|
|
253
|
+
return rootScopeRoutes.get(routeName);
|
|
254
|
+
}
|
|
255
|
+
|
|
220
256
|
// ============================================================================
|
|
221
257
|
// Search Schema Registry
|
|
222
258
|
// ============================================================================
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route name utilities for filtering internal route names.
|
|
3
|
+
*
|
|
4
|
+
* Internal names stay active in the runtime manifest for matching and local
|
|
5
|
+
* reverse() resolution, but they must not leak into public APIs or generated
|
|
6
|
+
* route maps.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const AUTO_GENERATED_ROUTE_PREFIX = "$path_";
|
|
10
|
+
export const INTERNAL_INCLUDE_SCOPE_PREFIX = "$prefix_";
|
|
11
|
+
|
|
12
|
+
const RESERVED_PREFIXES = [
|
|
13
|
+
AUTO_GENERATED_ROUTE_PREFIX,
|
|
14
|
+
INTERNAL_INCLUDE_SCOPE_PREFIX,
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if a route name is internal.
|
|
19
|
+
* Internal names include:
|
|
20
|
+
* - unnamed path() routes like "$path__health" or "docs.$path__health"
|
|
21
|
+
* - hidden include scopes like "$prefix_0.index" or "blog.$prefix_1.post"
|
|
22
|
+
*
|
|
23
|
+
* User-defined names containing "$" (e.g. "docs.$admin") are valid and must
|
|
24
|
+
* be preserved.
|
|
25
|
+
*/
|
|
26
|
+
export function isAutoGeneratedRouteName(name: string): boolean {
|
|
27
|
+
return name.split(".").some((segment) => {
|
|
28
|
+
return (
|
|
29
|
+
segment.startsWith(AUTO_GENERATED_ROUTE_PREFIX) ||
|
|
30
|
+
segment.startsWith(INTERNAL_INCLUDE_SCOPE_PREFIX)
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Validate that a user-provided route name does not collide with
|
|
37
|
+
* reserved internal prefixes. Checks every dot-separated segment,
|
|
38
|
+
* mirroring the same rule used by isAutoGeneratedRouteName().
|
|
39
|
+
*
|
|
40
|
+
* Throws with a clear message when a reserved prefix is detected.
|
|
41
|
+
*/
|
|
42
|
+
export function validateUserRouteName(name: string): void {
|
|
43
|
+
for (const segment of name.split(".")) {
|
|
44
|
+
for (const prefix of RESERVED_PREFIXES) {
|
|
45
|
+
if (segment.startsWith(prefix)) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Route name "${name}" contains segment "${segment}" which uses reserved internal prefix "${prefix}". ` +
|
|
48
|
+
`Choose a different name to avoid collision with auto-generated route names.`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/route-types.ts
CHANGED
|
@@ -169,6 +169,13 @@ export type IncludeItem = {
|
|
|
169
169
|
parent: unknown; // EntryData - avoid circular import
|
|
170
170
|
/** Counter snapshot from pattern extraction for consistent shortCode indices */
|
|
171
171
|
counters?: Record<string, number>;
|
|
172
|
+
/** Cache profiles for DSL-time cache("profileName") resolution */
|
|
173
|
+
cacheProfiles?: Record<
|
|
174
|
+
string,
|
|
175
|
+
import("./cache/profile-registry.js").CacheProfile
|
|
176
|
+
>;
|
|
177
|
+
/** Root scope flag for dot-local reverse resolution */
|
|
178
|
+
rootScoped?: boolean;
|
|
172
179
|
};
|
|
173
180
|
[IncludeBrand]: void;
|
|
174
181
|
};
|
|
@@ -52,7 +52,7 @@ export function parseAcceptTypes(accept: string): AcceptEntry[] {
|
|
|
52
52
|
for (let i = 0; i < parts.length; i++) {
|
|
53
53
|
const part = parts[i]!;
|
|
54
54
|
const segments = part.split(";");
|
|
55
|
-
const mime = segments[0]!.trim();
|
|
55
|
+
const mime = segments[0]!.trim().toLowerCase();
|
|
56
56
|
if (!mime) continue;
|
|
57
57
|
let q = 1.0;
|
|
58
58
|
for (let j = 1; j < segments.length; j++) {
|
|
@@ -45,10 +45,23 @@ export async function buildDebugManifest<TEnv = any>(
|
|
|
45
45
|
if (promiseResult !== null) {
|
|
46
46
|
const load = await (promiseResult as Promise<any>);
|
|
47
47
|
if (load && typeof load === "object" && "default" in load) {
|
|
48
|
-
|
|
49
|
-
if (typeof
|
|
50
|
-
|
|
48
|
+
// Promise<{ default: fn }> — e.g. dynamic import
|
|
49
|
+
if (typeof load.default !== "function") {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`[@rangojs/router] Unsupported async handler: { default } must be a function, ` +
|
|
52
|
+
`got ${typeof load.default}. Use () => import('./urls') for lazy loading.`,
|
|
53
|
+
);
|
|
51
54
|
}
|
|
55
|
+
load.default(helpers);
|
|
56
|
+
} else if (typeof load === "function") {
|
|
57
|
+
// Promise<fn>
|
|
58
|
+
load(helpers);
|
|
59
|
+
} else {
|
|
60
|
+
// Reject unsupported async handler results (same policy as manifest.ts)
|
|
61
|
+
throw new Error(
|
|
62
|
+
`[@rangojs/router] Unsupported async handler result (${typeof load}). ` +
|
|
63
|
+
`Lazy route handlers must resolve to a function or { default: fn }.`,
|
|
64
|
+
);
|
|
52
65
|
}
|
|
53
66
|
}
|
|
54
67
|
},
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { HandlerContext, InternalHandlerContext } from "../types";
|
|
8
|
-
import {
|
|
9
|
-
import { getSearchSchema } from "../route-map-builder.js";
|
|
8
|
+
import { _getRequestContext } from "../server/request-context.js";
|
|
9
|
+
import { getSearchSchema, isRouteRootScoped } from "../route-map-builder.js";
|
|
10
10
|
import { parseSearchParams, serializeSearchParams } from "../search-params.js";
|
|
11
11
|
import { contextGet, contextSet } from "../context-var.js";
|
|
12
12
|
import { NOCACHE_SYMBOL } from "../cache/taint.js";
|
|
13
|
+
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
14
|
+
import { PRERENDER_PASSTHROUGH } from "../prerender.js";
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Strip internal _rsc* query params from a URL.
|
|
@@ -28,11 +30,15 @@ export function stripInternalParams(url: URL): URL {
|
|
|
28
30
|
/**
|
|
29
31
|
* Resolve route name with namespace prefix support.
|
|
30
32
|
* Supports local names (dot-prefixed) and absolute names (global lookup).
|
|
33
|
+
*
|
|
34
|
+
* @param rootScoped - Explicit override for root-scope check. When undefined,
|
|
35
|
+
* falls back to the global scope registry, then to a heuristic.
|
|
31
36
|
*/
|
|
32
37
|
function resolveRouteName(
|
|
33
38
|
name: string,
|
|
34
39
|
routeMap: Record<string, string>,
|
|
35
40
|
currentRoutePrefix?: string,
|
|
41
|
+
rootScoped?: boolean,
|
|
36
42
|
): string | undefined {
|
|
37
43
|
// 1. Dot-prefixed (".article", ".author.posts") — local resolution only.
|
|
38
44
|
// Resolves within the current include() scope using the mount prefix.
|
|
@@ -65,6 +71,23 @@ function resolveRouteName(
|
|
|
65
71
|
}
|
|
66
72
|
}
|
|
67
73
|
|
|
74
|
+
// Fallback: try bare name at root scope only.
|
|
75
|
+
// Routes inside { name: "" } mounts are at root scope — their dot-local
|
|
76
|
+
// names can fall back to bare names (e.g., "sub.detail" reaching "flatIndex").
|
|
77
|
+
// Routes inside named mounts (e.g., { name: "magazine" }) are NOT at root
|
|
78
|
+
// scope — dot-local must not leak into unrelated global names.
|
|
79
|
+
//
|
|
80
|
+
// Resolution order: explicit param > scope registry > heuristic.
|
|
81
|
+
const isRootScoped =
|
|
82
|
+
rootScoped ??
|
|
83
|
+
isRouteRootScoped(currentRoutePrefix) ??
|
|
84
|
+
!currentRoutePrefix.includes(".");
|
|
85
|
+
if (isRootScoped) {
|
|
86
|
+
if (routeMap[lookupName] !== undefined) {
|
|
87
|
+
return routeMap[lookupName];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
68
91
|
return undefined;
|
|
69
92
|
}
|
|
70
93
|
|
|
@@ -73,6 +96,27 @@ function resolveRouteName(
|
|
|
73
96
|
return routeMap[name];
|
|
74
97
|
}
|
|
75
98
|
|
|
99
|
+
function createPrerenderPassthroughFn(
|
|
100
|
+
build: boolean,
|
|
101
|
+
isPassthroughRoute: boolean,
|
|
102
|
+
): () => typeof PRERENDER_PASSTHROUGH {
|
|
103
|
+
return () => {
|
|
104
|
+
if (!build) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
"ctx.passthrough() can only be called during build-time prerendering.",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (!isPassthroughRoute) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
"ctx.passthrough() is only available on routes declared with " +
|
|
112
|
+
"{ passthrough: true }. Remove the passthrough() call or add " +
|
|
113
|
+
"{ passthrough: true } to the Prerender options.",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return PRERENDER_PASSTHROUGH;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
76
120
|
/**
|
|
77
121
|
* Create a reverse function for URL generation from route names.
|
|
78
122
|
* Used by both HandlerContext and MiddlewareContext.
|
|
@@ -87,6 +131,7 @@ export function createReverseFunction(
|
|
|
87
131
|
routeMap: Record<string, string>,
|
|
88
132
|
currentRoutePrefix?: string,
|
|
89
133
|
currentParams?: Record<string, string>,
|
|
134
|
+
rootScoped?: boolean,
|
|
90
135
|
): (
|
|
91
136
|
name: string,
|
|
92
137
|
hrefParams?: Record<string, string>,
|
|
@@ -94,7 +139,12 @@ export function createReverseFunction(
|
|
|
94
139
|
) => string {
|
|
95
140
|
return (name, hrefParams, search) => {
|
|
96
141
|
// Resolve route name with namespace support
|
|
97
|
-
const pattern = resolveRouteName(
|
|
142
|
+
const pattern = resolveRouteName(
|
|
143
|
+
name,
|
|
144
|
+
routeMap,
|
|
145
|
+
currentRoutePrefix,
|
|
146
|
+
rootScoped,
|
|
147
|
+
);
|
|
98
148
|
|
|
99
149
|
if (pattern === undefined) {
|
|
100
150
|
throw new Error(
|
|
@@ -109,10 +159,10 @@ export function createReverseFunction(
|
|
|
109
159
|
? { ...currentParams, ...hrefParams }
|
|
110
160
|
: hrefParams;
|
|
111
161
|
|
|
112
|
-
// Substitute params (strip constraint syntax: :param(a|b) -> value)
|
|
162
|
+
// Substitute params (strip constraint and optional syntax: :param(a|b)? -> value)
|
|
113
163
|
if (effectiveParams) {
|
|
114
164
|
result = result.replace(
|
|
115
|
-
/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))
|
|
165
|
+
/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?\??/g,
|
|
116
166
|
(_, key) => {
|
|
117
167
|
const value = effectiveParams[key];
|
|
118
168
|
if (value === undefined) {
|
|
@@ -146,10 +196,11 @@ export function createHandlerContext<TEnv>(
|
|
|
146
196
|
routeMap: Record<string, string> = {},
|
|
147
197
|
routeName?: string,
|
|
148
198
|
responseType?: string,
|
|
199
|
+
isPassthroughRoute: boolean = false,
|
|
149
200
|
): InternalHandlerContext<any, TEnv> {
|
|
150
201
|
// Get variables from request context - this is the unified context
|
|
151
202
|
// shared between middleware and route handlers
|
|
152
|
-
const requestContext =
|
|
203
|
+
const requestContext = _getRequestContext();
|
|
153
204
|
const variables: any = requestContext?.var ?? {};
|
|
154
205
|
|
|
155
206
|
// If route has a search schema, parse URLSearchParams into typed object
|
|
@@ -179,7 +230,6 @@ export function createHandlerContext<TEnv>(
|
|
|
179
230
|
set: ((keyOrVar: any, value: any) => {
|
|
180
231
|
contextSet(variables, keyOrVar, value);
|
|
181
232
|
}) as HandlerContext<any, TEnv>["set"],
|
|
182
|
-
_originalRequest: request, // Raw request for advanced use
|
|
183
233
|
res: stubResponse, // Stub response for setting headers
|
|
184
234
|
headers: stubResponse.headers, // Shorthand for res.headers
|
|
185
235
|
// Placeholder use() - will be replaced with actual implementation during request
|
|
@@ -198,9 +248,21 @@ export function createHandlerContext<TEnv>(
|
|
|
198
248
|
}
|
|
199
249
|
requestContext.setLocationState(entries);
|
|
200
250
|
},
|
|
201
|
-
|
|
202
|
-
|
|
251
|
+
routeName: (routeName && !isAutoGeneratedRouteName(routeName)
|
|
252
|
+
? routeName
|
|
253
|
+
: undefined) as HandlerContext["routeName"],
|
|
254
|
+
// Scoped reverse for URL generation (auto-fills current request params).
|
|
255
|
+
// Resolve rootScoped eagerly so the reverse function is self-contained
|
|
256
|
+
// and does not depend on the global rootScopeRoutes registry at call time.
|
|
257
|
+
reverse: createReverseFunction(
|
|
258
|
+
routeMap,
|
|
259
|
+
routeName,
|
|
260
|
+
params,
|
|
261
|
+
routeName ? isRouteRootScoped(routeName) : undefined,
|
|
262
|
+
),
|
|
263
|
+
passthrough: createPrerenderPassthroughFn(false, isPassthroughRoute),
|
|
203
264
|
_responseType: responseType,
|
|
265
|
+
_routeName: routeName,
|
|
204
266
|
};
|
|
205
267
|
// Brand with taint symbol so "use cache" excludes ctx from cache keys
|
|
206
268
|
(ctx as any)[NOCACHE_SYMBOL] = true;
|
|
@@ -220,6 +282,7 @@ export function createPrerenderContext<TEnv>(
|
|
|
220
282
|
routeMap: Record<string, string>,
|
|
221
283
|
routeName?: string,
|
|
222
284
|
buildVars?: Record<string, any>,
|
|
285
|
+
isPassthroughRoute?: boolean,
|
|
223
286
|
): InternalHandlerContext<any, TEnv> {
|
|
224
287
|
const syntheticUrl = new URL(`http://prerender${pathname}`);
|
|
225
288
|
const variables = buildVars ?? {};
|
|
@@ -251,9 +314,6 @@ export function createPrerenderContext<TEnv>(
|
|
|
251
314
|
set: ((keyOrVar: any, value: any) => {
|
|
252
315
|
contextSet(variables, keyOrVar, value);
|
|
253
316
|
}) as any,
|
|
254
|
-
get _originalRequest(): Request {
|
|
255
|
-
return throwUnavailable("request");
|
|
256
|
-
},
|
|
257
317
|
get res(): Response {
|
|
258
318
|
return throwUnavailable("res");
|
|
259
319
|
},
|
|
@@ -266,10 +326,23 @@ export function createPrerenderContext<TEnv>(
|
|
|
266
326
|
},
|
|
267
327
|
theme: undefined,
|
|
268
328
|
setTheme: undefined,
|
|
329
|
+
routeName: (routeName && !isAutoGeneratedRouteName(routeName)
|
|
330
|
+
? routeName
|
|
331
|
+
: undefined) as HandlerContext["routeName"],
|
|
269
332
|
setLocationState: () => {
|
|
270
333
|
throwUnavailable("setLocationState");
|
|
271
334
|
},
|
|
272
|
-
reverse: createReverseFunction(
|
|
335
|
+
reverse: createReverseFunction(
|
|
336
|
+
routeMap,
|
|
337
|
+
routeName,
|
|
338
|
+
params,
|
|
339
|
+
routeName ? isRouteRootScoped(routeName) : undefined,
|
|
340
|
+
),
|
|
341
|
+
passthrough: createPrerenderPassthroughFn(
|
|
342
|
+
true,
|
|
343
|
+
isPassthroughRoute === true,
|
|
344
|
+
),
|
|
345
|
+
_routeName: routeName,
|
|
273
346
|
} as InternalHandlerContext<any, TEnv>;
|
|
274
347
|
}
|
|
275
348
|
|
|
@@ -322,9 +395,6 @@ export function createStaticContext<TEnv>(
|
|
|
322
395
|
set: ((keyOrVar: any, value: any) => {
|
|
323
396
|
contextSet(variables, keyOrVar, value);
|
|
324
397
|
}) as any,
|
|
325
|
-
get _originalRequest(): Request {
|
|
326
|
-
return throwUnavailable("request");
|
|
327
|
-
},
|
|
328
398
|
get res(): Response {
|
|
329
399
|
return throwUnavailable("res");
|
|
330
400
|
},
|
|
@@ -337,9 +407,18 @@ export function createStaticContext<TEnv>(
|
|
|
337
407
|
},
|
|
338
408
|
theme: undefined,
|
|
339
409
|
setTheme: undefined,
|
|
410
|
+
routeName: (routeName && !isAutoGeneratedRouteName(routeName)
|
|
411
|
+
? routeName
|
|
412
|
+
: undefined) as HandlerContext["routeName"],
|
|
340
413
|
setLocationState: () => {
|
|
341
414
|
throwUnavailable("setLocationState");
|
|
342
415
|
},
|
|
343
|
-
reverse: createReverseFunction(
|
|
416
|
+
reverse: createReverseFunction(
|
|
417
|
+
routeMap,
|
|
418
|
+
routeName,
|
|
419
|
+
undefined,
|
|
420
|
+
routeName ? isRouteRootScoped(routeName) : undefined,
|
|
421
|
+
),
|
|
422
|
+
_routeName: routeName,
|
|
344
423
|
} as InternalHandlerContext<any, TEnv>;
|
|
345
424
|
}
|
|
@@ -384,10 +384,12 @@ export async function resolveInterceptLoadersOnly<TEnv>(
|
|
|
384
384
|
return null;
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
387
|
+
// Match fresh-path semantics: only defer (no await) when loading is truthy.
|
|
388
|
+
// `loading: false` means "no loading UI, await loaders before render" —
|
|
389
|
+
// same as the fresh path's `if (interceptEntry.loading && ...)` check.
|
|
390
|
+
const loaderDataPromise = interceptEntry.loading
|
|
391
|
+
? Promise.all(loaderPromises)
|
|
392
|
+
: await Promise.all(loaderPromises);
|
|
391
393
|
|
|
392
394
|
return { loaderDataPromise, loaderIds };
|
|
393
395
|
}
|
|
@@ -28,6 +28,7 @@ export function findLazyIncludes<TEnv = any>(
|
|
|
28
28
|
urlPrefix: string;
|
|
29
29
|
namePrefix: string | undefined;
|
|
30
30
|
parent: unknown;
|
|
31
|
+
rootScoped?: boolean;
|
|
31
32
|
};
|
|
32
33
|
}> {
|
|
33
34
|
const lazyItems: Array<{
|
|
@@ -37,6 +38,7 @@ export function findLazyIncludes<TEnv = any>(
|
|
|
37
38
|
urlPrefix: string;
|
|
38
39
|
namePrefix: string | undefined;
|
|
39
40
|
parent: unknown;
|
|
41
|
+
rootScoped?: boolean;
|
|
40
42
|
};
|
|
41
43
|
}> = [];
|
|
42
44
|
|
|
@@ -137,6 +139,8 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
137
139
|
namespace: "lazy",
|
|
138
140
|
parent: (lazyContext?.parent as EntryData | null) ?? null,
|
|
139
141
|
counters: lazyCounters,
|
|
142
|
+
cacheProfiles: (lazyContext as any)?.cacheProfiles,
|
|
143
|
+
rootScoped: (lazyContext as any)?.rootScoped,
|
|
140
144
|
},
|
|
141
145
|
() => {
|
|
142
146
|
// Run the lazy patterns handler with the original context prefixes
|
|
@@ -22,7 +22,7 @@ import type { LoaderRevalidationResult, ActionContext } from "./types";
|
|
|
22
22
|
import { isHandle, type Handle } from "../handle.js";
|
|
23
23
|
import type { HandleStore } from "../server/handle-store.js";
|
|
24
24
|
import { getFetchableLoader } from "../server/fetchable-loader-store.js";
|
|
25
|
-
import {
|
|
25
|
+
import { _getRequestContext } from "../server/request-context.js";
|
|
26
26
|
import { debugLog } from "./logging.js";
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -182,7 +182,7 @@ function createLoaderExecutor<TEnv>(
|
|
|
182
182
|
callerLoaderId: string | null,
|
|
183
183
|
) => Promise<any> {
|
|
184
184
|
// Capture RequestContext eagerly for cookie access (ALS protection on Cloudflare)
|
|
185
|
-
const reqCtxRef =
|
|
185
|
+
const reqCtxRef = _getRequestContext();
|
|
186
186
|
|
|
187
187
|
// Dependency graph: loaderId -> set of loader IDs it directly depends on.
|
|
188
188
|
const dependsOn = new Map<string, Set<string>>();
|
|
@@ -243,6 +243,7 @@ function createLoaderExecutor<TEnv>(
|
|
|
243
243
|
const currentLoaderId = loader.$$id;
|
|
244
244
|
const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
|
|
245
245
|
params: ctx.params,
|
|
246
|
+
routeParams: (ctx.params ?? {}) as Record<string, string>,
|
|
246
247
|
request: ctx.request,
|
|
247
248
|
searchParams: ctx.searchParams,
|
|
248
249
|
search: (ctx as any).search,
|
|
@@ -251,12 +252,6 @@ function createLoaderExecutor<TEnv>(
|
|
|
251
252
|
env: ctx.env,
|
|
252
253
|
var: ctx.var,
|
|
253
254
|
get: ctx.get,
|
|
254
|
-
cookie(name: string) {
|
|
255
|
-
return reqCtxRef?.cookie(name);
|
|
256
|
-
},
|
|
257
|
-
cookies() {
|
|
258
|
-
return reqCtxRef?.cookies() ?? {};
|
|
259
|
-
},
|
|
260
255
|
use: <TDep, TDepParams = any>(
|
|
261
256
|
dep: LoaderDefinition<TDep, TDepParams>,
|
|
262
257
|
): Promise<TDep> => {
|
|
@@ -267,7 +262,7 @@ function createLoaderExecutor<TEnv>(
|
|
|
267
262
|
reverse: ctx.reverse as LoaderContext["reverse"],
|
|
268
263
|
};
|
|
269
264
|
|
|
270
|
-
const doneLoader = track(`loader:${loader.$$id}
|
|
265
|
+
const doneLoader = track(`loader:${loader.$$id}`, 2);
|
|
271
266
|
const promise = Promise.resolve(
|
|
272
267
|
loaderFn(loaderCtx as LoaderContext<any, TEnv>),
|
|
273
268
|
).finally(() => {
|
|
@@ -300,7 +295,7 @@ export function setupLoaderAccess<TEnv>(
|
|
|
300
295
|
// can disrupt AsyncLocalStorage, causing getRequestContext() to return
|
|
301
296
|
// undefined when handlers later call ctx.use(handle). Capturing early
|
|
302
297
|
// ensures the store reference survives ALS disruption.
|
|
303
|
-
const handleStoreRef =
|
|
298
|
+
const handleStoreRef = _getRequestContext()?._handleStore;
|
|
304
299
|
|
|
305
300
|
const useLoader = createLoaderExecutor(ctx, loaderPromises);
|
|
306
301
|
|
|
@@ -342,7 +337,7 @@ export function setupLoaderAccess<TEnv>(
|
|
|
342
337
|
*/
|
|
343
338
|
export function setupBuildUse<TEnv>(ctx: HandlerContext<any, TEnv>): void {
|
|
344
339
|
// Eagerly capture the HandleStore (same ALS protection as setupLoaderAccess).
|
|
345
|
-
const handleStoreRef =
|
|
340
|
+
const handleStoreRef = _getRequestContext()?._handleStore;
|
|
346
341
|
|
|
347
342
|
ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
348
343
|
// Handle case: return a push function bound to the current segment
|