@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2
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/AGENTS.md +6 -10
- package/README.md +289 -938
- package/dist/bin/rango.js +271 -46
- package/dist/vite/index.js +673 -193
- package/package.json +10 -8
- package/skills/api-client/SKILL.md +1 -1
- package/skills/breadcrumbs/SKILL.md +31 -14
- package/skills/cache-guide/SKILL.md +5 -2
- package/skills/caching/SKILL.md +59 -4
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/debug-manifest/SKILL.md +1 -1
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +9 -1
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +8 -8
- package/skills/hooks/SKILL.md +54 -892
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +4 -4
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +46 -14
- package/skills/layout/SKILL.md +27 -10
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +23 -1
- package/skills/middleware/SKILL.md +7 -3
- package/skills/migrate-nextjs/SKILL.md +167 -6
- package/skills/migrate-react-router/SKILL.md +59 -677
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/observability/SKILL.md +9 -1
- package/skills/parallel/SKILL.md +23 -4
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +28 -18
- package/skills/rango/SKILL.md +84 -25
- package/skills/response-routes/SKILL.md +15 -1
- package/skills/route/SKILL.md +71 -4
- package/skills/router-setup/SKILL.md +14 -3
- package/skills/scripts/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +3 -2
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +1 -1
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/handles.md +4 -2
- package/skills/testing/render-handler.md +15 -14
- package/skills/testing/reverse-and-types.md +8 -7
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -919
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +30 -3
- package/skills/vercel/SKILL.md +1 -1
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/browser/event-controller.ts +62 -10
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +65 -16
- package/src/browser/navigation-client.ts +32 -2
- package/src/browser/navigation-store.ts +128 -14
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +76 -17
- package/src/browser/prefetch/cache.ts +51 -11
- package/src/browser/prefetch/fetch.ts +59 -21
- package/src/browser/prefetch/queue.ts +19 -4
- package/src/browser/react/Link.tsx +13 -3
- package/src/browser/react/NavigationProvider.tsx +108 -4
- package/src/browser/response-adapter.ts +38 -9
- package/src/browser/rsc-router.tsx +54 -4
- package/src/browser/scroll-restoration.ts +7 -5
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +22 -10
- package/src/browser/types.ts +54 -1
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/index.ts +3 -1
- package/src/build/route-trie.ts +35 -7
- package/src/build/route-types/include-resolution.ts +347 -47
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-runtime.ts +262 -71
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +243 -20
- package/src/cache/document-cache.ts +54 -21
- package/src/cache/index.ts +1 -0
- package/src/cache/memory-segment-store.ts +110 -3
- package/src/cache/profile-registry.ts +15 -0
- package/src/cache/read-through-swr.ts +15 -1
- package/src/cache/segment-codec.ts +4 -4
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/types.ts +158 -0
- package/src/cache/vercel/vercel-cache-store.ts +401 -124
- package/src/client.rsc.tsx +0 -3
- package/src/client.tsx +0 -3
- package/src/cloudflare/tracing.ts +7 -8
- package/src/defer.ts +11 -22
- package/src/handle.ts +37 -15
- package/src/handles/MetaTags.tsx +16 -82
- package/src/handles/breadcrumbs.ts +12 -14
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +7 -8
- package/src/handles/meta.ts +7 -44
- package/src/host/errors.ts +15 -0
- package/src/host/index.ts +1 -0
- package/src/index.rsc.ts +8 -2
- package/src/index.ts +19 -13
- package/src/internal-debug.ts +11 -8
- package/src/prerender.ts +17 -4
- package/src/redirect-origin.ts +14 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/route-content-wrapper.tsx +12 -5
- package/src/route-definition/dsl-helpers.ts +21 -32
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +43 -43
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/resolve-handler-use.ts +0 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -4
- package/src/route-types.ts +0 -5
- package/src/router/find-match.ts +86 -8
- package/src/router/instrument.ts +9 -4
- package/src/router/lazy-includes.ts +72 -12
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/manifest.ts +56 -11
- package/src/router/match-api.ts +76 -32
- package/src/router/match-handlers.ts +181 -135
- package/src/router/match-middleware/background-revalidation.ts +40 -23
- package/src/router/match-middleware/cache-store.ts +39 -24
- package/src/router/match-result.ts +35 -15
- package/src/router/middleware.ts +64 -38
- package/src/router/navigation-snapshot.ts +7 -5
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +53 -64
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +11 -5
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +23 -8
- package/src/router/route-snapshot.ts +14 -2
- package/src/router/router-context.ts +3 -1
- package/src/router/router-interfaces.ts +32 -1
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +39 -3
- package/src/router/segment-resolution/loader-cache.ts +93 -2
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +3 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +6 -8
- package/src/router/telemetry.ts +9 -1
- package/src/router/tracing.ts +14 -5
- package/src/router/trie-matching.ts +19 -11
- package/src/router/url-params.ts +13 -0
- package/src/router.ts +47 -16
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler.ts +60 -33
- package/src/rsc/manifest-init.ts +1 -1
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/progressive-enhancement.ts +61 -4
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +429 -37
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +30 -0
- package/src/segment-loader-promise.ts +18 -0
- package/src/segment-system.tsx +149 -14
- package/src/server/context.ts +67 -9
- package/src/server/cookie-store.ts +73 -1
- package/src/server/loader-registry.ts +13 -1
- package/src/server/request-context.ts +169 -10
- package/src/ssr/index.tsx +462 -178
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/testing/collect-handle.ts +14 -8
- package/src/testing/dispatch.ts +152 -40
- package/src/testing/generated-routes.ts +27 -11
- package/src/testing/index.ts +6 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/render-route.tsx +13 -10
- package/src/testing/run-transition-when.ts +164 -0
- package/src/theme/ThemeProvider.tsx +36 -26
- package/src/types/handler-context.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/route-config.ts +19 -7
- package/src/types/segments.ts +100 -0
- package/src/urls/include-helper.ts +10 -8
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +44 -12
- package/src/urls/path-helper.ts +5 -0
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +43 -18
- package/src/urls/urls-function.ts +0 -1
- package/src/vercel/tracing.ts +7 -7
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/index.ts +7 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugins/vercel-output.ts +114 -25
- package/src/vite/plugins/version-injector.ts +22 -7
- package/src/vite/plugins/virtual-entries.ts +80 -22
- package/src/vite/rango.ts +29 -19
- package/src/vite/router-discovery.ts +171 -43
- package/src/vite/utils/prerender-utils.ts +17 -4
- package/src/vite/utils/shared-utils.ts +47 -0
- package/src/network-error-thrower.tsx +0 -18
package/src/route-map-builder.ts
CHANGED
|
@@ -20,11 +20,25 @@ let cachedPrecomputedEntries: Array<{
|
|
|
20
20
|
/**
|
|
21
21
|
* Register routes into the global route map.
|
|
22
22
|
* Routes are merged with any existing registered routes.
|
|
23
|
-
* Called by createRouter() during module evaluation
|
|
23
|
+
* Called by createRouter() during module evaluation, and by lazy-include
|
|
24
|
+
* expansion (src/router/lazy-includes.ts) with each expansion's route delta.
|
|
25
|
+
*
|
|
26
|
+
* Merges IN PLACE — O(|map|), not O(total routes). The previous
|
|
27
|
+
* `globalRouteMap = { ...globalRouteMap, ...map }` copy made every
|
|
28
|
+
* lazy-include first hit O(total routes) on the request path: with a 26k-route
|
|
29
|
+
* manifest the spread measured 8.9ms/call (M4, node), paid once per level of a
|
|
30
|
+
* nested async-include chain (3 calls on a 3-level chain — the 464ms edge
|
|
31
|
+
* cold-hit in issue #666).
|
|
32
|
+
*
|
|
33
|
+
* In-place mutation is safe because every getGlobalRouteMap() consumer reads
|
|
34
|
+
* it fresh per call (server/request-context.ts, rsc/loader-fetch.ts,
|
|
35
|
+
* router/intercept-resolution.ts, testing/generated-routes.ts,
|
|
36
|
+
* rsc/manifest-init.ts) — none memoizes the returned reference. If you add a
|
|
37
|
+
* consumer that caches the map object, it will now observe later
|
|
38
|
+
* registrations; snapshot it yourself if you need frozen contents.
|
|
24
39
|
*/
|
|
25
40
|
export function registerRouteMap(map: Record<string, string>): void {
|
|
26
|
-
|
|
27
|
-
globalRouteMap = { ...globalRouteMap, ...map };
|
|
41
|
+
Object.assign(globalRouteMap, map);
|
|
28
42
|
}
|
|
29
43
|
|
|
30
44
|
/**
|
|
@@ -139,6 +153,7 @@ export function clearAllRouterData(): void {
|
|
|
139
153
|
perRouterManifestMap.clear();
|
|
140
154
|
perRouterTrieMap.clear();
|
|
141
155
|
perRouterPrecomputedEntriesMap.clear();
|
|
156
|
+
authoritativeTrieRouters.clear();
|
|
142
157
|
}
|
|
143
158
|
|
|
144
159
|
export function setRouterManifest(
|
|
@@ -162,6 +177,23 @@ export function setRouterTrie(
|
|
|
162
177
|
perRouterTrieMap.set(routerId, trie);
|
|
163
178
|
}
|
|
164
179
|
|
|
180
|
+
// Routers whose trie came from the COMPLETE build manifest (deserialized via
|
|
181
|
+
// ensureRouterManifest). For these, a trie miss is a real 404 and findMatch
|
|
182
|
+
// skips the regex fallback scan — the only remaining route-count-proportional
|
|
183
|
+
// match path (#664). Dev rebuilds (manifest-init.ts, router-discovery HMR
|
|
184
|
+
// pushes) deliberately never mark authoritative: the dev-only trie-gap warning
|
|
185
|
+
// in find-match.ts depends on the fallback running on misses, and dev route
|
|
186
|
+
// churn (HMR, dev-time routes) makes a stale-trie 404 unacceptable there.
|
|
187
|
+
const authoritativeTrieRouters: Set<string> = new Set();
|
|
188
|
+
|
|
189
|
+
export function markRouterTrieAuthoritative(routerId: string): void {
|
|
190
|
+
authoritativeTrieRouters.add(routerId);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function isRouterTrieAuthoritative(routerId: string): boolean {
|
|
194
|
+
return authoritativeTrieRouters.has(routerId);
|
|
195
|
+
}
|
|
196
|
+
|
|
165
197
|
export function getRouterTrie(
|
|
166
198
|
routerId: string,
|
|
167
199
|
): import("./build/route-trie.js").TrieNode | undefined {
|
|
@@ -204,7 +236,12 @@ export async function ensureRouterManifest(routerId: string): Promise<void> {
|
|
|
204
236
|
if (loader) {
|
|
205
237
|
const mod = await loader();
|
|
206
238
|
if (mod.manifest) perRouterManifestMap.set(routerId, mod.manifest);
|
|
207
|
-
if (mod.trie)
|
|
239
|
+
if (mod.trie) {
|
|
240
|
+
perRouterTrieMap.set(routerId, mod.trie);
|
|
241
|
+
// A trie serialized into the build manifest comes from complete
|
|
242
|
+
// discovery — misses are authoritative 404s (see find-match.ts).
|
|
243
|
+
markRouterTrieAuthoritative(routerId);
|
|
244
|
+
}
|
|
208
245
|
if (mod.precomputedEntries)
|
|
209
246
|
perRouterPrecomputedEntriesMap.set(routerId, mod.precomputedEntries);
|
|
210
247
|
routerManifestLoaders.delete(routerId);
|
package/src/route-types.ts
CHANGED
|
@@ -98,10 +98,6 @@ export type NotFoundBoundaryItem = {
|
|
|
98
98
|
type: "notFoundBoundary";
|
|
99
99
|
uses?: AllUseItems[];
|
|
100
100
|
};
|
|
101
|
-
export type WhenItem = {
|
|
102
|
-
name: string;
|
|
103
|
-
type: "when";
|
|
104
|
-
};
|
|
105
101
|
export type CacheItem = {
|
|
106
102
|
name: string;
|
|
107
103
|
type: "cache";
|
|
@@ -231,7 +227,6 @@ export type InterceptUseItem =
|
|
|
231
227
|
| NotFoundBoundaryItem
|
|
232
228
|
| LayoutItem
|
|
233
229
|
| RouteItem
|
|
234
|
-
| WhenItem
|
|
235
230
|
| TransitionItem;
|
|
236
231
|
export type LoaderUseItem = RevalidateItem | CacheItem;
|
|
237
232
|
|
package/src/router/find-match.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { tryTrieMatch } from "./trie-matching.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getRouterTrie,
|
|
4
|
+
isRouterTrieAuthoritative,
|
|
5
|
+
} from "../route-map-builder.js";
|
|
3
6
|
import {
|
|
4
7
|
findMatch as findRouteMatch,
|
|
5
8
|
isLazyEvaluationNeeded,
|
|
@@ -20,27 +23,39 @@ function cloneMatchResult<TEnv>(
|
|
|
20
23
|
|
|
21
24
|
export interface FindMatchDeps<TEnv = any> {
|
|
22
25
|
routesEntries: RouteEntry<TEnv>[];
|
|
23
|
-
|
|
26
|
+
// Returns a Promise only when it had to resolve an async include provider
|
|
27
|
+
// (`() => import()`); void for eager/precomputed includes, so the per-entry
|
|
28
|
+
// match loop pays no microtask in the common case.
|
|
29
|
+
evaluateLazyEntry: (entry: RouteEntry<TEnv>) => void | Promise<void>;
|
|
24
30
|
routerId: string;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
/**
|
|
28
34
|
* Create a findMatch function bound to router state.
|
|
29
35
|
* Includes single-entry cache to avoid redundant matching within the same request.
|
|
36
|
+
*
|
|
37
|
+
* Async because a lazy include may be backed by an async provider
|
|
38
|
+
* (`() => import("./routes")`) that must be resolved before its routes can be
|
|
39
|
+
* matched. The hot path is unaffected: `await` only suspends on the first
|
|
40
|
+
* request to a not-yet-loaded dynamic include; eager routes resolve in the same
|
|
41
|
+
* microtask. Every caller already runs inside an async match pipeline.
|
|
30
42
|
*/
|
|
31
43
|
export function createFindMatch<TEnv = any>(
|
|
32
44
|
deps: FindMatchDeps<TEnv>,
|
|
33
|
-
): (
|
|
45
|
+
): (
|
|
46
|
+
pathname: string,
|
|
47
|
+
ms?: MetricsStore,
|
|
48
|
+
) => Promise<RouteMatchResult<TEnv> | null> {
|
|
34
49
|
// Single-entry cache for findMatch to avoid redundant matching within the same request.
|
|
35
50
|
// previewMatch and match both call findMatch with the same pathname — this ensures
|
|
36
51
|
// the route matching work (which may check thousands of routes) only happens once.
|
|
37
52
|
let lastFindMatchPathname: string | null = null;
|
|
38
53
|
let lastFindMatchResult: RouteMatchResult<TEnv> | null = null;
|
|
39
54
|
|
|
40
|
-
return function findMatch(
|
|
55
|
+
return async function findMatch(
|
|
41
56
|
pathname: string,
|
|
42
57
|
ms?: MetricsStore,
|
|
43
|
-
): RouteMatchResult<TEnv> | null {
|
|
58
|
+
): Promise<RouteMatchResult<TEnv> | null> {
|
|
44
59
|
if (lastFindMatchPathname === pathname) {
|
|
45
60
|
return cloneMatchResult(lastFindMatchResult);
|
|
46
61
|
}
|
|
@@ -71,11 +86,33 @@ export function createFindMatch<TEnv = any>(
|
|
|
71
86
|
const entryStart = performance.now();
|
|
72
87
|
let entry: RouteEntry<TEnv> | undefined;
|
|
73
88
|
let fallbackEntry: RouteEntry<TEnv> | undefined;
|
|
89
|
+
let candidateError: unknown;
|
|
90
|
+
let candidateFailed = false;
|
|
74
91
|
|
|
75
92
|
for (const e of deps.routesEntries) {
|
|
76
93
|
if (e.staticPrefix !== trieResult.sp) continue;
|
|
77
94
|
if (!fallbackEntry) fallbackEntry = e;
|
|
78
|
-
|
|
95
|
+
try {
|
|
96
|
+
const ev = deps.evaluateLazyEntry(e);
|
|
97
|
+
if (ev) await ev;
|
|
98
|
+
} catch (err) {
|
|
99
|
+
// Multiple async includes can share one collapsed static prefix, so
|
|
100
|
+
// this loop may evaluate a candidate that is NOT the route's owner.
|
|
101
|
+
// Remember the failure and keep scanning: if another candidate owns
|
|
102
|
+
// the route, this sibling's failure is correctly isolated (its
|
|
103
|
+
// _lazyInflight was cleared, so a later request retries). But if NO
|
|
104
|
+
// candidate ends up owning it, the failing one WAS the owner — a
|
|
105
|
+
// real, trie-matched route whose module failed to import — and we
|
|
106
|
+
// rethrow below as a 5xx rather than silently 404ing (see finding 3:
|
|
107
|
+
// owner failure must stay loud).
|
|
108
|
+
candidateError = err;
|
|
109
|
+
candidateFailed = true;
|
|
110
|
+
console.error(
|
|
111
|
+
`[@rangojs/router] include at "${e.staticPrefix}" failed to load: ` +
|
|
112
|
+
`${(err as Error)?.message ?? String(err)}`,
|
|
113
|
+
);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
79
116
|
if (
|
|
80
117
|
e.routes &&
|
|
81
118
|
trieResult.routeKey in (e.routes as Record<string, unknown>)
|
|
@@ -85,6 +122,14 @@ export function createFindMatch<TEnv = any>(
|
|
|
85
122
|
}
|
|
86
123
|
}
|
|
87
124
|
|
|
125
|
+
// The trie matched this route, so it EXISTS. If no same-prefix candidate
|
|
126
|
+
// could own it AND one failed to load, the owning module is the one that
|
|
127
|
+
// threw — surface the module failure as a 5xx, not the misleading 404 a
|
|
128
|
+
// fallthrough would give.
|
|
129
|
+
if (!entry && candidateFailed) {
|
|
130
|
+
throw candidateError;
|
|
131
|
+
}
|
|
132
|
+
|
|
88
133
|
if (!entry) entry = fallbackEntry;
|
|
89
134
|
|
|
90
135
|
if (!entry) {
|
|
@@ -95,7 +140,8 @@ export function createFindMatch<TEnv = any>(
|
|
|
95
140
|
);
|
|
96
141
|
if (parent) {
|
|
97
142
|
const lazyStart = performance.now();
|
|
98
|
-
deps.evaluateLazyEntry(parent);
|
|
143
|
+
const ev = deps.evaluateLazyEntry(parent);
|
|
144
|
+
if (ev) await ev;
|
|
99
145
|
pushMetric?.("match:lazy-eval", lazyStart);
|
|
100
146
|
}
|
|
101
147
|
entry = deps.routesEntries.find(
|
|
@@ -123,6 +169,17 @@ export function createFindMatch<TEnv = any>(
|
|
|
123
169
|
};
|
|
124
170
|
return cloneMatchResult(lastFindMatchResult);
|
|
125
171
|
}
|
|
172
|
+
} else if (isRouterTrieAuthoritative(deps.routerId)) {
|
|
173
|
+
// Authoritative miss (#664): this trie was deserialized from the
|
|
174
|
+
// COMPLETE build manifest, so trailing-slash redirects are already
|
|
175
|
+
// trie-native hits and a miss means no route exists. Skip the regex
|
|
176
|
+
// fallback — the only route-count-proportional match path — and do
|
|
177
|
+
// not evaluate lazy includes for unmatched (bot-probe) traffic.
|
|
178
|
+
// Trie hits that need lazy splicing keep the fallback loop below
|
|
179
|
+
// (trieMatched === true never reaches this branch).
|
|
180
|
+
lastFindMatchPathname = pathname;
|
|
181
|
+
lastFindMatchResult = null;
|
|
182
|
+
return null;
|
|
126
183
|
}
|
|
127
184
|
}
|
|
128
185
|
|
|
@@ -141,7 +198,28 @@ export function createFindMatch<TEnv = any>(
|
|
|
141
198
|
lastFindMatchResult = null;
|
|
142
199
|
return null;
|
|
143
200
|
}
|
|
144
|
-
|
|
201
|
+
try {
|
|
202
|
+
const ev = deps.evaluateLazyEntry(result.lazyEntry);
|
|
203
|
+
if (ev) await ev;
|
|
204
|
+
} catch (err) {
|
|
205
|
+
// If the trie matched this pathname, the route is REAL and this lazy
|
|
206
|
+
// entry owns (part of) it — a module-load failure is a genuine error, so
|
|
207
|
+
// propagate it as a 5xx. If the trie did NOT match, this is a
|
|
208
|
+
// genuinely-unmatched pathname (a typo, a bot probe): its 404 must not be
|
|
209
|
+
// upgraded to a 500 just because some lazy include — e.g. a failing root
|
|
210
|
+
// `include("/")` — happened to need evaluating while we probed. Isolate
|
|
211
|
+
// and fall through to the 404 (finding 4: the two match phases agree —
|
|
212
|
+
// owner failure is loud, unmatched stays 404).
|
|
213
|
+
if (trieMatched) throw err;
|
|
214
|
+
console.error(
|
|
215
|
+
`[@rangojs/router] include at "${result.lazyEntry.staticPrefix}" ` +
|
|
216
|
+
`failed to load while resolving unmatched path "${pathname}": ` +
|
|
217
|
+
`${(err as Error)?.message ?? String(err)}`,
|
|
218
|
+
);
|
|
219
|
+
lastFindMatchPathname = pathname;
|
|
220
|
+
lastFindMatchResult = null;
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
145
223
|
result = findRouteMatch(pathname, deps.routesEntries);
|
|
146
224
|
}
|
|
147
225
|
pushMetric?.("match:regex-fallback", regexStart);
|
package/src/router/instrument.ts
CHANGED
|
@@ -318,10 +318,10 @@ export function observeHandler<C, R>(
|
|
|
318
318
|
* sink is configured.
|
|
319
319
|
*
|
|
320
320
|
* This is the canonical emitter for SYNCHRONOUS facts that fire inside the
|
|
321
|
-
* request's ALS scope (
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
321
|
+
* request's ALS scope (revalidation decisions, cache-lookup decisions). A few
|
|
322
|
+
* emitters deliberately stay on the lower-level resolveSink + safeEmit because
|
|
323
|
+
* observeEvent's lazy, per-call getRouterContext() read does not fit them — keep
|
|
324
|
+
* this the complete list:
|
|
325
325
|
* - router.ts wrapLoaderPromise (loader.start/end/error) and
|
|
326
326
|
* segment-resolution/streamed-handler-telemetry.ts (streamed handler.error)
|
|
327
327
|
* capture the sink + request id EAGERLY and emit from a fire-and-forget
|
|
@@ -330,6 +330,11 @@ export function observeHandler<C, R>(
|
|
|
330
330
|
* loop (request.start/end/error, cache.decision, ...).
|
|
331
331
|
* - segment-resolution/helpers.ts emits via a caller-provided report.telemetry
|
|
332
332
|
* sink rather than the ALS router context.
|
|
333
|
+
* - rsc/handler.ts handleTimeoutResponse (request.timeout), the origin guard
|
|
334
|
+
* (request.origin-rejected), and handleStore.onError (late-handle
|
|
335
|
+
* handler.error) emit via router.telemetry directly — they run outside the
|
|
336
|
+
* RouterContext ALS (only match()/matchPartial() enter it), so a
|
|
337
|
+
* getRouterContext() read there throws and the event would vanish.
|
|
333
338
|
*/
|
|
334
339
|
export function observeEvent(event: TelemetryEvent): void {
|
|
335
340
|
// getRouterContext() either throws (real impl, outside a router context — e.g.
|
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
import type { UrlPatterns } from "../urls.js";
|
|
10
10
|
import type { AllUseItems, IncludeItem } from "../route-types.js";
|
|
11
11
|
import type { ResolvedRouteMap, RouteEntry, TrailingSlashMode } from "../types";
|
|
12
|
+
import {
|
|
13
|
+
isIncludeProvider,
|
|
14
|
+
resolveIncludeModule,
|
|
15
|
+
} from "../urls/include-provider.js";
|
|
12
16
|
|
|
13
17
|
export interface LazyEvalDeps<TEnv = any> {
|
|
14
18
|
routesEntries: RouteEntry<TEnv>[];
|
|
@@ -69,7 +73,7 @@ export function findLazyIncludes<TEnv = any>(
|
|
|
69
73
|
export function evaluateLazyEntry<TEnv = any>(
|
|
70
74
|
entry: RouteEntry<TEnv>,
|
|
71
75
|
deps: LazyEvalDeps<TEnv>,
|
|
72
|
-
): void {
|
|
76
|
+
): void | Promise<void> {
|
|
73
77
|
if (!entry.lazy || entry.lazyEvaluated || !entry.lazyPatterns) {
|
|
74
78
|
return;
|
|
75
79
|
}
|
|
@@ -87,15 +91,62 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
87
91
|
for (const [name, pattern] of Object.entries(routes)) {
|
|
88
92
|
deps.mergedRouteMap[name] = pattern;
|
|
89
93
|
}
|
|
90
|
-
|
|
94
|
+
// Register only this entry's routes (the delta): the full
|
|
95
|
+
// mergedRouteMap is seeded from the generated manifest at
|
|
96
|
+
// createRouter() time and already registered there — re-passing it
|
|
97
|
+
// made this request-path call O(total routes) (issue #666).
|
|
98
|
+
registerRouteMap(routes);
|
|
91
99
|
return;
|
|
92
100
|
}
|
|
93
101
|
}
|
|
94
102
|
}
|
|
95
103
|
|
|
96
|
-
|
|
104
|
+
// Async provider (`() => import("./routes")`): the route module is evaluated
|
|
105
|
+
// off the startup path, on the first request reaching this prefix. Concurrent
|
|
106
|
+
// first-hits share one in-flight promise so the import + expansion run exactly
|
|
107
|
+
// once. The eager path below stays fully synchronous (no Promise), so the
|
|
108
|
+
// per-entry match loop pays no microtask for normal includes.
|
|
109
|
+
const lazyPatterns = entry.lazyPatterns;
|
|
110
|
+
if (isIncludeProvider(lazyPatterns)) {
|
|
111
|
+
const inflight = (entry as { _lazyInflight?: Promise<void> })._lazyInflight;
|
|
112
|
+
if (inflight) return inflight;
|
|
113
|
+
const work = (async () => {
|
|
114
|
+
const resolved = resolveIncludeModule(
|
|
115
|
+
await lazyPatterns(),
|
|
116
|
+
entry.staticPrefix,
|
|
117
|
+
);
|
|
118
|
+
// Cache the resolved patterns: any later re-entry expands synchronously.
|
|
119
|
+
entry.lazyPatterns = resolved as unknown as UrlPatterns<TEnv>;
|
|
120
|
+
runExpansion(entry, deps, resolved as UrlPatterns<TEnv>);
|
|
121
|
+
})();
|
|
122
|
+
(entry as { _lazyInflight?: Promise<void> })._lazyInflight = work;
|
|
123
|
+
const clear = () => {
|
|
124
|
+
(entry as { _lazyInflight?: Promise<void> })._lazyInflight = undefined;
|
|
125
|
+
};
|
|
126
|
+
// On failure, clear the flag (lazyEvaluated stays false) so a later request
|
|
127
|
+
// can retry the import rather than wedging the route permanently.
|
|
128
|
+
work.then(clear, clear);
|
|
129
|
+
return work;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
runExpansion(entry, deps, lazyPatterns as UrlPatterns<TEnv>);
|
|
133
|
+
}
|
|
97
134
|
|
|
98
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Synchronously expand a lazy entry's (already-resolved) patterns into routes
|
|
137
|
+
* and splice any nested lazy includes as new entries. Runs once per entry.
|
|
138
|
+
*/
|
|
139
|
+
function runExpansion<TEnv = any>(
|
|
140
|
+
entry: RouteEntry<TEnv>,
|
|
141
|
+
deps: LazyEvalDeps<TEnv>,
|
|
142
|
+
lazyPatterns: UrlPatterns<TEnv>,
|
|
143
|
+
): void {
|
|
144
|
+
// lazyEvaluated is set at the END, only after the handler ran and the routes
|
|
145
|
+
// (and any nested includes) were spliced. Setting it up-front would mark the
|
|
146
|
+
// entry done even if the handler throws mid-expansion: the async provider
|
|
147
|
+
// path clears _lazyInflight on rejection so a later request can retry, but a
|
|
148
|
+
// premature lazyEvaluated=true would make evaluateLazyEntry short-circuit
|
|
149
|
+
// (line ~77) forever, wedging the route at 404 until the isolate restarts.
|
|
99
150
|
const lazyContext = entry.lazyContext;
|
|
100
151
|
|
|
101
152
|
const manifest = new Map<string, EntryData>();
|
|
@@ -105,12 +156,9 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
105
156
|
|
|
106
157
|
let handlerResult: AllUseItems[] = [];
|
|
107
158
|
|
|
108
|
-
const lazyCounters: Record<string, number> =
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
lazyCounters[key] = value;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
159
|
+
const lazyCounters: Record<string, number> = lazyContext?.counters
|
|
160
|
+
? { ...lazyContext.counters }
|
|
161
|
+
: {};
|
|
114
162
|
|
|
115
163
|
RangoContext.run(
|
|
116
164
|
{
|
|
@@ -170,7 +218,12 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
170
218
|
staticPrefix: extractStaticPrefix(fullPrefix),
|
|
171
219
|
routes: {} as ResolvedRouteMap<any>,
|
|
172
220
|
trailingSlash: entry.trailingSlash,
|
|
173
|
-
handler
|
|
221
|
+
// include entries don't invoke their own handler (real handlers come from
|
|
222
|
+
// the expanded routes); use the parent's placeholder. A provider has no
|
|
223
|
+
// `.handler` until resolved, so never read it here.
|
|
224
|
+
handler: isIncludeProvider(lazyInclude.patterns)
|
|
225
|
+
? entry.handler
|
|
226
|
+
: (lazyInclude.patterns as UrlPatterns<TEnv>).handler,
|
|
174
227
|
mountIndex: deps.nextMountIndex(),
|
|
175
228
|
routerId: deps.routerId,
|
|
176
229
|
lazy: true,
|
|
@@ -196,5 +249,12 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
196
249
|
deps.routesEntries.splice(insertIndex, 0, nestedEntry);
|
|
197
250
|
}
|
|
198
251
|
|
|
199
|
-
|
|
252
|
+
// Delta only — see the matching comment on the precomputed branch above and
|
|
253
|
+
// the WHY block on registerRouteMap (issue #666).
|
|
254
|
+
registerRouteMap(routesObject);
|
|
255
|
+
|
|
256
|
+
// Expansion fully succeeded (handler ran, routes + nested includes spliced) —
|
|
257
|
+
// mark done now so a mid-expansion throw above leaves lazyEvaluated=false and
|
|
258
|
+
// the entry retriable.
|
|
259
|
+
entry.lazyEvaluated = true;
|
|
200
260
|
}
|
|
@@ -420,10 +420,22 @@ function createLoaderExecutor<TEnv>(
|
|
|
420
420
|
// throw. rendered() gating uses the captured isDslLoader (above), so this
|
|
421
421
|
// does not grant rendered() to handler-invoked loaders. Uses a body-only
|
|
422
422
|
// scope, so isInsideLoaderScope() / barrier / deadlock gating is unchanged.
|
|
423
|
+
//
|
|
424
|
+
// `handlerInvoked` (!isDslLoader) rides on the scope for the CONSUMPTION-
|
|
425
|
+
// LANE RULE: a handler-consumed loader's value is a BAKED copy in every
|
|
426
|
+
// shared artifact (cache(), "use cache", the PPR shell), so its identity
|
|
427
|
+
// reads are exempt from the shell-capture guard — same allowance the
|
|
428
|
+
// cache-purity guards give it. DSL segment loaders keep their lane
|
|
429
|
+
// machinery (live = masked at capture, bake = guarded). A DSL loader's
|
|
430
|
+
// nested deps inherit isDslLoader=false only when the CHAIN started in a
|
|
431
|
+
// handler; a chain started by the segment funnel stays DSL (the loader
|
|
432
|
+
// scope ALS survives the body's awaits).
|
|
423
433
|
const promise = observePhase(PHASES.loader(loader.$$id), () =>
|
|
424
434
|
Promise.resolve(
|
|
425
|
-
runInsideLoaderBodyScope(
|
|
426
|
-
loaderFn(loaderCtx as LoaderContext<any, TEnv>),
|
|
435
|
+
runInsideLoaderBodyScope(
|
|
436
|
+
() => loaderFn(loaderCtx as LoaderContext<any, TEnv>),
|
|
437
|
+
loader.$$id,
|
|
438
|
+
!isDslLoader,
|
|
427
439
|
),
|
|
428
440
|
).finally(() => {
|
|
429
441
|
pendingLoaders.delete(loader.$$id);
|
package/src/router/manifest.ts
CHANGED
|
@@ -11,8 +11,17 @@ import MapRootLayout from "../server/root-layout";
|
|
|
11
11
|
import { joinPrefix } from "./pattern-matching.js";
|
|
12
12
|
import type { RouteEntry } from "../types";
|
|
13
13
|
import type { UrlPatterns } from "../urls";
|
|
14
|
+
import {
|
|
15
|
+
isIncludeProvider,
|
|
16
|
+
resolveIncludeModule,
|
|
17
|
+
} from "../urls/include-provider.js";
|
|
14
18
|
import { VERSION } from "@rangojs/router:version";
|
|
15
19
|
|
|
20
|
+
// Tags an error thrown while dynamically importing an async include's module
|
|
21
|
+
// (`() => import("./routes")`) so loadManifest's outer catch re-raises it as a
|
|
22
|
+
// server error rather than masking it as a RouteNotFoundError (404).
|
|
23
|
+
const MODULE_LOAD_FAILURE = Symbol.for("rango.moduleLoadFailure");
|
|
24
|
+
|
|
16
25
|
// Module-level manifest cache: avoids re-executing DSL handler on every request.
|
|
17
26
|
// Handler execution is deterministic (components, loaders, middleware are module-level
|
|
18
27
|
// stable references), so the resulting EntryData tree can be safely cached and reused
|
|
@@ -73,10 +82,14 @@ export async function loadManifest(
|
|
|
73
82
|
Store.mountIndex = mountIndex;
|
|
74
83
|
Store.isSSR = isSSR;
|
|
75
84
|
if (metricsStore) Store.metrics = metricsStore;
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
// Alias the request-scoped Store to the cached Map instead of copying it
|
|
86
|
+
// entry-by-entry. At request time this Store is throwaway (no ambient
|
|
87
|
+
// RangoContext store spans classify->match->render, so getOrCreateStore
|
|
88
|
+
// returns a fresh detached store here), so nothing mutates it in place after
|
|
89
|
+
// loadManifest returns. The fresh path below REASSIGNS Store.manifest rather
|
|
90
|
+
// than clearing it in place, so a later fresh load sharing this Store cannot
|
|
91
|
+
// poison the cached Map.
|
|
92
|
+
Store.manifest = cached;
|
|
80
93
|
pushMetric?.("manifest:cache-hit", cacheStart);
|
|
81
94
|
return cached.get(routeKey)!;
|
|
82
95
|
}
|
|
@@ -94,7 +107,11 @@ export async function loadManifest(
|
|
|
94
107
|
pushMetric?.("manifest:store-setup", storeSetupStart);
|
|
95
108
|
|
|
96
109
|
const clearStart = performance.now();
|
|
97
|
-
|
|
110
|
+
// Reassign rather than clear() in place: a prior cache-hit may have aliased
|
|
111
|
+
// Store.manifest to a shared module-cache Map (see cache-hit branch above),
|
|
112
|
+
// and clearing it in place would poison that cache. A fresh Map isolates this
|
|
113
|
+
// build; the cache still receives an independent copy at the end.
|
|
114
|
+
Store.manifest = new Map();
|
|
98
115
|
pushMetric?.("manifest:clear", clearStart);
|
|
99
116
|
|
|
100
117
|
try {
|
|
@@ -165,6 +182,29 @@ export async function loadManifest(
|
|
|
165
182
|
// not exist in the non-lazy (root handler) path and would produce
|
|
166
183
|
// mismatched shortCodes.
|
|
167
184
|
if (entry.lazy && entry.lazyPatterns) {
|
|
185
|
+
// Resolve an async include provider (`() => import("./routes")`) before
|
|
186
|
+
// running its handler. The match-time precomputed shortcut can skip
|
|
187
|
+
// evaluateLazyEntry's resolution, so render-time must resolve it here;
|
|
188
|
+
// cache the resolved patterns on the entry so later renders reuse them.
|
|
189
|
+
if (isIncludeProvider(entry.lazyPatterns)) {
|
|
190
|
+
try {
|
|
191
|
+
entry.lazyPatterns = resolveIncludeModule(
|
|
192
|
+
await entry.lazyPatterns(),
|
|
193
|
+
entry.staticPrefix,
|
|
194
|
+
) as unknown as UrlPatterns<any>;
|
|
195
|
+
} catch (err) {
|
|
196
|
+
// A failed dynamic import of a REAL, matched route's module is a
|
|
197
|
+
// server error, not a missing route. Tag it so the outer catch
|
|
198
|
+
// surfaces it as a 5xx instead of masking it as a
|
|
199
|
+
// RouteNotFoundError — which renders the 404 page (monitoring
|
|
200
|
+
// misses the failure and a CDN could cache the 404).
|
|
201
|
+
if (err && typeof err === "object") {
|
|
202
|
+
(err as Record<PropertyKey, unknown>)[MODULE_LOAD_FAILURE] =
|
|
203
|
+
true;
|
|
204
|
+
}
|
|
205
|
+
throw err;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
168
208
|
const lazyPatterns = entry.lazyPatterns as UrlPatterns<any>;
|
|
169
209
|
const includePrefix = (entry as any)._lazyPrefix || "";
|
|
170
210
|
// Slash-collapsing join so a trailing-slash parent prefix does not
|
|
@@ -192,12 +232,7 @@ export async function loadManifest(
|
|
|
192
232
|
|
|
193
233
|
if (promiseResult !== null) {
|
|
194
234
|
const load = await (promiseResult as Promise<any>);
|
|
195
|
-
if (
|
|
196
|
-
load &&
|
|
197
|
-
load !== null &&
|
|
198
|
-
typeof load === "object" &&
|
|
199
|
-
"default" in load
|
|
200
|
-
) {
|
|
235
|
+
if (load && typeof load === "object" && "default" in load) {
|
|
201
236
|
// Promise<{ default: () => Array }> - e.g., dynamic import
|
|
202
237
|
if (typeof load.default !== "function") {
|
|
203
238
|
throw new Error(
|
|
@@ -255,6 +290,16 @@ export async function loadManifest(
|
|
|
255
290
|
|
|
256
291
|
return Store.manifest.get(routeKey)!;
|
|
257
292
|
} catch (e) {
|
|
293
|
+
// A tagged async-include module-load failure is a server error for a REAL
|
|
294
|
+
// matched route — re-raise it unchanged (becomes a 5xx) instead of masking
|
|
295
|
+
// it as a RouteNotFoundError (which renders/caches a 404).
|
|
296
|
+
if (
|
|
297
|
+
e &&
|
|
298
|
+
typeof e === "object" &&
|
|
299
|
+
(e as Record<PropertyKey, unknown>)[MODULE_LOAD_FAILURE]
|
|
300
|
+
) {
|
|
301
|
+
throw e;
|
|
302
|
+
}
|
|
258
303
|
throw new RouteNotFoundError(
|
|
259
304
|
`Failed to load route handlers for ${path}: ${(e as Error).message}`,
|
|
260
305
|
{
|