@pyreon/router 0.2.1 → 0.3.1
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/lib/analysis/index.js.html +1 -1
- package/lib/index.js +235 -93
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +235 -91
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index2.d.ts +1 -1
- package/lib/types/index2.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/match.ts +408 -71
package/src/match.ts
CHANGED
|
@@ -65,7 +65,303 @@ export function stringifyQuery(query: Record<string, string>): string {
|
|
|
65
65
|
return parts.length ? `?${parts.join("&")}` : ""
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
// ───
|
|
68
|
+
// ─── Compiled route structures ───────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Pre-compiled segment info — computed once per route, reused on every match.
|
|
72
|
+
* Avoids repeated split()/filter()/startsWith(":") on hot paths.
|
|
73
|
+
*/
|
|
74
|
+
interface CompiledSegment {
|
|
75
|
+
/** Original segment string */
|
|
76
|
+
raw: string
|
|
77
|
+
/** true if this segment is a `:param` */
|
|
78
|
+
isParam: boolean
|
|
79
|
+
/** true if this segment is a `:param*` splat */
|
|
80
|
+
isSplat: boolean
|
|
81
|
+
/** Param name (without leading `:` and trailing `*`) — empty for static segments */
|
|
82
|
+
paramName: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface CompiledRoute {
|
|
86
|
+
route: RouteRecord
|
|
87
|
+
/** true for wildcard patterns: `(.*)` or `*` */
|
|
88
|
+
isWildcard: boolean
|
|
89
|
+
/** Pre-split and classified segments */
|
|
90
|
+
segments: CompiledSegment[]
|
|
91
|
+
/** Number of segments */
|
|
92
|
+
segmentCount: number
|
|
93
|
+
/** true if route has no dynamic segments (pure static) */
|
|
94
|
+
isStatic: boolean
|
|
95
|
+
/** For static routes: the normalized path (e.g. "/about") for Map lookup */
|
|
96
|
+
staticPath: string | null
|
|
97
|
+
/** Compiled children (lazily populated) */
|
|
98
|
+
children: CompiledRoute[] | null
|
|
99
|
+
/** First static segment (for dispatch index), null if first segment is dynamic or route is wildcard */
|
|
100
|
+
firstSegment: string | null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A flattened route entry — pre-joins parent+child segments at compile time
|
|
105
|
+
* so nested routes can be matched in a single pass without recursion.
|
|
106
|
+
*/
|
|
107
|
+
interface FlattenedRoute {
|
|
108
|
+
/** All segments from root to leaf, concatenated */
|
|
109
|
+
segments: CompiledSegment[]
|
|
110
|
+
segmentCount: number
|
|
111
|
+
/** The full matched chain from root to leaf (e.g. [adminLayout, usersPage]) */
|
|
112
|
+
matchedChain: RouteRecord[]
|
|
113
|
+
/** true if all segments are static */
|
|
114
|
+
isStatic: boolean
|
|
115
|
+
/** For static flattened routes: the full joined path */
|
|
116
|
+
staticPath: string | null
|
|
117
|
+
/** Pre-merged meta from all routes in the chain */
|
|
118
|
+
meta: RouteMeta
|
|
119
|
+
/** First static segment for dispatch index */
|
|
120
|
+
firstSegment: string | null
|
|
121
|
+
/** true if any segment is a splat */
|
|
122
|
+
hasSplat: boolean
|
|
123
|
+
/** true if this is a wildcard catch-all route (`*` or `(.*)`) */
|
|
124
|
+
isWildcard: boolean
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** WeakMap cache: compile each RouteRecord[] once */
|
|
128
|
+
const _compiledCache = new WeakMap<RouteRecord[], CompiledRoute[]>()
|
|
129
|
+
|
|
130
|
+
function compileSegment(raw: string): CompiledSegment {
|
|
131
|
+
if (raw.endsWith("*") && raw.startsWith(":")) {
|
|
132
|
+
return { raw, isParam: true, isSplat: true, paramName: raw.slice(1, -1) }
|
|
133
|
+
}
|
|
134
|
+
if (raw.startsWith(":")) {
|
|
135
|
+
return { raw, isParam: true, isSplat: false, paramName: raw.slice(1) }
|
|
136
|
+
}
|
|
137
|
+
return { raw, isParam: false, isSplat: false, paramName: "" }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function compileRoute(route: RouteRecord): CompiledRoute {
|
|
141
|
+
const pattern = route.path
|
|
142
|
+
const isWildcard = pattern === "(.*)" || pattern === "*"
|
|
143
|
+
|
|
144
|
+
if (isWildcard) {
|
|
145
|
+
return {
|
|
146
|
+
route,
|
|
147
|
+
isWildcard: true,
|
|
148
|
+
segments: [],
|
|
149
|
+
segmentCount: 0,
|
|
150
|
+
isStatic: false,
|
|
151
|
+
staticPath: null,
|
|
152
|
+
children: null,
|
|
153
|
+
firstSegment: null,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const segments = pattern.split("/").filter(Boolean).map(compileSegment)
|
|
158
|
+
const isStatic = segments.every((s) => !s.isParam)
|
|
159
|
+
const staticPath = isStatic ? `/${segments.map((s) => s.raw).join("/")}` : null
|
|
160
|
+
const first = segments.length > 0 ? segments[0] : undefined
|
|
161
|
+
const firstSegment = first && !first.isParam ? first.raw : null
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
route,
|
|
165
|
+
isWildcard: false,
|
|
166
|
+
segments,
|
|
167
|
+
segmentCount: segments.length,
|
|
168
|
+
isStatic,
|
|
169
|
+
staticPath,
|
|
170
|
+
children: null,
|
|
171
|
+
firstSegment,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function compileRoutes(routes: RouteRecord[]): CompiledRoute[] {
|
|
176
|
+
const cached = _compiledCache.get(routes)
|
|
177
|
+
if (cached) return cached
|
|
178
|
+
|
|
179
|
+
const compiled = routes.map((r) => {
|
|
180
|
+
const c = compileRoute(r)
|
|
181
|
+
if (r.children && r.children.length > 0) {
|
|
182
|
+
c.children = compileRoutes(r.children)
|
|
183
|
+
}
|
|
184
|
+
return c
|
|
185
|
+
})
|
|
186
|
+
_compiledCache.set(routes, compiled)
|
|
187
|
+
return compiled
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─── Route flattening ────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
/** Extract first static segment from a segment list, or null if dynamic/empty */
|
|
193
|
+
function getFirstSegment(segments: CompiledSegment[]): string | null {
|
|
194
|
+
const first = segments[0]
|
|
195
|
+
if (first && !first.isParam) return first.raw
|
|
196
|
+
return null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Build a FlattenedRoute from segments + metadata */
|
|
200
|
+
function makeFlatEntry(
|
|
201
|
+
segments: CompiledSegment[],
|
|
202
|
+
chain: RouteRecord[],
|
|
203
|
+
meta: RouteMeta,
|
|
204
|
+
isWildcard: boolean,
|
|
205
|
+
): FlattenedRoute {
|
|
206
|
+
const isStatic = !isWildcard && segments.every((s) => !s.isParam)
|
|
207
|
+
return {
|
|
208
|
+
segments,
|
|
209
|
+
segmentCount: segments.length,
|
|
210
|
+
matchedChain: chain,
|
|
211
|
+
isStatic,
|
|
212
|
+
staticPath: isStatic ? `/${segments.map((s) => s.raw).join("/")}` : null,
|
|
213
|
+
meta,
|
|
214
|
+
firstSegment: getFirstSegment(segments),
|
|
215
|
+
hasSplat: segments.some((s) => s.isSplat),
|
|
216
|
+
isWildcard,
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Flatten nested routes into leaf entries with pre-joined segments.
|
|
222
|
+
* This eliminates recursion during matching for the common case.
|
|
223
|
+
*/
|
|
224
|
+
function flattenRoutes(compiled: CompiledRoute[]): FlattenedRoute[] {
|
|
225
|
+
const result: FlattenedRoute[] = []
|
|
226
|
+
flattenWalk(result, compiled, [], [], {})
|
|
227
|
+
return result
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function flattenWalk(
|
|
231
|
+
result: FlattenedRoute[],
|
|
232
|
+
routes: CompiledRoute[],
|
|
233
|
+
parentSegments: CompiledSegment[],
|
|
234
|
+
parentChain: RouteRecord[],
|
|
235
|
+
parentMeta: RouteMeta,
|
|
236
|
+
): void {
|
|
237
|
+
for (const c of routes) {
|
|
238
|
+
const chain = [...parentChain, c.route]
|
|
239
|
+
const meta = c.route.meta ? { ...parentMeta, ...c.route.meta } : { ...parentMeta }
|
|
240
|
+
flattenOne(result, c, parentSegments, chain, meta)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function flattenOne(
|
|
245
|
+
result: FlattenedRoute[],
|
|
246
|
+
c: CompiledRoute,
|
|
247
|
+
parentSegments: CompiledSegment[],
|
|
248
|
+
chain: RouteRecord[],
|
|
249
|
+
meta: RouteMeta,
|
|
250
|
+
): void {
|
|
251
|
+
if (c.isWildcard) {
|
|
252
|
+
result.push(makeFlatEntry(parentSegments, chain, meta, true))
|
|
253
|
+
if (c.children && c.children.length > 0) {
|
|
254
|
+
flattenWalk(result, c.children, parentSegments, chain, meta)
|
|
255
|
+
}
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const joined = [...parentSegments, ...c.segments]
|
|
260
|
+
if (c.children && c.children.length > 0) {
|
|
261
|
+
flattenWalk(result, c.children, joined, chain, meta)
|
|
262
|
+
}
|
|
263
|
+
result.push(makeFlatEntry(joined, chain, meta, false))
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ─── Combined index ─────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
interface RouteIndex {
|
|
269
|
+
/** O(1) lookup for fully static paths (including nested) */
|
|
270
|
+
staticMap: Map<string, FlattenedRoute>
|
|
271
|
+
/** First-segment dispatch: maps first path segment → candidate routes */
|
|
272
|
+
segmentMap: Map<string, FlattenedRoute[]>
|
|
273
|
+
/** Routes whose first segment is dynamic (fallback) */
|
|
274
|
+
dynamicFirst: FlattenedRoute[]
|
|
275
|
+
/** Wildcard/catch-all routes */
|
|
276
|
+
wildcards: FlattenedRoute[]
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const _indexCache = new WeakMap<RouteRecord[], RouteIndex>()
|
|
280
|
+
|
|
281
|
+
/** Classify a single flattened route into the appropriate index bucket */
|
|
282
|
+
function indexFlatRoute(
|
|
283
|
+
f: FlattenedRoute,
|
|
284
|
+
staticMap: Map<string, FlattenedRoute>,
|
|
285
|
+
segmentMap: Map<string, FlattenedRoute[]>,
|
|
286
|
+
dynamicFirst: FlattenedRoute[],
|
|
287
|
+
wildcards: FlattenedRoute[],
|
|
288
|
+
): void {
|
|
289
|
+
// Static map: first static entry wins (preserves definition order)
|
|
290
|
+
if (f.isStatic && f.staticPath && !staticMap.has(f.staticPath)) {
|
|
291
|
+
staticMap.set(f.staticPath, f)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (f.isWildcard) {
|
|
295
|
+
wildcards.push(f)
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Root route "/" has 0 segments — already in static map
|
|
300
|
+
if (f.segmentCount === 0) return
|
|
301
|
+
|
|
302
|
+
// First-segment dispatch
|
|
303
|
+
if (f.firstSegment) {
|
|
304
|
+
let bucket = segmentMap.get(f.firstSegment)
|
|
305
|
+
if (!bucket) {
|
|
306
|
+
bucket = []
|
|
307
|
+
segmentMap.set(f.firstSegment, bucket)
|
|
308
|
+
}
|
|
309
|
+
bucket.push(f)
|
|
310
|
+
} else {
|
|
311
|
+
dynamicFirst.push(f)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function buildRouteIndex(routes: RouteRecord[], compiled: CompiledRoute[]): RouteIndex {
|
|
316
|
+
const cached = _indexCache.get(routes)
|
|
317
|
+
if (cached) return cached
|
|
318
|
+
|
|
319
|
+
const flattened = flattenRoutes(compiled)
|
|
320
|
+
|
|
321
|
+
const staticMap = new Map<string, FlattenedRoute>()
|
|
322
|
+
const segmentMap = new Map<string, FlattenedRoute[]>()
|
|
323
|
+
const dynamicFirst: FlattenedRoute[] = []
|
|
324
|
+
const wildcards: FlattenedRoute[] = []
|
|
325
|
+
|
|
326
|
+
for (const f of flattened) {
|
|
327
|
+
indexFlatRoute(f, staticMap, segmentMap, dynamicFirst, wildcards)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const index: RouteIndex = { staticMap, segmentMap, dynamicFirst, wildcards }
|
|
331
|
+
_indexCache.set(routes, index)
|
|
332
|
+
return index
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ─── Fast path splitting ─────────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
/** Split path into segments without allocating a filtered array */
|
|
338
|
+
function splitPath(path: string): string[] {
|
|
339
|
+
// Fast path for common cases
|
|
340
|
+
if (path === "/") return []
|
|
341
|
+
// Remove leading slash, split, no filter needed if path is clean
|
|
342
|
+
const start = path.charCodeAt(0) === 47 /* / */ ? 1 : 0
|
|
343
|
+
const end = path.length
|
|
344
|
+
if (start >= end) return []
|
|
345
|
+
|
|
346
|
+
const parts: string[] = []
|
|
347
|
+
let segStart = start
|
|
348
|
+
for (let i = start; i <= end; i++) {
|
|
349
|
+
if (i === end || path.charCodeAt(i) === 47 /* / */) {
|
|
350
|
+
if (i > segStart) {
|
|
351
|
+
parts.push(path.substring(segStart, i))
|
|
352
|
+
}
|
|
353
|
+
segStart = i + 1
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return parts
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Decode only if the segment contains a `%` character */
|
|
360
|
+
function decodeSafe(s: string): string {
|
|
361
|
+
return s.indexOf("%") >= 0 ? decodeURIComponent(s) : s
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ─── Path matching (compiled) ────────────────────────────────────────────────
|
|
69
365
|
|
|
70
366
|
/**
|
|
71
367
|
* Match a single route pattern against a path segment.
|
|
@@ -104,47 +400,78 @@ export function matchPath(pattern: string, path: string): Record<string, string>
|
|
|
104
400
|
return params
|
|
105
401
|
}
|
|
106
402
|
|
|
107
|
-
|
|
108
|
-
* Check if a path starts with a route's prefix (for nested route matching).
|
|
109
|
-
* Returns the remaining path suffix, or null if no match.
|
|
110
|
-
*/
|
|
111
|
-
function matchPrefix(
|
|
112
|
-
pattern: string,
|
|
113
|
-
path: string,
|
|
114
|
-
): { params: Record<string, string>; rest: string } | null {
|
|
115
|
-
if (pattern === "(.*)" || pattern === "*") return { params: {}, rest: path }
|
|
403
|
+
// ─── Compiled matching helpers ────────────────────────────────────────────────
|
|
116
404
|
|
|
117
|
-
|
|
118
|
-
|
|
405
|
+
/** Collect remaining path segments as a decoded splat value */
|
|
406
|
+
function captureSplat(pathParts: string[], from: number, pathLen: number): string {
|
|
407
|
+
const remaining: string[] = []
|
|
408
|
+
for (let j = from; j < pathLen; j++) {
|
|
409
|
+
const p = pathParts[j]
|
|
410
|
+
if (p !== undefined) remaining.push(decodeSafe(p))
|
|
411
|
+
}
|
|
412
|
+
return remaining.join("/")
|
|
413
|
+
}
|
|
119
414
|
|
|
120
|
-
|
|
415
|
+
// ─── Flattened route matching ─────────────────────────────────────────────────
|
|
416
|
+
|
|
417
|
+
/** Try to match a flattened route against path parts */
|
|
418
|
+
function matchFlattened(
|
|
419
|
+
f: FlattenedRoute,
|
|
420
|
+
pathParts: string[],
|
|
421
|
+
pathLen: number,
|
|
422
|
+
): Record<string, string> | null {
|
|
423
|
+
if (f.segmentCount !== pathLen) {
|
|
424
|
+
// Could still match if route has a splat
|
|
425
|
+
if (!f.hasSplat || pathLen < f.segmentCount) return null
|
|
426
|
+
}
|
|
121
427
|
|
|
122
428
|
const params: Record<string, string> = {}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
429
|
+
const segments = f.segments
|
|
430
|
+
const count = f.segmentCount
|
|
431
|
+
for (let i = 0; i < count; i++) {
|
|
432
|
+
const seg = segments[i]
|
|
433
|
+
const pt = pathParts[i]
|
|
434
|
+
if (!seg || pt === undefined) return null
|
|
435
|
+
if (seg.isSplat) {
|
|
436
|
+
params[seg.paramName] = captureSplat(pathParts, i, pathLen)
|
|
437
|
+
return params
|
|
131
438
|
}
|
|
132
|
-
if (
|
|
133
|
-
params[
|
|
134
|
-
} else if (
|
|
439
|
+
if (seg.isParam) {
|
|
440
|
+
params[seg.paramName] = decodeSafe(pt)
|
|
441
|
+
} else if (seg.raw !== pt) {
|
|
135
442
|
return null
|
|
136
443
|
}
|
|
137
444
|
}
|
|
445
|
+
return params
|
|
446
|
+
}
|
|
138
447
|
|
|
139
|
-
|
|
140
|
-
|
|
448
|
+
/** Search a list of flattened candidates for a match */
|
|
449
|
+
function searchCandidates(
|
|
450
|
+
candidates: FlattenedRoute[],
|
|
451
|
+
pathParts: string[],
|
|
452
|
+
pathLen: number,
|
|
453
|
+
): MatchResult | null {
|
|
454
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
455
|
+
const f = candidates[i]
|
|
456
|
+
if (!f) continue
|
|
457
|
+
const params = matchFlattened(f, pathParts, pathLen)
|
|
458
|
+
if (params) {
|
|
459
|
+
return { params, matched: f.matchedChain }
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return null
|
|
141
463
|
}
|
|
142
464
|
|
|
143
465
|
// ─── Route resolution ─────────────────────────────────────────────────────────
|
|
144
466
|
|
|
467
|
+
interface MatchResult {
|
|
468
|
+
params: Record<string, string>
|
|
469
|
+
matched: RouteRecord[]
|
|
470
|
+
}
|
|
471
|
+
|
|
145
472
|
/**
|
|
146
473
|
* Resolve a raw path (including query string and hash) against the route tree.
|
|
147
|
-
*
|
|
474
|
+
* Uses flattened index for O(1) static lookup and first-segment dispatch.
|
|
148
475
|
*/
|
|
149
476
|
export function resolveRoute(rawPath: string, routes: RouteRecord[]): ResolvedRoute {
|
|
150
477
|
const qIdx = rawPath.indexOf("?")
|
|
@@ -157,63 +484,73 @@ export function resolveRoute(rawPath: string, routes: RouteRecord[]): ResolvedRo
|
|
|
157
484
|
|
|
158
485
|
const query = parseQuery(queryPart)
|
|
159
486
|
|
|
160
|
-
|
|
161
|
-
|
|
487
|
+
// Build index (cached after first call)
|
|
488
|
+
const compiled = compileRoutes(routes)
|
|
489
|
+
const index = buildRouteIndex(routes, compiled)
|
|
490
|
+
|
|
491
|
+
// Fast path 1: O(1) static Map lookup (covers nested static too)
|
|
492
|
+
const staticMatch = index.staticMap.get(cleanPath)
|
|
493
|
+
if (staticMatch) {
|
|
162
494
|
return {
|
|
163
495
|
path: cleanPath,
|
|
164
|
-
params:
|
|
496
|
+
params: {},
|
|
165
497
|
query,
|
|
166
498
|
hash,
|
|
167
|
-
matched:
|
|
168
|
-
meta:
|
|
499
|
+
matched: staticMatch.matchedChain,
|
|
500
|
+
meta: staticMatch.meta,
|
|
169
501
|
}
|
|
170
502
|
}
|
|
171
503
|
|
|
172
|
-
|
|
173
|
-
|
|
504
|
+
// Split path for segment-based matching
|
|
505
|
+
const pathParts = splitPath(cleanPath)
|
|
506
|
+
const pathLen = pathParts.length
|
|
174
507
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
508
|
+
// Fast path 2: first-segment dispatch (O(1) bucket lookup + small scan)
|
|
509
|
+
if (pathLen > 0) {
|
|
510
|
+
const first = pathParts[0] as string
|
|
511
|
+
const bucket = index.segmentMap.get(first)
|
|
512
|
+
if (bucket) {
|
|
513
|
+
const match = searchCandidates(bucket, pathParts, pathLen)
|
|
514
|
+
if (match) {
|
|
515
|
+
return {
|
|
516
|
+
path: cleanPath,
|
|
517
|
+
params: match.params,
|
|
518
|
+
query,
|
|
519
|
+
hash,
|
|
520
|
+
matched: match.matched,
|
|
521
|
+
meta: mergeMeta(match.matched),
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
189
525
|
}
|
|
190
|
-
return null
|
|
191
|
-
}
|
|
192
526
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
527
|
+
// Fallback: dynamic-first-segment routes
|
|
528
|
+
const dynMatch = searchCandidates(index.dynamicFirst, pathParts, pathLen)
|
|
529
|
+
if (dynMatch) {
|
|
530
|
+
return {
|
|
531
|
+
path: cleanPath,
|
|
532
|
+
params: dynMatch.params,
|
|
533
|
+
query,
|
|
534
|
+
hash,
|
|
535
|
+
matched: dynMatch.matched,
|
|
536
|
+
meta: mergeMeta(dynMatch.matched),
|
|
537
|
+
}
|
|
203
538
|
}
|
|
204
539
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
540
|
+
// Fallback: wildcard/catch-all routes
|
|
541
|
+
const w = index.wildcards[0]
|
|
542
|
+
if (w) {
|
|
543
|
+
return {
|
|
544
|
+
path: cleanPath,
|
|
545
|
+
params: {},
|
|
546
|
+
query,
|
|
547
|
+
hash,
|
|
548
|
+
matched: w.matchedChain,
|
|
549
|
+
meta: w.meta,
|
|
550
|
+
}
|
|
551
|
+
}
|
|
213
552
|
|
|
214
|
-
|
|
215
|
-
if (exactParams === null) return null
|
|
216
|
-
return { params: { ...parentParams, ...exactParams }, matched }
|
|
553
|
+
return { path: cleanPath, params: {}, query, hash, matched: [], meta: {} }
|
|
217
554
|
}
|
|
218
555
|
|
|
219
556
|
/** Merge meta from matched routes (leaf takes precedence) */
|