@rangojs/router 0.0.0-experimental.115 → 0.0.0-experimental.116
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/vite/index.js +148 -97
- package/package.json +17 -18
- package/skills/api-client/SKILL.md +211 -0
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/rango/SKILL.md +1 -0
- package/skills/response-routes/SKILL.md +61 -43
- package/skills/typesafety/SKILL.md +3 -3
- package/src/__augment-tests__/augmented.check.ts +2 -3
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +28 -1
- package/src/build/index.ts +8 -1
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +43 -0
- package/src/client.tsx +4 -23
- package/src/errors.ts +0 -3
- package/src/href-client.ts +7 -8
- package/src/index.rsc.ts +1 -2
- package/src/index.ts +1 -2
- package/src/router/find-match.ts +54 -6
- package/src/router/lazy-includes.ts +33 -14
- package/src/router/manifest.ts +19 -6
- package/src/router/pattern-matching.ts +15 -2
- package/src/router/router-interfaces.ts +11 -0
- package/src/router/trie-matching.ts +22 -3
- package/src/router.ts +21 -7
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +16 -13
- package/src/urls/index.ts +1 -2
- package/src/urls/type-extraction.ts +33 -24
- package/src/vite/discovery/discover-routers.ts +46 -29
- package/src/vite/discovery/state.ts +7 -0
- package/src/vite/plugins/client-ref-hashing.ts +12 -1
- package/src/vite/rango.ts +32 -4
- package/src/vite/utils/client-chunks.ts +41 -7
- package/src/vite/utils/manifest-utils.ts +8 -75
- package/src/vite/utils/shared-utils.ts +58 -0
package/src/href-client.ts
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
|
|
17
17
|
import type { GetRegisteredRoutes } from "./types.js";
|
|
18
18
|
import type { JsonSerialize } from "./serialize.js";
|
|
19
|
-
import type { ResponseEnvelope } from "./urls.js";
|
|
20
19
|
|
|
21
20
|
/**
|
|
22
21
|
* Parse constraint values into a union type for paths
|
|
@@ -163,16 +162,16 @@ type ResponsePayloadForKey<
|
|
|
163
162
|
}[keyof TRoutes];
|
|
164
163
|
|
|
165
164
|
/**
|
|
166
|
-
* Public response type for a route, keyed by pattern or concrete path.
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* `
|
|
165
|
+
* Public response type for a route, keyed by pattern or concrete path. JSON
|
|
166
|
+
* response routes send the handler's return value verbatim (bare), so the
|
|
167
|
+
* payload is wrapped only in `JsonSerialize` to describe the JSON **wire** value
|
|
168
|
+
* a consumer receives from `fetch().then(r => r.json())` — e.g. a handler
|
|
169
|
+
* returning `{ createdAt: Date }` resolves here to `{ createdAt: string }`.
|
|
171
170
|
*/
|
|
172
171
|
export type PathResponse<
|
|
173
172
|
TPath extends string,
|
|
174
173
|
TRoutes = GetRegisteredRoutes,
|
|
175
|
-
> =
|
|
174
|
+
> = JsonSerialize<ResponsePayloadFor<TPath, TRoutes>>;
|
|
176
175
|
|
|
177
176
|
/**
|
|
178
177
|
* Strip trailing slash from a path (e.g., "/blog/" -> "/blog" | "/blog/")
|
|
@@ -256,7 +255,7 @@ declare global {
|
|
|
256
255
|
*
|
|
257
256
|
* The payload is the JSON **wire** shape (via `Rango.JsonSerialize`), not the
|
|
258
257
|
* handler's raw return — a handler returning `{ createdAt: Date }` resolves
|
|
259
|
-
* here to `
|
|
258
|
+
* here to `{ createdAt: string }` (bare, no envelope), matching what
|
|
260
259
|
* `fetch().then(r => r.json())` actually yields.
|
|
261
260
|
*
|
|
262
261
|
* Only resolves once `Rango.RegisteredRoutes` carries response metadata (the
|
package/src/index.rsc.ts
CHANGED
package/src/index.ts
CHANGED
package/src/router/find-match.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { tryTrieMatch } from "./trie-matching.js";
|
|
2
|
-
import {
|
|
2
|
+
import { getRouterTrie } from "../route-map-builder.js";
|
|
3
3
|
import {
|
|
4
4
|
findMatch as findRouteMatch,
|
|
5
5
|
isLazyEvaluationNeeded,
|
|
@@ -8,6 +8,19 @@ import {
|
|
|
8
8
|
import type { MetricsStore } from "../server/context";
|
|
9
9
|
import type { RouteEntry } from "../types";
|
|
10
10
|
|
|
11
|
+
// Return a shallow copy with an independent `params` object. The single-entry
|
|
12
|
+
// cache below is module-lifetime and keyed only on pathname, so the same result
|
|
13
|
+
// object is handed to every same-pathname request in the isolate. ctx.params
|
|
14
|
+
// aliases this `params` (see request-context), so without an own copy a handler
|
|
15
|
+
// that mutates ctx.params would corrupt the cached entry for later requests.
|
|
16
|
+
// `entry` and the flags are intentionally shared by reference: they are
|
|
17
|
+
// read-only, and entry identity is compared in match-api (prevMatch.entry).
|
|
18
|
+
function cloneMatchResult<TEnv>(
|
|
19
|
+
r: RouteMatchResult<TEnv> | null,
|
|
20
|
+
): RouteMatchResult<TEnv> | null {
|
|
21
|
+
return r ? { ...r, params: { ...r.params } } : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
export interface FindMatchDeps<TEnv = any> {
|
|
12
25
|
routesEntries: RouteEntry<TEnv>[];
|
|
13
26
|
evaluateLazyEntry: (entry: RouteEntry<TEnv>) => void;
|
|
@@ -35,9 +48,10 @@ export function createFindMatch<TEnv = any>(
|
|
|
35
48
|
pathname: string,
|
|
36
49
|
ms?: MetricsStore,
|
|
37
50
|
): RouteMatchResult<TEnv> | null {
|
|
38
|
-
// Return cached result if same pathname (avoids double-match per request)
|
|
51
|
+
// Return cached result if same pathname (avoids double-match per request).
|
|
52
|
+
// Clone so a caller mutating ctx.params cannot corrupt the shared cache.
|
|
39
53
|
if (lastFindMatchPathname === pathname) {
|
|
40
|
-
return lastFindMatchResult;
|
|
54
|
+
return cloneMatchResult(lastFindMatchResult);
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
// Helper to push sub-metrics
|
|
@@ -56,12 +70,19 @@ export function createFindMatch<TEnv = any>(
|
|
|
56
70
|
// routers and must not be used — in multi-router setups (host routing)
|
|
57
71
|
// overlapping paths like "/" would match the wrong app's route.
|
|
58
72
|
const routeTrie = getRouterTrie(deps.routerId);
|
|
73
|
+
// Whether the trie produced a match for this pathname (independent of
|
|
74
|
+
// whether the owning RouteEntry was resolvable yet). Used to suppress the
|
|
75
|
+
// R3 dev warning below: if the trie DID match but we fell through to the
|
|
76
|
+
// regex fallback only because a lazy entry was not spliced in yet, that is
|
|
77
|
+
// not a trie gap.
|
|
78
|
+
let trieMatched = false;
|
|
59
79
|
if (routeTrie) {
|
|
60
80
|
const trieStart = performance.now();
|
|
61
81
|
const trieResult = tryTrieMatch(routeTrie, pathname);
|
|
62
82
|
pushMetric?.("match:trie", trieStart);
|
|
63
83
|
|
|
64
84
|
if (trieResult) {
|
|
85
|
+
trieMatched = true;
|
|
65
86
|
// Find the RouteEntry that contains this route.
|
|
66
87
|
// Multiple entries can share the same staticPrefix (e.g., several
|
|
67
88
|
// include("/", patterns) calls all produce staticPrefix=""). Evaluate
|
|
@@ -114,7 +135,6 @@ export function createFindMatch<TEnv = any>(
|
|
|
114
135
|
params: trieResult.params,
|
|
115
136
|
optionalParams: new Set(trieResult.optionalParams || []),
|
|
116
137
|
redirectTo: trieResult.redirectTo,
|
|
117
|
-
ancestry: trieResult.ancestry,
|
|
118
138
|
...(trieResult.pr ? { pr: true } : {}),
|
|
119
139
|
...(trieResult.pt ? { pt: true } : {}),
|
|
120
140
|
...(trieResult.responseType
|
|
@@ -125,7 +145,7 @@ export function createFindMatch<TEnv = any>(
|
|
|
125
145
|
: {}),
|
|
126
146
|
...(trieResult.rscFirst ? { rscFirst: true } : {}),
|
|
127
147
|
};
|
|
128
|
-
return lastFindMatchResult;
|
|
148
|
+
return cloneMatchResult(lastFindMatchResult);
|
|
129
149
|
}
|
|
130
150
|
}
|
|
131
151
|
}
|
|
@@ -153,8 +173,36 @@ export function createFindMatch<TEnv = any>(
|
|
|
153
173
|
}
|
|
154
174
|
pushMetric?.("match:regex-fallback", regexStart);
|
|
155
175
|
|
|
176
|
+
// The trie is the single source of truth and is built before findMatch in
|
|
177
|
+
// both dev (handler rebuild) and production (ensureRouterManifest). If the
|
|
178
|
+
// trie was present yet the regex fallback resolved a real match, the trie
|
|
179
|
+
// has a gap (e.g. a route shape it cannot represent) and dev/prod could
|
|
180
|
+
// diverge if the trie were ever absent. Surface it in dev; folded out in
|
|
181
|
+
// production builds.
|
|
182
|
+
//
|
|
183
|
+
// Suppress when the trie DID match (`trieMatched`): that path falls through
|
|
184
|
+
// to the regex fallback only on the first request to a not-yet-spliced lazy
|
|
185
|
+
// entry (e.g. a 2+-level nested include whose deeper parent has not been
|
|
186
|
+
// evaluated). The trie knew the route; runtime lazy discovery simply lagged.
|
|
187
|
+
// That is the supported lazy-include flow, not a trie gap, so warning on it
|
|
188
|
+
// is a false positive (it manufactures bug reports and erodes the signal).
|
|
189
|
+
if (
|
|
190
|
+
process.env.NODE_ENV !== "production" &&
|
|
191
|
+
routeTrie &&
|
|
192
|
+
!trieMatched &&
|
|
193
|
+
result &&
|
|
194
|
+
!isLazyEvaluationNeeded(result)
|
|
195
|
+
) {
|
|
196
|
+
console.warn(
|
|
197
|
+
`[@rangojs/router] Route "${pathname}" resolved via the regex fallback ` +
|
|
198
|
+
`even though the route trie was present. The trie should be the single ` +
|
|
199
|
+
`matching source of truth; this indicates a trie gap. Please report this ` +
|
|
200
|
+
`with your route configuration.`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
156
204
|
lastFindMatchPathname = pathname;
|
|
157
205
|
lastFindMatchResult = result;
|
|
158
|
-
return result;
|
|
206
|
+
return cloneMatchResult(result);
|
|
159
207
|
};
|
|
160
208
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { registerRouteMap } from "../route-map-builder.js";
|
|
2
|
-
import { extractStaticPrefix } from "./pattern-matching.js";
|
|
2
|
+
import { extractStaticPrefix, joinPrefix } from "./pattern-matching.js";
|
|
3
3
|
import {
|
|
4
4
|
type EntryData,
|
|
5
5
|
RangoContext,
|
|
@@ -81,11 +81,16 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
81
81
|
// Check for pre-computed routes from build-time data.
|
|
82
82
|
// Only leaf nodes (no nested includes) are precomputed, so entries with
|
|
83
83
|
// nested lazy includes fall through to the handler below.
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
84
|
+
//
|
|
85
|
+
// The load-bearing protection against two includes sharing a staticPrefix
|
|
86
|
+
// lives UPSTREAM in buildPrecomputedByPrefix (build/prefix-tree-utils): a
|
|
87
|
+
// shared staticPrefix is omitted from the map entirely, so currentPrecomputed
|
|
88
|
+
// never returns routes for it and the shortcut is skipped. The live-count
|
|
89
|
+
// check below is a secondary guard only — it is TIMING-BLIND (it counts
|
|
90
|
+
// routesEntries, which cannot see a nested sibling that has not been spliced
|
|
91
|
+
// in yet), so it must NOT be relied on alone. Kept as defense-in-depth for the
|
|
92
|
+
// all-siblings-live case (e.g. several include("/", ...) placeholders created
|
|
93
|
+
// up front).
|
|
89
94
|
const currentPrecomputed = deps.getPrecomputedByPrefix();
|
|
90
95
|
if (currentPrecomputed) {
|
|
91
96
|
const routes = currentPrecomputed.get(entry.staticPrefix);
|
|
@@ -113,7 +118,15 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
113
118
|
const lazyPatterns = entry.lazyPatterns as UrlPatterns<TEnv>;
|
|
114
119
|
const lazyContext = entry.lazyContext;
|
|
115
120
|
|
|
116
|
-
// Create a new context for evaluating the lazy patterns
|
|
121
|
+
// Create a new context for evaluating the lazy patterns.
|
|
122
|
+
// KNOWN REDUNDANCY (LP3, docs/internal/matching-stability-review.md): this
|
|
123
|
+
// runs lazyPatterns.handler() purely to extract `patterns` (route name ->
|
|
124
|
+
// pattern) for matching, and DISCARDS the EntryData `manifest` it builds.
|
|
125
|
+
// loadManifest() then runs the SAME handler again on the first request to
|
|
126
|
+
// build the EntryData tree for rendering. Unifying the two runs is deferred
|
|
127
|
+
// (the two run in different contexts — see the LP3 todo in
|
|
128
|
+
// lazy-include-perf.test.ts). The precomputed-entries shortcut above avoids
|
|
129
|
+
// THIS run entirely for leaf includes.
|
|
117
130
|
const manifest = new Map<string, EntryData>();
|
|
118
131
|
const patterns = new Map<string, string>();
|
|
119
132
|
const patternsByPrefix = new Map<string, Map<string, string>>();
|
|
@@ -145,10 +158,13 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
145
158
|
includeScope: lazyContext?.includeScope,
|
|
146
159
|
},
|
|
147
160
|
() => {
|
|
148
|
-
// Run the lazy patterns handler with the original context prefixes
|
|
149
|
-
// The prefix comes from the IncludeItem stored in lazyPatterns
|
|
161
|
+
// Run the lazy patterns handler with the original context prefixes.
|
|
162
|
+
// The prefix comes from the IncludeItem stored in lazyPatterns. Use the
|
|
163
|
+
// slash-collapsing join so a trailing-slash parent prefix does not bake a
|
|
164
|
+
// double slash into the registered route patterns (entry.routes,
|
|
165
|
+
// reverse(), EntryData.pattern, mountPath) when the handler runs.
|
|
150
166
|
const includePrefix = (entry as any)._lazyPrefix || "";
|
|
151
|
-
const fullPrefix = (lazyContext?.urlPrefix
|
|
167
|
+
const fullPrefix = joinPrefix(lazyContext?.urlPrefix, includePrefix);
|
|
152
168
|
|
|
153
169
|
if (fullPrefix || lazyContext?.namePrefix) {
|
|
154
170
|
runWithPrefixes(fullPrefix, lazyContext?.namePrefix, () => {
|
|
@@ -190,10 +206,13 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
190
206
|
// Detect nested lazy includes and register them as new entries
|
|
191
207
|
const nestedLazyIncludes = findLazyIncludes(handlerResult);
|
|
192
208
|
for (const lazyInclude of nestedLazyIncludes) {
|
|
193
|
-
// Compute the full URL prefix (combining parent prefix if any)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
209
|
+
// Compute the full URL prefix (combining parent prefix if any). Use the
|
|
210
|
+
// slash-collapsing join so a trailing-slash parent prefix does not produce
|
|
211
|
+
// a double-slash staticPrefix the trie's sp can never match.
|
|
212
|
+
const fullPrefix = joinPrefix(
|
|
213
|
+
lazyInclude.context.urlPrefix,
|
|
214
|
+
lazyInclude.prefix,
|
|
215
|
+
);
|
|
197
216
|
|
|
198
217
|
const nestedEntry: RouteEntry<TEnv> & { _lazyPrefix?: string } = {
|
|
199
218
|
prefix: "",
|
package/src/router/manifest.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type MetricsStore,
|
|
15
15
|
} from "../server/context";
|
|
16
16
|
import MapRootLayout from "../server/root-layout";
|
|
17
|
+
import { joinPrefix } from "./pattern-matching.js";
|
|
17
18
|
import type { RouteEntry } from "../types";
|
|
18
19
|
import type { UrlPatterns } from "../urls";
|
|
19
20
|
import { VERSION } from "@rangojs/router:version";
|
|
@@ -23,10 +24,17 @@ import { VERSION } from "@rangojs/router:version";
|
|
|
23
24
|
// stable references), so the resulting EntryData tree can be safely cached and reused
|
|
24
25
|
// across requests within the same isolate.
|
|
25
26
|
//
|
|
26
|
-
// Cache is keyed by (VERSION, mountIndex, routeKey, isSSR).
|
|
27
|
+
// Cache is keyed by (VERSION, routerId, mountIndex, routeKey, isSSR). routeKey is
|
|
28
|
+
// REQUIRED in the key: loadManifest() runs the handler with forRoute=routeKey, and
|
|
29
|
+
// path-helper.ts prunes (skips registering) every route except forRoute, so the
|
|
30
|
+
// resulting Store.manifest is pruned to the requested route — NOT the full include.
|
|
31
|
+
// Dropping routeKey would make a sibling route miss and overwrite this entry with its
|
|
32
|
+
// own pruned manifest, so alternating sibling requests would thrash (re-run the
|
|
33
|
+
// handler every time). Running the include handler once per isolate instead of once
|
|
34
|
+
// per route is possible but needs an unpruned manifest cache with prune-on-read — see
|
|
35
|
+
// LP1 in docs/internal/matching-stability-review.md. VERSION comes from the
|
|
27
36
|
// @rangojs/router:version virtual module which Vite invalidates on RSC module HMR.
|
|
28
37
|
// When VERSION changes, this module re-evaluates and the cache is recreated empty.
|
|
29
|
-
// Including VERSION in the key is additional defense against stale entries.
|
|
30
38
|
const manifestModuleCache = new Map<string, Map<string, EntryData>>();
|
|
31
39
|
|
|
32
40
|
/**
|
|
@@ -34,8 +42,8 @@ const manifestModuleCache = new Map<string, Map<string, EntryData>>();
|
|
|
34
42
|
* Handles lazy imports, unwrapping, and validation
|
|
35
43
|
*
|
|
36
44
|
* Results are cached at module level after first execution. Subsequent calls
|
|
37
|
-
* for the same (routeKey, isSSR) within the same isolate
|
|
38
|
-
* without re-executing the DSL handler.
|
|
45
|
+
* for the same (routerId, mountIndex, routeKey, isSSR) within the same isolate
|
|
46
|
+
* return cached data without re-executing the DSL handler.
|
|
39
47
|
*/
|
|
40
48
|
/**
|
|
41
49
|
* Clear the module-level manifest cache.
|
|
@@ -65,9 +73,11 @@ export async function loadManifest(
|
|
|
65
73
|
|
|
66
74
|
const mountIndex = entry.mountIndex;
|
|
67
75
|
|
|
68
|
-
// Check module-level cache (persists across requests within same isolate)
|
|
76
|
+
// Check module-level cache (persists across requests within same isolate).
|
|
69
77
|
// Include routerId so multi-router setups (host routing) don't share cached
|
|
70
78
|
// EntryData across routers with overlapping mountIndex + routeKey combinations.
|
|
79
|
+
// routeKey is in the key because loadManifest() builds a manifest pruned to
|
|
80
|
+
// forRoute=routeKey (see path-helper.ts) — see the cache comment above.
|
|
71
81
|
const cacheKey = `${VERSION}:${entry.routerId ?? ""}:${mountIndex ?? ""}:${routeKey}:${isSSR ? 1 : 0}`;
|
|
72
82
|
const cached = manifestModuleCache.get(cacheKey);
|
|
73
83
|
if (cached) {
|
|
@@ -176,7 +186,10 @@ export async function loadManifest(
|
|
|
176
186
|
if (entry.lazy && entry.lazyPatterns) {
|
|
177
187
|
const lazyPatterns = entry.lazyPatterns as UrlPatterns<any>;
|
|
178
188
|
const includePrefix = (entry as any)._lazyPrefix || "";
|
|
179
|
-
|
|
189
|
+
// Slash-collapsing join so a trailing-slash parent prefix does not
|
|
190
|
+
// bake a double slash into the registered route patterns (must match
|
|
191
|
+
// the same join in evaluateLazyEntry / the build-time runWithPrefixes).
|
|
192
|
+
const fullPrefix = joinPrefix(lazyContext?.urlPrefix, includePrefix);
|
|
180
193
|
|
|
181
194
|
if (fullPrefix || lazyContext?.namePrefix) {
|
|
182
195
|
return runWithPrefixes(fullPrefix, lazyContext?.namePrefix, () =>
|
|
@@ -317,6 +317,21 @@ export function extractStaticPrefix(pattern: string): string {
|
|
|
317
317
|
return pattern.slice(0, lastSlash);
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Join a URL prefix to a sub-prefix, collapsing the duplicate slash when the
|
|
322
|
+
* base ends with "/" and the sub-prefix starts with "/". This mirrors the
|
|
323
|
+
* canonical join in `include()` (urls/include-helper.ts) and `runWithPrefixes`
|
|
324
|
+
* (server/context.ts) so a nested lazy include's runtime staticPrefix matches
|
|
325
|
+
* the build-time trie's `sp` (e.g. `include("/parent/", …)` containing
|
|
326
|
+
* `include("/child", …)` resolves to `/parent/child`, not `/parent//child`).
|
|
327
|
+
*/
|
|
328
|
+
export function joinPrefix(base: string | undefined, prefix: string): string {
|
|
329
|
+
if (!base) return prefix;
|
|
330
|
+
return base.endsWith("/") && prefix.startsWith("/")
|
|
331
|
+
? base + prefix.slice(1)
|
|
332
|
+
: base + prefix;
|
|
333
|
+
}
|
|
334
|
+
|
|
320
335
|
/**
|
|
321
336
|
* Match a pathname against registered routes
|
|
322
337
|
*
|
|
@@ -343,8 +358,6 @@ export interface RouteMatchResult<TEnv = any> {
|
|
|
343
358
|
params: Record<string, string>;
|
|
344
359
|
optionalParams: Set<string>;
|
|
345
360
|
redirectTo?: string;
|
|
346
|
-
/** Ancestry shortCodes for layout pruning (from trie match) */
|
|
347
|
-
ancestry?: string[];
|
|
348
361
|
/** Route has pre-rendered data available (from trie) */
|
|
349
362
|
pr?: true;
|
|
350
363
|
/** Passthrough: handler kept for live fallback on unknown params (from trie) */
|
|
@@ -365,6 +365,17 @@ export interface RangoInternal<
|
|
|
365
365
|
/** @internal basename for runtime manifest generation */
|
|
366
366
|
readonly __basename?: string;
|
|
367
367
|
|
|
368
|
+
/**
|
|
369
|
+
* @internal Router-level error/notFound fallbacks (`createRouter` options),
|
|
370
|
+
* exposed for the build-time clientChunks discovery so a `"use client"`
|
|
371
|
+
* default boundary is routed into the dedicated `app-fallback` chunk. Unlike
|
|
372
|
+
* the route-tree `errorBoundary()`/`notFoundBoundary()` helpers these never
|
|
373
|
+
* land in `EntryData`, so they are read directly off the router instance.
|
|
374
|
+
*/
|
|
375
|
+
readonly __defaultErrorBoundary?: RangoOptions<TEnv>["defaultErrorBoundary"];
|
|
376
|
+
readonly __defaultNotFoundBoundary?: RangoOptions<TEnv>["defaultNotFoundBoundary"];
|
|
377
|
+
readonly __notFound?: RangoOptions<TEnv>["notFound"];
|
|
378
|
+
|
|
368
379
|
match(
|
|
369
380
|
request: Request,
|
|
370
381
|
input?: RouterRequestInput<TEnv>,
|
|
@@ -19,8 +19,6 @@ export interface TrieMatchResult {
|
|
|
19
19
|
* from `params` (read as `undefined`), matching the
|
|
20
20
|
* `ExtractParams<"/:locale?/...">` type. */
|
|
21
21
|
optionalParams?: string[];
|
|
22
|
-
/** Ancestry shortCodes for layout pruning */
|
|
23
|
-
ancestry: string[];
|
|
24
22
|
/** Redirect target if trailing slash requires it */
|
|
25
23
|
redirectTo?: string;
|
|
26
24
|
/** Route has pre-rendered data available */
|
|
@@ -63,6 +61,19 @@ export function tryTrieMatch(
|
|
|
63
61
|
pathnameHasTrailingSlash,
|
|
64
62
|
);
|
|
65
63
|
}
|
|
64
|
+
// A root-level wildcard ("/*") matches "/" with an empty remainder, the
|
|
65
|
+
// same value the regex matcher produces for the bare prefix. Without this
|
|
66
|
+
// the trie misses, the regex fallback runs, and its no-config branch emits
|
|
67
|
+
// a corrupt slice-off redirect. The static terminal still wins above.
|
|
68
|
+
if (trie.w) {
|
|
69
|
+
return validateAndBuild(
|
|
70
|
+
trie.w,
|
|
71
|
+
[],
|
|
72
|
+
"",
|
|
73
|
+
pathname,
|
|
74
|
+
pathnameHasTrailingSlash,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
66
77
|
return null;
|
|
67
78
|
}
|
|
68
79
|
|
|
@@ -105,6 +116,15 @@ function walkTrie(
|
|
|
105
116
|
if (node.r) {
|
|
106
117
|
return { leaf: node.r, paramValues: [...paramValues] };
|
|
107
118
|
}
|
|
119
|
+
// A wildcard at this node matches the bare prefix with an empty remainder
|
|
120
|
+
// (e.g. "/files" against "/files/*"), mirroring the regex matcher's `*=""`.
|
|
121
|
+
// walkTrie otherwise only reaches node.w in the index<length branch below,
|
|
122
|
+
// so without this a request to the wildcard's own prefix misses the trie
|
|
123
|
+
// and the regex fallback emits a corrupt redirect. A static terminal
|
|
124
|
+
// (node.r) still wins.
|
|
125
|
+
if (node.w) {
|
|
126
|
+
return { leaf: node.w, paramValues: [...paramValues], wildcardValue: "" };
|
|
127
|
+
}
|
|
108
128
|
return null;
|
|
109
129
|
}
|
|
110
130
|
|
|
@@ -229,7 +249,6 @@ function validateAndBuild(
|
|
|
229
249
|
routeKey: leaf.n,
|
|
230
250
|
sp: leaf.sp,
|
|
231
251
|
params,
|
|
232
|
-
ancestry: leaf.a,
|
|
233
252
|
};
|
|
234
253
|
|
|
235
254
|
if (leaf.op) result.optionalParams = leaf.op;
|
package/src/router.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type { AllUseItems } from "./route-types.js";
|
|
|
21
21
|
import type { UrlPatterns } from "./urls.js";
|
|
22
22
|
import type { UrlBuilder } from "./urls/pattern-types.js";
|
|
23
23
|
import { urls } from "./urls.js";
|
|
24
|
+
import { buildPrecomputedByPrefix } from "./build/prefix-tree-utils.js";
|
|
24
25
|
import {
|
|
25
26
|
type EntryData,
|
|
26
27
|
getContext,
|
|
@@ -70,6 +71,7 @@ import {
|
|
|
70
71
|
} from "./router/middleware.js";
|
|
71
72
|
import {
|
|
72
73
|
extractStaticPrefix,
|
|
74
|
+
joinPrefix,
|
|
73
75
|
traverseBack,
|
|
74
76
|
} from "./router/pattern-matching.js";
|
|
75
77
|
import { resolveSink, safeEmit, getRequestId } from "./router/telemetry.js";
|
|
@@ -363,9 +365,11 @@ export function createRouter<TEnv = any>(
|
|
|
363
365
|
getRouterPrecomputedEntries(routerId) ?? getPrecomputedEntries();
|
|
364
366
|
if (current !== precomputedSource) {
|
|
365
367
|
precomputedSource = current;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
368
|
+
// buildPrecomputedByPrefix drops any staticPrefix owned by more than one
|
|
369
|
+
// leaf include instead of collapsing it last-wins (which would mis-assign
|
|
370
|
+
// one include's routes to another's entry and 500 a valid sibling route).
|
|
371
|
+
// Such shared-prefix includes resolve via the handler path instead.
|
|
372
|
+
precomputedByPrefix = current ? buildPrecomputedByPrefix(current) : null;
|
|
369
373
|
}
|
|
370
374
|
return precomputedByPrefix;
|
|
371
375
|
}
|
|
@@ -832,10 +836,13 @@ export function createRouter<TEnv = any>(
|
|
|
832
836
|
|
|
833
837
|
// Create placeholder RouteEntry for each lazy include
|
|
834
838
|
for (const lazyInclude of lazyIncludes) {
|
|
835
|
-
// Compute the full URL prefix (combining parent prefix if any)
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
+
// Compute the full URL prefix (combining parent prefix if any). Use the
|
|
840
|
+
// slash-collapsing join so a trailing-slash parent prefix does not
|
|
841
|
+
// produce a double-slash staticPrefix the trie's sp can never match.
|
|
842
|
+
const fullPrefix = joinPrefix(
|
|
843
|
+
lazyInclude.context.urlPrefix,
|
|
844
|
+
lazyInclude.prefix,
|
|
845
|
+
);
|
|
839
846
|
|
|
840
847
|
const lazyEntry: RouteEntry<TEnv> & { _lazyPrefix?: string } = {
|
|
841
848
|
prefix: "",
|
|
@@ -998,6 +1005,13 @@ export function createRouter<TEnv = any>(
|
|
|
998
1005
|
// Expose basename for runtime manifest generation
|
|
999
1006
|
__basename: basename,
|
|
1000
1007
|
|
|
1008
|
+
// Expose router-level boundary defaults for build-time clientChunks
|
|
1009
|
+
// discovery (so a "use client" default boundary lands in app-fallback).
|
|
1010
|
+
// These are createRouter options, never pushed onto EntryData.
|
|
1011
|
+
__defaultErrorBoundary: defaultErrorBoundary,
|
|
1012
|
+
__defaultNotFoundBoundary: defaultNotFoundBoundary,
|
|
1013
|
+
__notFound: notFound,
|
|
1014
|
+
|
|
1001
1015
|
// RSC request handler (lazily created on first call)
|
|
1002
1016
|
fetch: (() => {
|
|
1003
1017
|
// Handler is created on first call and reused
|
package/src/rsc/manifest-init.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
setRouteTrie,
|
|
14
14
|
setRouterManifest,
|
|
15
15
|
setRouterTrie,
|
|
16
|
+
setRouterPrecomputedEntries,
|
|
16
17
|
} from "../route-map-builder.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -36,47 +37,13 @@ export async function buildRouterTrieFromUrlpatterns(
|
|
|
36
37
|
undefined,
|
|
37
38
|
router.basename ? { urlPrefix: router.basename } : undefined,
|
|
38
39
|
);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const routeToStaticPrefix: Record<string, string> = {};
|
|
47
|
-
for (const name of Object.keys(generated.routeManifest)) {
|
|
48
|
-
routeToStaticPrefix[name] = "";
|
|
49
|
-
}
|
|
50
|
-
// Override with prefix from include() entries so the trie
|
|
51
|
-
// returns the correct sp for lazy entry lookup in findMatch.
|
|
52
|
-
// Walk recursively to include routes in nested includes.
|
|
53
|
-
if (generated.prefixTree) {
|
|
54
|
-
const visitPrefixNode = (node: any): void => {
|
|
55
|
-
const sp = node.staticPrefix || "";
|
|
56
|
-
for (const route of node.routes || []) {
|
|
57
|
-
routeToStaticPrefix[route] = sp;
|
|
58
|
-
}
|
|
59
|
-
for (const child of Object.values(node.children || {})) {
|
|
60
|
-
visitPrefixNode(child);
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
for (const node of Object.values(generated.prefixTree)) {
|
|
64
|
-
visitPrefixNode(node);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
const trie = buildRouteTrie(
|
|
68
|
-
generated.routeManifest,
|
|
69
|
-
generated._routeAncestry,
|
|
70
|
-
routeToStaticPrefix,
|
|
71
|
-
generated.routeTrailingSlash,
|
|
72
|
-
generated.prerenderRoutes
|
|
73
|
-
? new Set(generated.prerenderRoutes)
|
|
74
|
-
: undefined,
|
|
75
|
-
generated.passthroughRoutes
|
|
76
|
-
? new Set(generated.passthroughRoutes)
|
|
77
|
-
: undefined,
|
|
78
|
-
generated.responseTypeRoutes,
|
|
79
|
-
);
|
|
40
|
+
// Build the trie through the SAME shared helper the production discovery uses
|
|
41
|
+
// (discover-routers.ts), so the dev runtime-rebuilt trie and the prod
|
|
42
|
+
// serialized trie cannot drift. buildPerRouterTrie returns null when there
|
|
43
|
+
// are no routes.
|
|
44
|
+
const { buildPerRouterTrie } = await import("../build/route-trie.js");
|
|
45
|
+
const trie = buildPerRouterTrie(generated);
|
|
46
|
+
if (trie) {
|
|
80
47
|
setRouterTrie(router.id, trie);
|
|
81
48
|
// Set global trie only if not already set by another router
|
|
82
49
|
if (!getRouteTrie()) {
|
|
@@ -84,6 +51,26 @@ export async function buildRouterTrieFromUrlpatterns(
|
|
|
84
51
|
}
|
|
85
52
|
}
|
|
86
53
|
setRouterManifest(router.id, generated.routeManifest);
|
|
54
|
+
|
|
55
|
+
// Match the production discovery path: precompute leaf-include entries so the
|
|
56
|
+
// match-time shortcut in evaluateLazyEntry applies in dev/Cloudflare too.
|
|
57
|
+
// Without this, dev re-runs each matched leaf include's handler at match time
|
|
58
|
+
// (evaluateLazyEntry) AND again at render time (loadManifest); with it, the
|
|
59
|
+
// match-time run is skipped and the handler runs once per first request.
|
|
60
|
+
// Identical route ownership to the handler path (the shortcut is guarded by
|
|
61
|
+
// the same prefixIsShared and #506 checks production uses).
|
|
62
|
+
const { flattenLeafEntries } = await import("../build/prefix-tree-utils.js");
|
|
63
|
+
const precomputed: Array<{
|
|
64
|
+
staticPrefix: string;
|
|
65
|
+
routes: Record<string, string>;
|
|
66
|
+
}> = [];
|
|
67
|
+
flattenLeafEntries(
|
|
68
|
+
generated.prefixTree,
|
|
69
|
+
generated.routeManifest,
|
|
70
|
+
precomputed,
|
|
71
|
+
);
|
|
72
|
+
setRouterPrecomputedEntries(router.id, precomputed);
|
|
73
|
+
|
|
87
74
|
// Merge into global manifest (needed for reverse/href across routers)
|
|
88
75
|
const existing = hasCachedManifest() ? getGlobalRouteMap() : {};
|
|
89
76
|
setCachedManifest({ ...existing, ...generated.routeManifest });
|