Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
389540f
flat AST parsing
pcattori Dec 10, 2025
50095d5
variants
pcattori Dec 10, 2025
a350c1c
regexp
pcattori Dec 10, 2025
99057dd
bench parse part
pcattori Dec 10, 2025
13701d1
span
pcattori Dec 10, 2025
68e69b9
split (copied from lib)
pcattori Dec 10, 2025
380df7c
route-pattern
pcattori Dec 10, 2025
25eec84
regexp escape
pcattori Dec 11, 2025
e8255b5
variant as key + paramIndices
pcattori Dec 11, 2025
64e9476
part api
pcattori Dec 11, 2025
2969df1
vitest
pcattori Dec 11, 2025
81601b3
route-pattern variants
pcattori Dec 11, 2025
102f2e1
route-pattern namespace
pcattori Dec 12, 2025
52ec2c0
trie
pcattori Dec 12, 2025
844cb48
wip: matchers namespace
pcattori Dec 12, 2025
d3dc321
trie class + variant param names + bug fixes
pcattori Dec 13, 2025
9b9a6cb
todos
pcattori Dec 13, 2025
bd07dc1
trie matcher
pcattori Dec 13, 2025
4a6a951
fix: add wildcard name even when that param is the first one (index = 0)
pcattori Dec 13, 2025
0e2659f
params (for now without `undefined` for unmatched params, will add la…
pcattori Dec 13, 2025
8b500e2
full params + explicit es2025 backports
pcattori Dec 13, 2025
ef31f37
fix wildcard deduplication in trie branches
pcattori Dec 14, 2025
9471879
state paramNames as arrays
pcattori Dec 14, 2025
057eb6a
ranking!
pcattori Dec 14, 2025
946cab3
fix: wildcard rankIndex
pcattori Dec 15, 2025
dc91151
better ranking
pcattori Dec 15, 2025
d9c32a5
fix outdated tests
pcattori Dec 15, 2025
d7c3fc6
Matcher.match returns params + data
pcattori Dec 15, 2025
fa3d9bb
TrieMatcher doesn't expose `rank`
pcattori Dec 15, 2025
5a75823
fix rank comparison
pcattori Dec 15, 2025
6b91dbf
demo
pcattori Dec 15, 2025
9f993b2
matchAll
pcattori Dec 15, 2025
b8ebf3a
matchAll demo
pcattori Dec 15, 2025
f7e5f6a
add lib2 triematcher to comparison benchmark
pcattori Dec 16, 2025
ab5e920
reorg
pcattori Dec 16, 2025
c05278d
remove part parse benchmark
pcattori Dec 16, 2025
16024e2
fix ERR_UNSUPPORTED_DIR_IMPORT
pcattori Dec 16, 2025
1a69ff0
fix optional matching when begin index = 0
pcattori Dec 16, 2025
6e5d2f3
join
pcattori Dec 16, 2025
283902d
shallow copy
pcattori Dec 16, 2025
1067380
nameIndex: undefined (value optional, not key optional)
pcattori Dec 16, 2025
b05dce5
refactor + port matching
pcattori Dec 17, 2025
2de6d4e
fix Rank.comparison when rank only differs beyond length of other rank
pcattori Dec 17, 2025
f971fc8
replace Object.entries(...) with for...in
pcattori Dec 17, 2025
7305bf5
optimize simple dynamic segments
pcattori Dec 17, 2025
58089f0
RoutePattern search
pcattori Dec 18, 2025
8e9d2ab
search ranking
pcattori Dec 18, 2025
898a6fe
search ranking in demo
pcattori Dec 18, 2025
d25263e
derive paramNames directly in trie.ts
pcattori Dec 18, 2025
6db4b17
unnamed wilcards get '*' as their name
pcattori Dec 18, 2025
423e3d8
remove unused Part.toRegExp
pcattori Dec 18, 2025
856a305
ParseError
pcattori Dec 18, 2025
330b466
reorg
pcattori Dec 18, 2025
2eb6773
rename "part" to "part pattern"
pcattori Dec 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/route-pattern/bench/comparison.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { bench, describe } from 'vitest'
import FindMyWay from 'find-my-way'
import { match } from 'path-to-regexp'
import { ArrayMatcher, TrieMatcher } from '../src'
import { TrieMatcher as TrieMatcher2 } from '../src/lib2'

type Syntax = 'route-pattern' | 'find-my-way' | 'path-to-regexp'

Expand All @@ -24,6 +25,11 @@ const matchers: Array<{
syntax: Syntax
createMatcher: () => Matcher
}> = [
{
name: 'route-pattern/lib2/trie',
syntax: 'route-pattern',
createMatcher: () => new TrieMatcher2(),
},
{
name: 'route-pattern/trie',
syntax: 'route-pattern',
Expand Down
1 change: 1 addition & 0 deletions packages/route-pattern/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@ark/attest": "^0.49.0",
"@types/node": "catalog:",
"@typescript/native-preview": "catalog:",
"dedent": "^1.7.1",
"find-my-way": "^9.1.0",
"path-to-regexp": "^8.2.0",
"typescript": "catalog:",
Expand Down
23 changes: 23 additions & 0 deletions packages/route-pattern/src/lib2/errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import dedent from 'dedent'
import { describe, expect, it } from 'vitest'

import { ParseError } from './errors.ts'

describe('ParseError', () => {
it('exposes type, source, and index properties', () => {
let error = new ParseError('unmatched (', 'foo(bar', 3)
expect(error.type).toBe('unmatched (')
expect(error.source).toBe('foo(bar')
expect(error.index).toBe(3)
})

it('shows caret under the problematic index', () => {
let error = new ParseError('unmatched (', 'api/(v:major', 4)
expect(error.toString()).toBe(dedent`
ParseError: unmatched (

api/(v:major
^
`)
})
})
18 changes: 18 additions & 0 deletions packages/route-pattern/src/lib2/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
type ParseErrorType = 'unmatched (' | 'unmatched )' | 'missing variable name' | 'dangling escape'

export class ParseError extends Error {
type: ParseErrorType
source: string
index: number

constructor(type: ParseErrorType, source: string, index: number) {
let underline = ' '.repeat(index) + '^'
let message = `${type}\n\n${source}\n${underline}`

super(message)
this.name = 'ParseError'
this.type = type
this.source = source
this.index = index
}
}
5 changes: 5 additions & 0 deletions packages/route-pattern/src/lib2/es2025.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* Backport of [RegExp.escape](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape)
*/
export const RegExp_escape = (string: string): string =>
string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
2 changes: 2 additions & 0 deletions packages/route-pattern/src/lib2/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { TrieMatcher } from './matchers/index.ts'
export { ParseError } from './errors.ts'
2 changes: 2 additions & 0 deletions packages/route-pattern/src/lib2/matchers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { Matcher } from './matcher.ts'
export { TrieMatcher } from './trie/index.ts'
10 changes: 10 additions & 0 deletions packages/route-pattern/src/lib2/matchers/matcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type * as RoutePattern from '../route-pattern/index.ts'

export type Params = Record<string, string | undefined>

export type Matcher<data> = {
add: (pattern: RoutePattern.AST, data: data) => void
match: (url: URL) => { params: Params; data: data } | null
matchAll: (url: URL) => Array<{ params: Params; data: data }>
size: number
}
1 change: 1 addition & 0 deletions packages/route-pattern/src/lib2/matchers/trie/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { TrieMatcher } from './matcher.ts'
37 changes: 37 additions & 0 deletions packages/route-pattern/src/lib2/matchers/trie/matcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as RoutePattern from '../../route-pattern/index.ts'
import { Trie, type Match } from './trie.ts'
import * as Rank from './rank.ts'

export class TrieMatcher<data> {
#trie: Trie<data> = new Trie()
#size: number = 0

add(pattern: string | RoutePattern.AST, data: data) {
pattern = typeof pattern === 'string' ? RoutePattern.parse(pattern) : pattern
this.#trie.insert(pattern, data)
this.#size += 1
}

match(url: URL) {
let best: Match<data> | null = null
for (let match of this.#trie.search(url)) {
if (best === null || Rank.lessThan(match.rank, best.rank)) {
best = match
}
}
return best ? { params: best.params, data: best.data } : null
}

matchAll(url: URL) {
let matches = []
for (let match of this.#trie.search(url)) {
matches.push(match)
}
matches.sort((a, b) => Rank.compare(a.rank, b.rank))
return matches
}

get size() {
return this.#size
}
}
28 changes: 28 additions & 0 deletions packages/route-pattern/src/lib2/matchers/trie/rank.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as RoutePattern from '../../route-pattern/index.ts'

type Rank = {
hierarchical: Array<string>
search: RoutePattern.Search.Constraints
}

export function lessThan(a: Rank, b: Rank): boolean {
return compare(a, b) === -1
}

export function compare(a: Rank, b: Rank): number {
let hierarchical = compareHierarchical(a.hierarchical, b.hierarchical)
if (hierarchical !== 0) return hierarchical
return RoutePattern.Search.compare(a.search, b.search)
}

function compareHierarchical(a: Rank['hierarchical'], b: Rank['hierarchical']): -1 | 0 | 1 {
for (let i = 0; i < a.length; i++) {
let segmentA = a[i]
let segmentB = b[i]
if (segmentA < segmentB) return -1
if (segmentA > segmentB) return 1
}
if (a.length < b.length) return -1
if (a.length > b.length) return 1
return 0
}
Loading