@buzola/router 0.0.8 → 0.0.10

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.
@@ -2,7 +2,7 @@ import type { RouteMatch, RouteNode } from './types.js';
2
2
  /**
3
3
  * Match a URL against the route tree.
4
4
  * Returns an array of RouteMatch from root layout to leaf page, or null if no match.
5
- * Performs depth-first matching.
5
+ * When multiple routes match, selects the most specific one (fewest dynamic params).
6
6
  */
7
7
  export declare function matchRoutes(nodes: RouteNode[], url: URL): RouteMatch[] | null;
8
8
  //# sourceMappingURL=matcher.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"matcher.d.ts","sourceRoot":"","sources":["../../src/engine/matcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAEvD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,GAAG,GAAG,UAAU,EAAE,GAAG,IAAI,CAS7E"}
1
+ {"version":3,"file":"matcher.d.ts","sourceRoot":"","sources":["../../src/engine/matcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAEvD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,GAAG,GAAG,UAAU,EAAE,GAAG,IAAI,CAG7E"}
@@ -1,31 +1,54 @@
1
1
  /**
2
2
  * Match a URL against the route tree.
3
3
  * Returns an array of RouteMatch from root layout to leaf page, or null if no match.
4
- * Performs depth-first matching.
4
+ * When multiple routes match, selects the most specific one (fewest dynamic params).
5
5
  */
6
6
  export function matchRoutes(nodes, url) {
7
7
  const pathname = url.pathname;
8
+ return pickBestMatch(nodes, pathname);
9
+ }
10
+ /**
11
+ * Find the best match among sibling nodes.
12
+ * Tries all siblings and picks the most specific match (fewest dynamic params).
13
+ */
14
+ function pickBestMatch(nodes, pathname) {
15
+ let best = null;
16
+ let bestParamCount = Infinity;
8
17
  for (const node of nodes) {
9
18
  const result = matchNode(node, pathname);
10
- if (result)
11
- return result;
19
+ if (result) {
20
+ const paramCount = countParams(result);
21
+ if (paramCount < bestParamCount) {
22
+ best = result;
23
+ bestParamCount = paramCount;
24
+ if (paramCount === 0)
25
+ break; // Can't do better than zero params
26
+ }
27
+ }
12
28
  }
13
- return null;
29
+ return best;
30
+ }
31
+ /**
32
+ * Count total dynamic params across all matches in a chain.
33
+ */
34
+ function countParams(matches) {
35
+ let count = 0;
36
+ for (const match of matches) {
37
+ count += Object.keys(match.params).length;
38
+ }
39
+ return count;
14
40
  }
15
41
  function matchNode(node, pathname) {
16
42
  // Layout nodes: try to match children, prepend self if any child matches
17
43
  if (node.isLayout && !node.isIndex) {
18
- for (const child of node.children) {
19
- const childMatch = matchNode(child, pathname);
20
- if (childMatch) {
21
- // Layout is part of the match chain
22
- const layoutMatch = {
23
- node,
24
- params: extractLayoutParams(node, pathname),
25
- pathname,
26
- };
27
- return [layoutMatch, ...childMatch];
28
- }
44
+ const childMatch = pickBestMatch(node.children, pathname);
45
+ if (childMatch) {
46
+ const layoutMatch = {
47
+ node,
48
+ params: extractLayoutParams(node, pathname),
49
+ pathname,
50
+ };
51
+ return [layoutMatch, ...childMatch];
29
52
  }
30
53
  return null;
31
54
  }
@@ -37,11 +60,9 @@ function matchNode(node, pathname) {
37
60
  const match = { node, params, pathname };
38
61
  // If this node has children (e.g., a page with nested routes), try them too
39
62
  if (node.children.length > 0) {
40
- for (const child of node.children) {
41
- const childMatch = matchNode(child, pathname);
42
- if (childMatch) {
43
- return [match, ...childMatch];
44
- }
63
+ const childMatch = pickBestMatch(node.children, pathname);
64
+ if (childMatch) {
65
+ return [match, ...childMatch];
45
66
  }
46
67
  }
47
68
  return [match];
@@ -1 +1 @@
1
- {"version":3,"file":"matcher.js","sourceRoot":"","sources":["../../src/engine/matcher.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"matcher.js","sourceRoot":"","sources":["../../src/engine/matcher.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buzola/router",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -145,6 +145,50 @@ describe('matchRoutes', () => {
145
145
  expect(matches![1].params).toEqual({ userId: '42' })
146
146
  })
147
147
 
148
+ it('prefers static route over dynamic sibling when layout with matchPath comes first', () => {
149
+ // Simulates the real-world scenario: layout with dynamic matchPath children
150
+ // appears before a static route at the same level (e.g., /project/detail layout
151
+ // with matchPath /project/:id vs /project/create static page)
152
+ const tree = createTestTree([
153
+ {
154
+ path: '/',
155
+ component: dummyComponent,
156
+ isLayout: true,
157
+ children: [
158
+ {
159
+ path: '/project/detail',
160
+ component: dummyComponent,
161
+ isLayout: true,
162
+ children: [
163
+ { path: '/', matchPath: '/project/:id', component: dummyComponent, isIndex: true },
164
+ ],
165
+ },
166
+ { path: '/project/create', component: dummyComponent },
167
+ ],
168
+ },
169
+ ])
170
+
171
+ // /project/create should match the static route, not /project/:id with id=create
172
+ const matches = matchRoutes(tree, new URL('http://localhost/project/create'))
173
+ expect(matches).not.toBeNull()
174
+ const leaf = matches![matches!.length - 1]
175
+ expect(leaf.node.fullPath).toBe('/project/create')
176
+ expect(leaf.params).toEqual({})
177
+ })
178
+
179
+ it('prefers route with fewer dynamic params among siblings', () => {
180
+ const tree = createTestTree([
181
+ { path: '/users/:userId', component: dummyComponent },
182
+ { path: '/users/me', component: dummyComponent },
183
+ ])
184
+
185
+ const matches = matchRoutes(tree, new URL('http://localhost/users/me'))
186
+ expect(matches).not.toBeNull()
187
+ expect(matches).toHaveLength(1)
188
+ expect(matches![0].node.fullPath).toBe('/users/me')
189
+ expect(matches![0].params).toEqual({})
190
+ })
191
+
148
192
  it('does not match partial paths', () => {
149
193
  const tree = createTestTree([
150
194
  { path: '/about', component: dummyComponent },
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it } from 'bun:test'
2
2
  import React from 'react'
3
3
  import { createPage } from '../define/create-page.js'
4
- import type { StandardSchema } from '../engine/types.js'
4
+ import type { EffectivePageParams, NavigateOptions, RegisteredPage, StandardSchema } from '../engine/types.js'
5
5
  import type { LinkProps } from '../react/link.js'
6
6
 
7
7
  // Register pages for type-safe testing
@@ -229,3 +229,76 @@ describe('Link JSX generic inference', () => {
229
229
  expect(true).toBe(true)
230
230
  })
231
231
  })
232
+
233
+ // ─── NavigateFn type safety ─────────────────────────────────────────────────
234
+
235
+ /**
236
+ * Replicate the NavigateFn type from hooks.ts for testing without React hooks.
237
+ */
238
+ type NavigateFn = <P extends RegisteredPage>(
239
+ to: P,
240
+ ...args: [keyof EffectivePageParams<P>] extends [never] ? [options?: NavigateOptions]
241
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
242
+ : {} extends EffectivePageParams<P> ? [options?: NavigateOptions & { params?: EffectivePageParams<P> }]
243
+ : [options: NavigateOptions & { params: EffectivePageParams<P> }]
244
+ ) => void
245
+
246
+ describe('NavigateFn type safety', () => {
247
+ const noop = (() => {}) as NavigateFn
248
+
249
+ it('static page does not require options', () => {
250
+ noop('home')
251
+ noop('about')
252
+ expect(true).toBe(true)
253
+ })
254
+
255
+ it('static page accepts NavigateOptions without params', () => {
256
+ noop('home', { replace: true })
257
+ expect(true).toBe(true)
258
+ })
259
+
260
+ it('static page rejects params', () => {
261
+ // @ts-expect-error — static page should not accept params
262
+ noop('home', { params: { foo: 'bar' } })
263
+ expect(true).toBe(true)
264
+ })
265
+
266
+ it('dynamic page requires options with params', () => {
267
+ noop('users/detail', { params: { userId: '1' } })
268
+
269
+ // @ts-expect-error — missing required options with params
270
+ noop('users/detail')
271
+ expect(true).toBe(true)
272
+ })
273
+
274
+ it('dynamic page requires correct param names', () => {
275
+ // @ts-expect-error — wrong param name (id instead of userId)
276
+ noop('users/detail', { params: { id: '1' } })
277
+ expect(true).toBe(true)
278
+ })
279
+
280
+ it('dynamic page rejects params passed directly as options (without params wrapper)', () => {
281
+ // @ts-expect-error — { userId: '1' } is not valid, must be { params: { userId: '1' } }
282
+ noop('users/detail', { userId: '1' })
283
+ expect(true).toBe(true)
284
+ })
285
+
286
+ it('multi-param page requires all params', () => {
287
+ noop('users/posts', { params: { userId: '1', postId: '2' } })
288
+
289
+ // @ts-expect-error — missing postId
290
+ noop('users/posts', { params: { userId: '1' } })
291
+ expect(true).toBe(true)
292
+ })
293
+
294
+ it('rejects unregistered pages', () => {
295
+ // @ts-expect-error — page not in BuzolaPageMap
296
+ noop('unknown')
297
+ expect(true).toBe(true)
298
+ })
299
+
300
+ it('allows NavigateOptions alongside params', () => {
301
+ noop('users/detail', { params: { userId: '1' }, replace: true })
302
+ expect(true).toBe(true)
303
+ })
304
+ })
@@ -3,33 +3,58 @@ import type { RouteMatch, RouteNode } from './types.js'
3
3
  /**
4
4
  * Match a URL against the route tree.
5
5
  * Returns an array of RouteMatch from root layout to leaf page, or null if no match.
6
- * Performs depth-first matching.
6
+ * When multiple routes match, selects the most specific one (fewest dynamic params).
7
7
  */
8
8
  export function matchRoutes(nodes: RouteNode[], url: URL): RouteMatch[] | null {
9
9
  const pathname = url.pathname
10
+ return pickBestMatch(nodes, pathname)
11
+ }
12
+
13
+ /**
14
+ * Find the best match among sibling nodes.
15
+ * Tries all siblings and picks the most specific match (fewest dynamic params).
16
+ */
17
+ function pickBestMatch(nodes: RouteNode[], pathname: string): RouteMatch[] | null {
18
+ let best: RouteMatch[] | null = null
19
+ let bestParamCount = Infinity
10
20
 
11
21
  for (const node of nodes) {
12
22
  const result = matchNode(node, pathname)
13
- if (result) return result
23
+ if (result) {
24
+ const paramCount = countParams(result)
25
+ if (paramCount < bestParamCount) {
26
+ best = result
27
+ bestParamCount = paramCount
28
+ if (paramCount === 0) break // Can't do better than zero params
29
+ }
30
+ }
14
31
  }
15
32
 
16
- return null
33
+ return best
34
+ }
35
+
36
+ /**
37
+ * Count total dynamic params across all matches in a chain.
38
+ */
39
+ function countParams(matches: RouteMatch[]): number {
40
+ let count = 0
41
+ for (const match of matches) {
42
+ count += Object.keys(match.params).length
43
+ }
44
+ return count
17
45
  }
18
46
 
19
47
  function matchNode(node: RouteNode, pathname: string): RouteMatch[] | null {
20
48
  // Layout nodes: try to match children, prepend self if any child matches
21
49
  if (node.isLayout && !node.isIndex) {
22
- for (const child of node.children) {
23
- const childMatch = matchNode(child, pathname)
24
- if (childMatch) {
25
- // Layout is part of the match chain
26
- const layoutMatch: RouteMatch = {
27
- node,
28
- params: extractLayoutParams(node, pathname),
29
- pathname,
30
- }
31
- return [layoutMatch, ...childMatch]
50
+ const childMatch = pickBestMatch(node.children, pathname)
51
+ if (childMatch) {
52
+ const layoutMatch: RouteMatch = {
53
+ node,
54
+ params: extractLayoutParams(node, pathname),
55
+ pathname,
32
56
  }
57
+ return [layoutMatch, ...childMatch]
33
58
  }
34
59
  return null
35
60
  }
@@ -43,11 +68,9 @@ function matchNode(node: RouteNode, pathname: string): RouteMatch[] | null {
43
68
 
44
69
  // If this node has children (e.g., a page with nested routes), try them too
45
70
  if (node.children.length > 0) {
46
- for (const child of node.children) {
47
- const childMatch = matchNode(child, pathname)
48
- if (childMatch) {
49
- return [match, ...childMatch]
50
- }
71
+ const childMatch = pickBestMatch(node.children, pathname)
72
+ if (childMatch) {
73
+ return [match, ...childMatch]
51
74
  }
52
75
  }
53
76