diff --git a/e2e/react-router/compiled-matcher/.gitignore b/e2e/react-router/compiled-matcher/.gitignore
new file mode 100644
index 00000000000..4d2da67b504
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/.gitignore
@@ -0,0 +1,11 @@
+node_modules
+.DS_Store
+dist
+dist-hash
+dist-ssr
+*.local
+
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/react-router/compiled-matcher/index.html b/e2e/react-router/compiled-matcher/index.html
new file mode 100644
index 00000000000..21e30f16951
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/index.html
@@ -0,0 +1,11 @@
+
+
+
+ ()
diff --git a/e2e/react-router/compiled-matcher/src/routes/$id/bar/foo.tsx b/e2e/react-router/compiled-matcher/src/routes/$id/bar/foo.tsx
new file mode 100644
index 00000000000..d297530468a
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/$id/bar/foo.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/$id/bar/foo')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/$id/bar/foo"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/$id/foo/bar.tsx b/e2e/react-router/compiled-matcher/src/routes/$id/foo/bar.tsx
new file mode 100644
index 00000000000..47af8bdfe9b
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/$id/foo/bar.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/$id/foo/bar')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/$id/foo/bar"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/__root.tsx b/e2e/react-router/compiled-matcher/src/routes/__root.tsx
new file mode 100644
index 00000000000..3b0a92c6043
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/__root.tsx
@@ -0,0 +1,46 @@
+import {
+ HeadContent,
+ Link,
+ Outlet,
+ createRootRoute,
+ useRouter,
+} from '@tanstack/react-router'
+import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
+
+export const Route = createRootRoute({
+ component: RootComponent,
+ notFoundComponent: () => {
+ return (
+
+
This is the notFoundComponent configured on root route
+
Start Over
+
+ )
+ },
+})
+
+function RootComponent() {
+ const router = useRouter()
+ return (
+ <>
+
+
+ {Object.keys(router.routesByPath).map((to) => (
+
+ {to}
+
+ ))}
+
+
+
+ {/* Start rendering router matches */}
+
+ >
+ )
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/a/$.tsx b/e2e/react-router/compiled-matcher/src/routes/a/$.tsx
new file mode 100644
index 00000000000..c1efe777e3e
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/a/$.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/a/$')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/a/$"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/a/$id.tsx b/e2e/react-router/compiled-matcher/src/routes/a/$id.tsx
new file mode 100644
index 00000000000..563909e296b
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/a/$id.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/a/$id')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/a/$id"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/a/b/c/d/e/f.tsx b/e2e/react-router/compiled-matcher/src/routes/a/b/c/d/e/f.tsx
new file mode 100644
index 00000000000..e6cb4db25ee
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/a/b/c/d/e/f.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/a/b/c/d/e/f')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/a/b/c/d/e/f"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/a/index.tsx b/e2e/react-router/compiled-matcher/src/routes/a/index.tsx
new file mode 100644
index 00000000000..9f3aaa404e5
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/a/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/a/')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/a/"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/a/profile/index.tsx b/e2e/react-router/compiled-matcher/src/routes/a/profile/index.tsx
new file mode 100644
index 00000000000..5d67db2e70d
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/a/profile/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/a/profile/')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/a/profile/"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/a/profile/settings.tsx b/e2e/react-router/compiled-matcher/src/routes/a/profile/settings.tsx
new file mode 100644
index 00000000000..517b4219fe5
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/a/profile/settings.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/a/profile/settings')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/a/profile/settings"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/a/user-{$id}.tsx b/e2e/react-router/compiled-matcher/src/routes/a/user-{$id}.tsx
new file mode 100644
index 00000000000..d8b6f32fc17
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/a/user-{$id}.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/a/user-{$id}')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return {'Hello "/a/user-{$id}"!'}
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/a/{-$slug}.tsx b/e2e/react-router/compiled-matcher/src/routes/a/{-$slug}.tsx
new file mode 100644
index 00000000000..8ce00b261f7
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/a/{-$slug}.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/a/{-$slug}')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return {'Hello "/a/{-$slug}"!'}
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/about.tsx b/e2e/react-router/compiled-matcher/src/routes/about.tsx
new file mode 100644
index 00000000000..1e6c7068e00
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/about.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/about')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/about"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/api/user-{$id}.tsx b/e2e/react-router/compiled-matcher/src/routes/api/user-{$id}.tsx
new file mode 100644
index 00000000000..85578d57917
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/api/user-{$id}.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/api/user-{$id}')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return {'Hello "/api/user-{$id}"!'}
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/b/$.tsx b/e2e/react-router/compiled-matcher/src/routes/b/$.tsx
new file mode 100644
index 00000000000..1d360267045
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/b/$.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/b/$')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/b/$"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/b/$id.tsx b/e2e/react-router/compiled-matcher/src/routes/b/$id.tsx
new file mode 100644
index 00000000000..004b382eaed
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/b/$id.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/b/$id')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/b/$id"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/b/index.tsx b/e2e/react-router/compiled-matcher/src/routes/b/index.tsx
new file mode 100644
index 00000000000..dc875444191
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/b/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/b/')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/b/"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/b/profile/index.tsx b/e2e/react-router/compiled-matcher/src/routes/b/profile/index.tsx
new file mode 100644
index 00000000000..5f32f2d4eef
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/b/profile/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/b/profile/')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/b/profile/"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/b/profile/settings.tsx b/e2e/react-router/compiled-matcher/src/routes/b/profile/settings.tsx
new file mode 100644
index 00000000000..eeec76d78ab
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/b/profile/settings.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/b/profile/settings')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/b/profile/settings"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/b/user-{$id}.tsx b/e2e/react-router/compiled-matcher/src/routes/b/user-{$id}.tsx
new file mode 100644
index 00000000000..afd075f3501
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/b/user-{$id}.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/b/user-{$id}')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return {'Hello "/b/user-{$id}"!'}
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/b/{-$slug}.tsx b/e2e/react-router/compiled-matcher/src/routes/b/{-$slug}.tsx
new file mode 100644
index 00000000000..d03dcbe5371
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/b/{-$slug}.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/b/{-$slug}')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return {'Hello "/b/{-$slug}"!'}
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/beep/boop.tsx b/e2e/react-router/compiled-matcher/src/routes/beep/boop.tsx
new file mode 100644
index 00000000000..2d0115db443
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/beep/boop.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/beep/boop')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/beep/boop"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/cache/temp_{$}.log.tsx b/e2e/react-router/compiled-matcher/src/routes/cache/temp_{$}.log.tsx
new file mode 100644
index 00000000000..2c0abaaf804
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/cache/temp_{$}.log.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/cache/temp_{$}/log')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return {'Hello "/cache/temp_{$}.log"!'}
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/files/$.tsx b/e2e/react-router/compiled-matcher/src/routes/files/$.tsx
new file mode 100644
index 00000000000..0c4ab5c1a07
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/files/$.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/files/$')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/files/$"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/foo/$bar.index.tsx b/e2e/react-router/compiled-matcher/src/routes/foo/$bar.index.tsx
new file mode 100644
index 00000000000..a55255222b4
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/foo/$bar.index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/foo/$bar/')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/foo/$bar/"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/foo/$bar.tsx b/e2e/react-router/compiled-matcher/src/routes/foo/$bar.tsx
new file mode 100644
index 00000000000..b83a197ac7c
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/foo/$bar.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/foo/$bar')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/foo/$bar"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/foo/$id/bar.tsx b/e2e/react-router/compiled-matcher/src/routes/foo/$id/bar.tsx
new file mode 100644
index 00000000000..025eb7a2bf6
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/foo/$id/bar.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/foo/$id/bar')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/foo/$id/bar"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/foo/bar/$id.tsx b/e2e/react-router/compiled-matcher/src/routes/foo/bar/$id.tsx
new file mode 100644
index 00000000000..e06fb3ff6b2
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/foo/bar/$id.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/foo/bar/$id')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/foo/bar/$id"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/foo/{-$bar}/qux.tsx b/e2e/react-router/compiled-matcher/src/routes/foo/{-$bar}/qux.tsx
new file mode 100644
index 00000000000..37af5c50756
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/foo/{-$bar}/qux.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/foo/{-$bar}/qux')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return {'Hello "/foo/{-$bar}/qux"!'}
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/images/thumb_{$}.tsx b/e2e/react-router/compiled-matcher/src/routes/images/thumb_{$}.tsx
new file mode 100644
index 00000000000..e81725cb16f
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/images/thumb_{$}.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/images/thumb_{$}')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return {'Hello "/images/thumb_{$}"!'}
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/index.tsx b/e2e/react-router/compiled-matcher/src/routes/index.tsx
new file mode 100644
index 00000000000..d58928d9ed0
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/logs/{$}.txt.tsx b/e2e/react-router/compiled-matcher/src/routes/logs/{$}.txt.tsx
new file mode 100644
index 00000000000..456bceb9483
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/logs/{$}.txt.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/logs/{$}/txt')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return {'Hello "/logs/{$}.txt"!'}
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/one.tsx b/e2e/react-router/compiled-matcher/src/routes/one.tsx
new file mode 100644
index 00000000000..8334d30aa20
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/one.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/one')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/one"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/one/two.tsx b/e2e/react-router/compiled-matcher/src/routes/one/two.tsx
new file mode 100644
index 00000000000..8fe6c5615bb
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/one/two.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/one/two')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/one/two"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/posts/{-$slug}.tsx b/e2e/react-router/compiled-matcher/src/routes/posts/{-$slug}.tsx
new file mode 100644
index 00000000000..c494586b528
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/posts/{-$slug}.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/posts/{-$slug}')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return {'Hello "/posts/{-$slug}"!'}
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/users/$id.tsx b/e2e/react-router/compiled-matcher/src/routes/users/$id.tsx
new file mode 100644
index 00000000000..99635ef89a8
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/users/$id.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/users/$id')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/users/$id"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/users/profile/index.tsx b/e2e/react-router/compiled-matcher/src/routes/users/profile/index.tsx
new file mode 100644
index 00000000000..b042f0e5c94
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/users/profile/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/users/profile/')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/users/profile/"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/users/profile/settings.tsx b/e2e/react-router/compiled-matcher/src/routes/users/profile/settings.tsx
new file mode 100644
index 00000000000..d0eece68ae4
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/users/profile/settings.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/users/profile/settings')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/users/profile/settings"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/z/y/x/index.tsx b/e2e/react-router/compiled-matcher/src/routes/z/y/x/index.tsx
new file mode 100644
index 00000000000..fb49b8aeaeb
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/z/y/x/index.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/z/y/x/')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/z/y/x/"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/z/y/x/u.tsx b/e2e/react-router/compiled-matcher/src/routes/z/y/x/u.tsx
new file mode 100644
index 00000000000..20afb916ca7
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/z/y/x/u.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/z/y/x/u')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/z/y/x/u"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/z/y/x/v.tsx b/e2e/react-router/compiled-matcher/src/routes/z/y/x/v.tsx
new file mode 100644
index 00000000000..a19ed76a280
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/z/y/x/v.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/z/y/x/v')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/z/y/x/v"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/routes/z/y/x/w.tsx b/e2e/react-router/compiled-matcher/src/routes/z/y/x/w.tsx
new file mode 100644
index 00000000000..e6f02739452
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/routes/z/y/x/w.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/z/y/x/w')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return Hello "/z/y/x/w"!
+}
diff --git a/e2e/react-router/compiled-matcher/src/styles.css b/e2e/react-router/compiled-matcher/src/styles.css
new file mode 100644
index 00000000000..0b8e317099c
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/src/styles.css
@@ -0,0 +1,13 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+html {
+ color-scheme: light dark;
+}
+* {
+ @apply border-gray-200 dark:border-gray-800;
+}
+body {
+ @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
+}
diff --git a/e2e/react-router/compiled-matcher/tailwind.config.mjs b/e2e/react-router/compiled-matcher/tailwind.config.mjs
new file mode 100644
index 00000000000..4986094b9d5
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/tailwind.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
+}
diff --git a/e2e/react-router/compiled-matcher/tests/app.spec.ts b/e2e/react-router/compiled-matcher/tests/app.spec.ts
new file mode 100644
index 00000000000..285474fb480
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/tests/app.spec.ts
@@ -0,0 +1,38 @@
+import { expect, test } from '@playwright/test'
+
+test.beforeEach(async ({ page }) => {
+ await page.goto('/')
+})
+
+test('exact path matching', async ({ page }) => {
+ const links = [
+ { url: '', route: '/' },
+ { url: '/', route: '/' },
+ { url: '/users/profile/settings', route: '/users/profile/settings' },
+ // { url: '/foo/123', route: '/foo/$bar/' },
+ // { url: '/FOO/123', route: '/foo/$bar/' },
+ // { url: '/foo/123/', route: '/foo/$bar/' },
+ { url: '/b/123', route: '/b/$id' },
+ { url: '/foo/qux', route: '/foo/{-$bar}/qux' },
+ { url: '/foo/123/qux', route: '/foo/{-$bar}/qux' },
+ { url: '/a/user-123', route: '/a/user-{$id}' },
+ { url: '/a/123', route: '/a/$id' },
+ { url: '/a/123/more', route: '/a/$' },
+ { url: '/files', route: '/files/$' },
+ { url: '/files/hello-world.txt', route: '/files/$' },
+ { url: '/something/foo/bar', route: '/$id/foo/bar' },
+ { url: '/files/deep/nested/file.json', route: '/files/$' },
+ { url: '/files/', route: '/files/$' },
+ { url: '/images/thumb_200x300.jpg', route: '/images/thumb_{$}' },
+ { url: '/logs/2020/01/01/error.txt', route: '/logs/{$}.txt' },
+ { url: '/cache/temp_user456.log', route: '/cache/temp_{$}.log' },
+ { url: '/a/b/c/d/e', route: '/a/$' },
+ ]
+ for (const link of links) {
+ await test.step(`nav to '${link.url}'`, async () => {
+ console.log(`nav to '${link.url}'`)
+ await page.goto(link.url)
+ await expect(page.getByText(`Hello "${link.route}"!`)).toBeVisible()
+ })
+ }
+})
diff --git a/e2e/react-router/compiled-matcher/tests/setup/global.setup.ts b/e2e/react-router/compiled-matcher/tests/setup/global.setup.ts
new file mode 100644
index 00000000000..3593d10ab90
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/tests/setup/global.setup.ts
@@ -0,0 +1,6 @@
+import { e2eStartDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function setup() {
+ await e2eStartDummyServer(packageJson.name)
+}
diff --git a/e2e/react-router/compiled-matcher/tests/setup/global.teardown.ts b/e2e/react-router/compiled-matcher/tests/setup/global.teardown.ts
new file mode 100644
index 00000000000..62fd79911cc
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/tests/setup/global.teardown.ts
@@ -0,0 +1,6 @@
+import { e2eStopDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function teardown() {
+ await e2eStopDummyServer(packageJson.name)
+}
diff --git a/e2e/react-router/compiled-matcher/tsconfig.json b/e2e/react-router/compiled-matcher/tsconfig.json
new file mode 100644
index 00000000000..82cf0bcd2c9
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "target": "ESNext",
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "allowJs": true,
+ "types": ["vite/client"]
+ },
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/e2e/react-router/compiled-matcher/vite.config.js b/e2e/react-router/compiled-matcher/vite.config.js
new file mode 100644
index 00000000000..ab615485ae6
--- /dev/null
+++ b/e2e/react-router/compiled-matcher/vite.config.js
@@ -0,0 +1,11 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import { tanstackRouter } from '@tanstack/router-plugin/vite'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [
+ tanstackRouter({ target: 'react', autoCodeSplitting: true }),
+ react(),
+ ],
+})
diff --git a/packages/router-core/package.json b/packages/router-core/package.json
index c1ca570c79b..f827a08d251 100644
--- a/packages/router-core/package.json
+++ b/packages/router-core/package.json
@@ -30,6 +30,7 @@
"test:types:ts59": "tsc",
"test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
"test:unit": "vitest",
+ "test:perf": "vitest bench",
"test:unit:dev": "pnpm run test:unit --watch",
"build": "vite build"
},
diff --git a/packages/router-core/src/compile-matcher.ts b/packages/router-core/src/compile-matcher.ts
new file mode 100644
index 00000000000..9d2d4d58e62
--- /dev/null
+++ b/packages/router-core/src/compile-matcher.ts
@@ -0,0 +1,721 @@
+import {
+ SEGMENT_TYPE_OPTIONAL_PARAM,
+ SEGMENT_TYPE_PARAM,
+ SEGMENT_TYPE_PATHNAME,
+ SEGMENT_TYPE_WILDCARD,
+ parsePathname,
+} from './path'
+import type { LRUCache } from './lru-cache'
+import type { Segment } from './path'
+import type { processRouteTree } from './router'
+
+export type CompiledMatcher = (
+ parser: typeof parsePathname,
+ from: string,
+ fuzzy?: boolean,
+ cache?: LRUCache>,
+) => readonly [path: string, params: Record] | undefined
+
+/**
+ * Compiles a sorted list of routes (as returned by `processRouteTree().flatRoutes`)
+ * into a matcher function.
+ *
+ * Run-time use (requires eval permissions):
+ * ```ts
+ * const fn = compileMatcher(processRouteTree({ routeTree }).flatRoutes)
+ * const matcher = new Function('parsePathname', 'from', 'fuzzy', 'cache', fn) as CompiledMatcher
+ * ```
+ *
+ * Build-time use:
+ * ```ts
+ * const fn = compileMatcher(processRouteTree({ routeTree }).flatRoutes)
+ * sourceCode += `const matcher = (parsePathname, from, fuzzy, cache) => { ${fn} }`
+ * ```
+ */
+export function compileMatcher(
+ flatRoutes: ReturnType['flatRoutes'],
+) {
+ const parsedRoutes = flatRoutes.map((route) => ({
+ path: route.fullPath,
+ segments: parsePathname(route.fullPath),
+ }))
+
+ const all = toConditions(
+ prepareOptionalParams(prepareIndexRoutes(parsedRoutes)),
+ )
+
+ // We start by building a flat tree with all routes as leaf nodes, children of the same root node.
+ const tree: RootNode = { type: 'root', children: [] }
+
+ const children: Array = []
+ for (const { conditions, path, segments } of all) {
+ children.push({
+ type: 'leaf',
+ route: { path, segments },
+ parent: tree,
+ conditions,
+ })
+ }
+ tree.children = removeUnreachable(children)
+
+ expandTree(tree)
+ contractTree(tree)
+
+ let fn = ''
+ fn += printHead(all)
+ fn += printTree(tree)
+
+ return fn
+}
+
+type ParsedRoute = {
+ path: string
+ segments: ReturnType
+}
+
+// we duplicate routes that end in a static `/`, so they're also matched if that final `/` is not present
+function prepareIndexRoutes(
+ parsedRoutes: Array,
+): Array {
+ const result: Array = []
+ for (const route of parsedRoutes) {
+ result.push(route)
+ const last = route.segments.at(-1)!
+ if (
+ route.segments.length > 1 &&
+ last.type === SEGMENT_TYPE_PATHNAME &&
+ last.value === '/'
+ ) {
+ const clone: ParsedRoute = {
+ ...route,
+ segments: route.segments.slice(0, -1),
+ }
+ result.push(clone)
+ }
+ }
+ return result
+}
+
+// we replace routes w/ optional params, with
+// - 1 version where it's a regular param
+// - 1 version where it's removed entirely
+function prepareOptionalParams(
+ parsedRoutes: Array,
+): Array {
+ const result: Array = []
+ for (const route of parsedRoutes) {
+ const index = route.segments.findIndex(
+ (s) => s.type === SEGMENT_TYPE_OPTIONAL_PARAM,
+ )
+ if (index === -1) {
+ result.push(route)
+ continue
+ }
+ // for every optional param in the route, we need to push a version of the route without it, and a version of the route with it as a regular param
+ // example:
+ // /foo/{-$bar}/qux => [/foo/qux, /foo/$bar/qux]
+ // /a/{-$b}/c/{-$d} => [/a/c, /a/c/$d, /a/$b/c, /a/$b/c/$d]
+ const withRegular: ParsedRoute = {
+ ...route,
+ segments: route.segments.map((s, i) =>
+ i === index ? { ...s, type: SEGMENT_TYPE_PARAM } : s,
+ ),
+ }
+ const withoutOptional: ParsedRoute = {
+ ...route,
+ segments: route.segments.filter((_, i) => i !== index),
+ }
+ const chunk = prepareOptionalParams([withRegular, withoutOptional])
+ result.push(...chunk)
+ }
+ return result
+}
+
+type Condition =
+ | { key: string; type: 'static'; index: number; value: string; caseSensitive: boolean }
+ | { key: string; type: 'length'; direction: 'eq' | 'gte'; value: number }
+ | { key: string; type: 'startsWith'; index: number; value: string; caseSensitive: boolean }
+ | { key: string; type: 'endsWith'; index: number; value: string; caseSensitive: boolean }
+ | { key: string; type: 'globalEndsWith'; value: string; caseSensitive: boolean }
+
+// each segment of a route can have zero or more conditions that need to be met for the route to match
+function toConditions(routes: Array) {
+ return routes.map((route) => {
+ const conditions: Array = []
+
+ let hasWildcard = false
+ let minLength = 0
+ for (let i = 0; i < route.segments.length; i++) {
+ const segment = route.segments[i]!
+ if (segment.type === SEGMENT_TYPE_PATHNAME) {
+ minLength += 1
+ if (i === 0 && segment.value === '/') continue // skip leading slash
+ // @ts-expect-error -- not typed yet, i don't know how I'm gonna get this value here
+ const caseSensitive = route.caseSensitive ?? false
+ const value = caseSensitive ? segment.value : segment.value.toLowerCase()
+ conditions.push({
+ type: 'static',
+ index: i,
+ value,
+ caseSensitive,
+ key: `static_${caseSensitive}_${i}_${value}`,
+ })
+ continue
+ }
+ if (segment.type === SEGMENT_TYPE_PARAM) {
+ minLength += 1
+ // @ts-expect-error -- not typed yet, i don't know how I'm gonna get this value here
+ const caseSensitive = route.caseSensitive ?? false
+ if (segment.prefixSegment) {
+ const value = caseSensitive ? segment.prefixSegment : segment.prefixSegment.toLowerCase()
+ conditions.push({
+ type: 'startsWith',
+ index: i,
+ value,
+ caseSensitive,
+ key: `startsWith_${caseSensitive}_${i}_${value}`,
+ })
+ }
+ if (segment.suffixSegment) {
+ const value = caseSensitive ? segment.suffixSegment : segment.suffixSegment.toLowerCase()
+ conditions.push({
+ type: 'endsWith',
+ index: i,
+ value,
+ caseSensitive,
+ key: `endsWith_${caseSensitive}_${i}_${value}`,
+ })
+ }
+ continue
+ }
+ if (segment.type === SEGMENT_TYPE_WILDCARD) {
+ hasWildcard = true
+ // @ts-expect-error -- not typed yet, i don't know how I'm gonna get this value here
+ const caseSensitive = route.caseSensitive ?? false
+ if (segment.prefixSegment) {
+ const value = caseSensitive ? segment.prefixSegment : segment.prefixSegment.toLowerCase()
+ conditions.push({
+ type: 'startsWith',
+ index: i,
+ value,
+ caseSensitive,
+ key: `startsWith_${caseSensitive}_${i}_${value}`,
+ })
+ }
+ if (segment.suffixSegment) {
+ const value = caseSensitive ? segment.suffixSegment : segment.suffixSegment.toLowerCase()
+ conditions.push({
+ type: 'globalEndsWith',
+ value,
+ caseSensitive,
+ key: `globalEndsWith_${caseSensitive}_${i}_${value}`,
+ })
+ }
+ if (segment.suffixSegment || segment.prefixSegment) {
+ minLength += 1
+ }
+ continue
+ }
+ throw new Error(`Unhandled segment type: ${segment.type}`)
+ }
+
+ if (hasWildcard) {
+ conditions.push({
+ type: 'length',
+ direction: 'gte',
+ value: minLength,
+ key: `length_gte_${minLength}`,
+ })
+ } else {
+ conditions.push({
+ type: 'length',
+ direction: 'eq',
+ value: minLength,
+ key: `length_eq_${minLength}`,
+ })
+ }
+
+ return {
+ ...route,
+ conditions,
+ }
+ })
+}
+
+type LeafNode = {
+ type: 'leaf'
+ conditions: Array
+ route: ParsedRoute
+ parent: BranchNode | RootNode
+}
+type RootNode = { type: 'root'; children: Array }
+type BranchNode = {
+ type: 'branch'
+ conditions: Array
+ children: Array
+ parent: BranchNode | RootNode
+}
+
+/**
+ * Recursively expand each node of the tree until there is only one child left
+ *
+ * For each child node in a parent node, we try to find subsequent siblings that would share the same condition to be matched.
+ * If we find any, we group them together into a new branch node that replaces the original child node and the grouped siblings in the parent node.
+ *
+ * We repeat the process in each newly created branch node until there is only one child left in each branch node.
+ *
+ * This turns
+ * ```
+ * if (a && b && c && d) return route1;
+ * if (a && b && e && f) return route2;
+ * ```
+ * into
+ * ```
+ * if (a && b) {
+ * if (c) { if (d) return route1; }
+ * if (e) { if (f) return route2; }
+ * }
+ * ```
+ *
+ */
+function expandTree(tree: RootNode) {
+ const stack: Array = [tree]
+ while (stack.length > 0) {
+ const node = stack.shift()!
+ if (node.children.length <= 1) continue
+
+ const resolved = new Set()
+ for (let i = 0; i < node.children.length; i++) {
+ const child = node.children[i]!
+ if (resolved.has(child)) continue
+
+ // segment-based conditions should try to group as many children as possible
+ const bestSegment = findBestSegmentCondition(
+ node,
+ i,
+ node.children.length - i - 1,
+ )
+ // length-based conditions should try to group as few children as possible
+ const bestLength = findBestLengthCondition(node, i, 0)
+
+ if (bestSegment.score === Infinity && bestLength.score === Infinity) {
+ // no grouping possible, just add the child as is
+ resolved.add(child)
+ continue
+ }
+
+ const selected =
+ bestSegment.score < bestLength.score ? bestSegment : bestLength
+ const condition = selected.condition!
+ const newNode: BranchNode = {
+ type: 'branch',
+ conditions: [condition],
+ children: selected.candidates,
+ parent: node,
+ }
+ node.children.splice(i, selected.candidates.length, newNode)
+ stack.push(newNode)
+ resolved.add(newNode)
+ for (const c of selected.candidates) {
+ c.conditions = c.conditions.filter((sc) => sc.key !== condition.key)
+ }
+
+ // find all conditions that are shared by all candidates, and lift them to the new node
+ for (const condition of newNode.children[0]!.conditions) {
+ if (
+ newNode.children.every((c) =>
+ c.conditions.some((sc) => sc.key === condition.key),
+ )
+ ) {
+ newNode.conditions.push(condition)
+ }
+ }
+ for (let i = 1; i < newNode.conditions.length; i++) {
+ const condition = newNode.conditions[i]!
+ for (const c of newNode.children) {
+ c.conditions = c.conditions.filter((sc) => sc.key !== condition.key)
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Recursively shorten branches that have a single child into a leaf node.
+ *
+ * For each branch node in the tree, if it has only one child, we can replace the branch node with that child node,
+ * and merge the conditions of the branch node into the child node.
+ *
+ * This turns
+ * `if (a) { if (b) { return route } }`
+ * into
+ * `if (a && b) { return route }`
+ */
+function contractTree(tree: RootNode) {
+ const stack = tree.children.filter((c) => c.type === 'branch')
+ while (stack.length > 0) {
+ const node = stack.pop()!
+ if (node.children.length === 1) {
+ const child = node.children[0]!
+ node.parent.children.splice(node.parent.children.indexOf(node), 1, child)
+ child.parent = node.parent
+ child.conditions = [...node.conditions, ...child.conditions]
+
+ // reduce length-based conditions into a single condition
+ const lengthConditions = child.conditions.filter(
+ (c) => c.type === 'length',
+ )
+ if (lengthConditions.some((c) => c.direction === 'eq')) {
+ for (const c of lengthConditions) {
+ if (c.direction === 'gte') {
+ child.conditions = child.conditions.filter((sc) => sc.key !== c.key)
+ }
+ }
+ } else if (lengthConditions.length > 0) {
+ const minLength = Math.min(...lengthConditions.map((c) => c.value))
+ child.conditions = child.conditions.filter((c) => c.type !== 'length')
+ child.conditions.push({
+ type: 'length',
+ direction: 'eq',
+ value: minLength,
+ key: `length_eq_${minLength}`,
+ })
+ }
+ }
+ for (const child of node.children) {
+ if (child.type === 'branch') {
+ stack.push(child)
+ }
+ }
+ }
+}
+
+/**
+ * Remove leaves that are not reachable due to the conditions a previous leaf sibling.
+ *
+ * This turns
+ * ```
+ * if (a) return route1;
+ * if (a && b) return route2;
+ * ```
+ * into
+ * ```
+ * if (a) return route1;
+ * ```
+ */
+function removeUnreachable(nodes: Array) {
+ loop: for (let i = 0; i < nodes.length; i++) {
+ const candidate = nodes[i]!
+
+ // look through all previous siblings
+ for (let j = 0; j < i; j++) {
+ const sibling = nodes[j]!
+ // if every condition the sibling requires is also present in the candidate,
+ // then that means the candidate is unreachable
+ const candidateIsUnreachable = sibling.conditions.every((c) => {
+ if (c.type === 'length' && c.direction === 'gte') {
+ return candidate.conditions.some((sc) => sc.key === c.key || (sc.type === 'length' && sc.direction === 'eq' && sc.value >= c.value))
+ }
+ // TODO: we could add other "covering" cases like the one above here,
+ // such as the sibling having a `startsWith` condition and the candidate having a static condition that starts with the same value (taking case sensitivity into account)
+ return candidate.conditions.some((sc) => sc.key === c.key)
+ })
+ if (candidateIsUnreachable) {
+ nodes.splice(i, 1)
+ i -= 1
+ continue loop
+ }
+ }
+ }
+ return nodes
+}
+
+function printTree(node: RootNode | BranchNode | LeafNode) {
+ let str = ''
+ if (node.type === 'root') {
+ for (const child of node.children) {
+ str += printTree(child)
+ }
+ return str
+ }
+ if (node.conditions.length) {
+ str += 'if ('
+ str += printConditions(node)
+ str += ')'
+ }
+ if (node.type === 'branch') {
+ if (node.conditions.length && node.children.length) str += `{`
+ for (const child of node.children) {
+ str += printTree(child)
+ }
+ if (node.conditions.length && node.children.length) str += `}`
+ } else {
+ str += printRoute(node.route)
+ }
+ return str
+}
+
+function printConditions(node: BranchNode | LeafNode) {
+ const conditions = node.conditions
+ const lengths = conditions.filter((c) => c.type === 'length')
+ const segment = conditions.filter((c) => c.type !== 'length')
+ const results: Array = []
+ if (lengths.length > 1) {
+ const exact = lengths.find((c) => c.direction === 'eq')
+ if (exact) {
+ results.push(printCondition(exact))
+ } else {
+ results.push(printCondition(lengths[0]!))
+ }
+ } else if (lengths.length === 1) {
+ results.push(printCondition(lengths[0]!))
+ }
+ const [minLength] = findLengthAtNode(node)
+ for (const c of segment) {
+ results.push(printCondition(c, minLength))
+ }
+ return results.join(' && ')
+}
+
+function printCondition(condition: Condition, minLength: number = 0) {
+ switch (condition.type) {
+ case 'static':
+ if (condition.caseSensitive) {
+ return `s${condition.index} === '${condition.value}'`
+ } else {
+ return `sc${condition.index} === '${condition.value}'`
+ }
+ case 'length':
+ if (condition.direction === 'eq') {
+ return `length(${condition.value})`
+ } else if (condition.direction === 'gte') {
+ return `l >= ${condition.value}`
+ }
+ break
+ case 'startsWith':
+ if (condition.caseSensitive) {
+ return `s${condition.index}.startsWith('${condition.value}')`
+ } else if (minLength > condition.index) {
+ return `sc${condition.index}.startsWith('${condition.value}')`
+ } else {
+ return `sc${condition.index}?.startsWith('${condition.value}')`
+ }
+ case 'endsWith':
+ if (condition.caseSensitive) {
+ return `s${condition.index}.endsWith('${condition.value}')`
+ } else if (minLength > condition.index) {
+ return `sc${condition.index}.endsWith('${condition.value}')`
+ } else {
+ return `sc${condition.index}?.endsWith('${condition.value}')`
+ }
+ case 'globalEndsWith':
+ if (condition.caseSensitive) {
+ return `last.endsWith('${condition.value}')`
+ } else {
+ return `last.toLowerCase().endsWith('${condition.value}')`
+ }
+ }
+ throw new Error(`Unhandled condition type: ${condition.type}`)
+}
+
+function printRoute(route: ParsedRoute) {
+ const length = route.segments.length
+ /**
+ * return [
+ * route.path,
+ * { foo: s2, bar: s4 }
+ * ]
+ */
+ let result = `{`
+ let hasWildcard = false
+ for (let i = 0; i < route.segments.length; i++) {
+ const segment = route.segments[i]!
+ if (segment.type === SEGMENT_TYPE_PARAM) {
+ const name = segment.value.replace(/^\$/, '')
+ const value = `s${i}`
+ if (segment.prefixSegment && segment.suffixSegment) {
+ result += `${name}: ${value}.slice(${segment.prefixSegment.length}, -${segment.suffixSegment.length}), `
+ } else if (segment.prefixSegment) {
+ result += `${name}: ${value}.slice(${segment.prefixSegment.length}), `
+ } else if (segment.suffixSegment) {
+ result += `${name}: ${value}.slice(0, -${segment.suffixSegment.length}), `
+ } else {
+ result += `${name}: ${value}, `
+ }
+ } else if (segment.type === SEGMENT_TYPE_WILDCARD) {
+ hasWildcard = true
+ const value = `s.slice(${i}).join('/')`
+ if (segment.prefixSegment && segment.suffixSegment) {
+ result += `_splat: ${value}.slice(${segment.prefixSegment.length}, -${segment.suffixSegment.length}), `
+ result += `'*': ${value}.slice(${segment.prefixSegment.length}, -${segment.suffixSegment.length}), `
+ } else if (segment.prefixSegment) {
+ result += `_splat: ${value}.slice(${segment.prefixSegment.length}), `
+ result += `'*': ${value}.slice(${segment.prefixSegment.length}), `
+ } else if (segment.suffixSegment) {
+ result += `_splat: ${value}.slice(0, -${segment.suffixSegment.length}), `
+ result += `'*': ${value}.slice(0, -${segment.suffixSegment.length}), `
+ } else {
+ result += `_splat: ${value}, `
+ result += `'*': ${value}, `
+ }
+ break
+ }
+ }
+ result += `}`
+ return hasWildcard
+ ? `return ['${route.path}', ${result}];`
+ : `return ['${route.path}', params(${result}, ${length})];`
+}
+
+function printHead(
+ routes: Array }>,
+) {
+ let head =
+ 'const s = parsePathname(from[0] === "/" ? from : "/" + from, cache).map((s) => s.value);'
+ head += 'const l = s.length;'
+
+ // the `length()` function does exact match by default, but greater-than-or-equal match if `fuzzy` is true
+ head += 'const length = fuzzy ? (n) => l >= n : (n) => l === n;'
+
+ // the `params()` function returns the params object, and if `fuzzy` is true, it also adds a `**` property with the remaining segments
+ head +=
+ "const params = fuzzy ? (p, n) => { if (n && l > n) p['**'] = s.slice(n).join('/'); return p } : (p) => p;"
+
+ // extract all segments from the input
+ // const [, s1, s2, s3] = s;
+ const max = routes.reduce((max, r) => Math.max(max, r.segments.length), 0)
+ if (max > 0)
+ head += `const [,${Array.from({ length: max - 1 }, (_, i) => `s${i + 1}`).join(', ')}] = s;`
+
+ // add toLowerCase version of each segment that is needed in a case-insensitive match
+ // const sc1 = s1?.toLowerCase();
+ const caseInsensitiveSegments = new Set()
+ for (const route of routes) {
+ for (const condition of route.conditions) {
+ if ((condition.type === 'static' || condition.type === 'endsWith' || condition.type === 'startsWith') && !condition.caseSensitive) {
+ caseInsensitiveSegments.add(condition.index)
+ }
+ }
+ }
+ for (const index of caseInsensitiveSegments) {
+ head += `const sc${index} = s${index}?.toLowerCase();`
+ }
+
+ // wildcard with a suffix requires accessing the last segment, whithout knowing its index
+ const hasWildcardWithSuffix = routes.some((route) => route.conditions.some((c) => c.type === 'globalEndsWith'))
+ if (hasWildcardWithSuffix) {
+ head += 'const last = s[l - 1];'
+ }
+
+ return head
+}
+
+function findBestSegmentCondition(
+ node: RootNode | BranchNode,
+ i: number,
+ target: number,
+) {
+ const child = node.children[i]!
+ let bestCondition: Condition | undefined
+ let bestMatchScore = Infinity
+ let bestCandidates = [child]
+ for (const c of child.conditions) {
+ const candidates = [child]
+ for (let j = i + 1; j < node.children.length; j++) {
+ const sibling = node.children[j]!
+ if (sibling.conditions.some((sc) => sc.key === c.key)) {
+ candidates.push(sibling)
+ } else {
+ break
+ }
+ }
+ const score = Math.abs(candidates.length - target)
+ if (score < bestMatchScore) {
+ bestMatchScore = score
+ bestCondition = c
+ bestCandidates = candidates
+ }
+ }
+
+ return {
+ score: bestMatchScore,
+ condition: bestCondition,
+ candidates: bestCandidates,
+ }
+}
+
+function findBestLengthCondition(
+ node: RootNode | BranchNode,
+ i: number,
+ target: number,
+) {
+ const child = node.children[i]!
+ const childLengthCondition = child.conditions.find((c) => c.type === 'length')
+ if (!childLengthCondition) {
+ return { score: Infinity, condition: undefined, candidates: [child] }
+ }
+ const [currentMinLength, lengthKind] = findLengthAtNode(node)
+ if (lengthKind === 'exact' || currentMinLength >= childLengthCondition.value) {
+ return { score: Infinity, condition: undefined, candidates: [child] }
+ }
+ let bestMatchScore = Infinity
+ let bestLength: number | undefined
+ let bestCandidates = [child]
+ let bestExact = false
+ for (let l = currentMinLength + 1; l <= childLengthCondition.value; l++) {
+ const candidates = [child]
+ let exact =
+ childLengthCondition.direction === 'eq' &&
+ l === childLengthCondition.value
+ for (let j = i + 1; j < node.children.length; j++) {
+ const sibling = node.children[j]!
+ const lengthCondition = sibling.conditions.find(
+ (c) => c.type === 'length',
+ )
+ if (!lengthCondition) break
+ if (lengthCondition.value < l) break
+ candidates.push(sibling)
+ exact &&=
+ lengthCondition.direction === 'eq' && lengthCondition.value === l
+ }
+ const score = Math.abs(candidates.length - target)
+ if (score < bestMatchScore) {
+ bestMatchScore = score
+ bestLength = l
+ bestCandidates = candidates
+ bestExact = exact
+ }
+ }
+ const condition: Condition = {
+ type: 'length',
+ direction: bestExact ? 'eq' : 'gte',
+ value: bestLength!,
+ key: `length_${bestExact ? 'eq' : 'gte'}_${bestLength}`,
+ }
+ return { score: bestMatchScore, condition, candidates: bestCandidates }
+}
+
+function findLengthAtNode(
+ node: RootNode | BranchNode | LeafNode,
+) {
+ if (node.type === 'root') return [1, 'min'] as const
+ let currentMinLength = 1
+ let exactLength = false
+ let n = node
+ do {
+ const lengthCondition = n.conditions.find((c) => c.type === 'length')
+ if (!lengthCondition) continue
+ if (lengthCondition.direction === 'eq') {
+ exactLength = true
+ break
+ }
+ if (lengthCondition.direction === 'gte') {
+ currentMinLength = lengthCondition.value
+ break
+ }
+ } while (n.parent.type === 'branch' && (n = n.parent))
+ return [
+ currentMinLength,
+ exactLength ? 'exact' : 'min',
+ ] as const
+}
\ No newline at end of file
diff --git a/packages/router-core/src/index.ts b/packages/router-core/src/index.ts
index ca34da17222..050af54a253 100644
--- a/packages/router-core/src/index.ts
+++ b/packages/router-core/src/index.ts
@@ -107,7 +107,12 @@ export {
matchByPath,
} from './path'
export type { Segment } from './path'
+
+export { compileMatcher } from './compile-matcher'
+export type { CompiledMatcher } from './compile-matcher'
+
export { encode, decode } from './qss'
+
export { rootRouteId } from './root'
export type { RootRouteId } from './root'
diff --git a/packages/router-core/tests/compile-matcher.bench.ts b/packages/router-core/tests/compile-matcher.bench.ts
new file mode 100644
index 00000000000..b80a194cc43
--- /dev/null
+++ b/packages/router-core/tests/compile-matcher.bench.ts
@@ -0,0 +1,221 @@
+import { bench, describe } from 'vitest'
+import {
+ joinPaths,
+ matchPathname,
+ parsePathname,
+ processRouteTree,
+} from '../src'
+import { createLRUCache } from '../src/lru-cache'
+import { compileMatcher } from '../src/compile-matcher'
+import type { CompiledMatcher } from '../src/compile-matcher'
+import type { ParsePathnameCache } from '../src/path'
+
+interface TestRoute {
+ id: string
+ isRoot?: boolean
+ path?: string
+ fullPath: string
+ rank?: number
+ parentRoute?: TestRoute
+ children?: Array
+ options?: {
+ caseSensitive?: boolean
+ }
+}
+
+type PathOrChildren = string | [string, Array]
+
+function createRoute(
+ pathOrChildren: Array,
+ parentPath: string,
+): Array {
+ return pathOrChildren.map((route) => {
+ if (Array.isArray(route)) {
+ const fullPath = joinPaths([parentPath, route[0]])
+ const children = createRoute(route[1], fullPath)
+ const r = {
+ id: fullPath,
+ path: route[0],
+ fullPath,
+ children: children,
+ }
+ children.forEach((child) => {
+ child.parentRoute = r
+ })
+
+ return r
+ }
+
+ const fullPath = joinPaths([parentPath, route])
+
+ return {
+ id: fullPath,
+ path: route,
+ fullPath,
+ }
+ })
+}
+
+function createRouteTree(pathOrChildren: Array): TestRoute {
+ return {
+ id: '__root__',
+ fullPath: '',
+ isRoot: true,
+ path: undefined,
+ children: createRoute(pathOrChildren, ''),
+ }
+}
+
+const routeTree = createRouteTree([
+ '/',
+ '/users/profile/settings', // static-deep (longest static path)
+ '/users/profile', // static-medium (medium static path)
+ '/api/user-{$id}', // param-with-prefix (param with prefix has higher score)
+ '/users/$id', // param-simple (plain param)
+ '/posts/{-$slug}', // optional-param (optional param ranks lower than regular param)
+ '/files/$', // wildcard (lowest priority)
+ '/about', // static-shallow (shorter static path)
+ '/a/profile/settings',
+ '/a/profile',
+ '/a/user-{$id}',
+ '/a/$id',
+ '/a/{-$slug}',
+ '/a/$',
+ '/a',
+ '/b/profile/settings',
+ '/b/profile',
+ '/b/user-{$id}',
+ '/b/$id',
+ '/b/{-$slug}',
+ '/b/$',
+ '/b',
+ '/foo/bar/$id',
+ '/foo/$id/bar',
+ '/foo/$bar',
+ '/foo/$bar/',
+ '/foo/{-$bar}/qux',
+ '/$id/bar/foo',
+ '/$id/foo/bar',
+ '/a/b/c/d/e/f',
+ '/beep/boop',
+ '/compiled/two',
+ '/compiled',
+ '/z/y/x/w',
+ '/z/y/x/v',
+ '/z/y/x/u',
+ '/z/y/x',
+ '/images/thumb_{$}', // wildcard with prefix
+ '/logs/{$}.txt', // wildcard with suffix
+ '/cache/temp_{$}.log', // wildcard with prefix and suffix
+ '/momomo/{-$one}/$two'
+])
+const result = processRouteTree({ routeTree })
+
+const compiled = (() => {
+ const cache: ParsePathnameCache = createLRUCache(1000)
+ const fn = compileMatcher(result.flatRoutes)
+ const buildMatcher = new Function(
+ 'parsePathname',
+ 'from',
+ 'fuzzy',
+ 'cache',
+ fn,
+ ) as CompiledMatcher
+ const wrappedMatcher = (from: string) => {
+ return buildMatcher(parsePathname, from, false, cache)
+ }
+ return wrappedMatcher
+})()
+
+const original = (() => {
+ const cache: ParsePathnameCache = createLRUCache(1000)
+
+ const wrappedMatcher = (from: string) => {
+ const match = result.flatRoutes.find((r) =>
+ matchPathname('/', from, { to: r.fullPath }, cache),
+ )
+ return match
+ }
+ return wrappedMatcher
+})()
+
+const testCases = [
+ '',
+ '/',
+ '/users/profile/settings',
+ '/foo/123',
+ '/foo/123/',
+ '/b/123',
+ '/foo/qux',
+ '/foo/123/qux',
+ '/foo/qux',
+ '/a/user-123',
+ '/a/123',
+ '/a/123/more',
+ '/files',
+ '/files/hello-world.txt',
+ '/something/foo/bar',
+ '/files/deep/nested/file.json',
+ '/files/',
+ '/images/thumb_200x300.jpg',
+ '/logs/error.txt',
+ '/cache/temp_user456.log',
+ '/a/b/c/d/e',
+ '/momomo/1111/2222',
+ '/momomo/2222',
+]
+
+describe('build.bench needle in a haystack', () => {
+ bench(
+ 'original',
+ () => {
+ for (const from of testCases) {
+ original(from)
+ }
+ },
+ { warmupIterations: 10 },
+ )
+ bench(
+ 'compiled',
+ () => {
+ for (const from of testCases) {
+ compiled(from)
+ }
+ },
+ { warmupIterations: 10 },
+ )
+})
+
+/**
+ * Sometimes in the app, we already know the path we want to match against.
+ * The compiled matcher does not support this.
+ * This benchmark tests the performance of the compiled matcher looking through ALL routes
+ * vs. the original matcher comparing against a single path.
+ */
+describe('build.bench single match', () => {
+ const solutions = testCases.map((from) => original(from)?.fullPath)
+
+ const cache: ParsePathnameCache = createLRUCache(1000)
+ const originalSingle = (from: string, to: string) => matchPathname('/', from, { to }, cache)
+
+ bench(
+ 'original (single)',
+ () => {
+ for (let i = 0; i < testCases.length; i++) {
+ const from = testCases[i]!
+ const match = solutions[i]!
+ originalSingle(from, match)
+ }
+ },
+ { warmupIterations: 10 },
+ )
+ bench(
+ 'compiled',
+ () => {
+ for (const from of testCases) {
+ compiled(from)
+ }
+ },
+ { warmupIterations: 10 },
+ )
+})
diff --git a/packages/router-core/tests/compile-matcher.test.ts b/packages/router-core/tests/compile-matcher.test.ts
new file mode 100644
index 00000000000..ebb8de388da
--- /dev/null
+++ b/packages/router-core/tests/compile-matcher.test.ts
@@ -0,0 +1,391 @@
+import { describe, expect, it, test } from 'vitest'
+import { format } from 'prettier'
+import {
+ joinPaths,
+ matchPathname,
+ parsePathname,
+ processRouteTree,
+} from '../src'
+import { compileMatcher } from '../src/compile-matcher'
+import type { CompiledMatcher } from '../src/compile-matcher'
+
+interface TestRoute {
+ id: string
+ isRoot?: boolean
+ path?: string
+ fullPath: string
+ rank?: number
+ parentRoute?: TestRoute
+ children?: Array
+ options?: {
+ caseSensitive?: boolean
+ }
+}
+
+type PathOrChildren = string | [string, Array]
+
+function createRoute(
+ pathOrChildren: Array,
+ parentPath: string,
+): Array {
+ return pathOrChildren.map((route) => {
+ if (Array.isArray(route)) {
+ const fullPath = joinPaths([parentPath, route[0]])
+ const children = createRoute(route[1], fullPath)
+ const r = {
+ id: fullPath,
+ path: route[0],
+ fullPath,
+ children: children,
+ }
+ children.forEach((child) => {
+ child.parentRoute = r
+ })
+
+ return r
+ }
+
+ const fullPath = joinPaths([parentPath, route])
+
+ return {
+ id: fullPath,
+ path: route,
+ fullPath,
+ }
+ })
+}
+
+function createRouteTree(pathOrChildren: Array): TestRoute {
+ return {
+ id: '__root__',
+ fullPath: '',
+ isRoot: true,
+ path: undefined,
+ children: createRoute(pathOrChildren, ''),
+ }
+}
+
+const routeTree = createRouteTree([
+ '/',
+ '/users/profile/settings', // static-deep (longest static path)
+ '/users/profile', // static-medium (medium static path)
+ '/api/user-{$id}', // param-with-prefix (param with prefix has higher score)
+ '/users/$id', // param-simple (plain param)
+ '/posts/{-$slug}', // optional-param (optional param ranks lower than regular param)
+ '/files/$', // wildcard (lowest priority)
+ '/about', // static-shallow (shorter static path)
+ '/a/profile/settings',
+ '/a/profile',
+ '/a/user-{$id}',
+ '/a/$id',
+ '/a/{-$slug}',
+ '/a/$',
+ '/a',
+ '/b/profile/settings',
+ '/b/profile',
+ '/b/user-{$id}',
+ '/b/$id',
+ '/b/{-$slug}',
+ '/b/$',
+ '/b',
+ '/foo/bar/$id',
+ '/foo/$id/bar',
+ '/foo/$bar',
+ '/foo/$bar/',
+ '/foo/{-$bar}/qux',
+ '/$id/bar/foo',
+ '/$id/foo/bar',
+ '/a/b/c/d/e/f',
+ '/beep/boop',
+ '/one/two',
+ '/one',
+ '/z/y/x/w',
+ '/z/y/x/v',
+ '/z/y/x/u',
+ '/z/y/x',
+ '/images/thumb_{$}', // wildcard with prefix
+ '/logs/{$}.txt', // wildcard with suffix
+ '/cache/temp_{$}.log', // wildcard with prefix and suffix,
+ '/momomo/{-$one}/$two'
+])
+
+// required keys on a `route` object for `processRouteTree` to correctly generate `flatRoutes`
+// - id
+// - children
+// - isRoot
+// - path
+// - fullPath
+
+const result = processRouteTree({ routeTree })
+
+function originalMatcher(
+ from: string,
+ fuzzy?: boolean,
+): readonly [string, Record] | undefined {
+ let match
+ for (const route of result.flatRoutes) {
+ const result = matchPathname('/', from, { to: route.fullPath, fuzzy })
+ if (result) {
+ match = [route.fullPath, result] as const
+ break
+ }
+ }
+ return match
+}
+
+describe('work in progress', () => {
+ it('is ordered', () => {
+ expect(result.flatRoutes.map((r) => r.id)).toMatchInlineSnapshot(`
+ [
+ "/a/b/c/d/e/f",
+ "/z/y/x/u",
+ "/z/y/x/v",
+ "/z/y/x/w",
+ "/a/profile/settings",
+ "/b/profile/settings",
+ "/users/profile/settings",
+ "/z/y/x",
+ "/foo/bar/$id",
+ "/a/profile",
+ "/b/profile",
+ "/beep/boop",
+ "/one/two",
+ "/users/profile",
+ "/foo/$id/bar",
+ "/foo/{-$bar}/qux",
+ "/a/user-{$id}",
+ "/api/user-{$id}",
+ "/b/user-{$id}",
+ "/foo/$bar/",
+ "/a/$id",
+ "/b/$id",
+ "/foo/$bar",
+ "/users/$id",
+ "/momomo/{-$one}/$two",
+ "/a/{-$slug}",
+ "/b/{-$slug}",
+ "/posts/{-$slug}",
+ "/cache/temp_{$}.log",
+ "/images/thumb_{$}",
+ "/logs/{$}.txt",
+ "/a/$",
+ "/b/$",
+ "/files/$",
+ "/a",
+ "/about",
+ "/b",
+ "/one",
+ "/",
+ "/$id/bar/foo",
+ "/$id/foo/bar",
+ ]
+ `)
+ })
+
+ const fn = compileMatcher(result.flatRoutes)
+
+ it('generates a matching function', async () => {
+ expect(await format(fn, { parser: 'typescript' })).toMatchInlineSnapshot(`
+ "const s = parsePathname(from[0] === "/" ? from : "/" + from, cache).map(
+ (s) => s.value,
+ );
+ const l = s.length;
+ const length = fuzzy ? (n) => l >= n : (n) => l === n;
+ const params = fuzzy
+ ? (p, n) => {
+ if (n && l > n) p["**"] = s.slice(n).join("/");
+ return p;
+ }
+ : (p) => p;
+ const [, s1, s2, s3, s4, s5, s6] = s;
+ const sc1 = s1?.toLowerCase();
+ const sc2 = s2?.toLowerCase();
+ const sc3 = s3?.toLowerCase();
+ const sc4 = s4?.toLowerCase();
+ const sc5 = s5?.toLowerCase();
+ const sc6 = s6?.toLowerCase();
+ const last = s[l - 1];
+ if (
+ length(7) &&
+ sc1 === "a" &&
+ sc2 === "b" &&
+ sc3 === "c" &&
+ sc4 === "d" &&
+ sc5 === "e" &&
+ sc6 === "f"
+ )
+ return ["/a/b/c/d/e/f", params({}, 7)];
+ if (length(5) && sc1 === "z" && sc2 === "y" && sc3 === "x") {
+ if (sc4 === "u") return ["/z/y/x/u", params({}, 5)];
+ if (sc4 === "v") return ["/z/y/x/v", params({}, 5)];
+ if (sc4 === "w") return ["/z/y/x/w", params({}, 5)];
+ }
+ if (length(4)) {
+ if (sc2 === "profile" && sc3 === "settings") {
+ if (sc1 === "a") return ["/a/profile/settings", params({}, 4)];
+ if (sc1 === "b") return ["/b/profile/settings", params({}, 4)];
+ if (sc1 === "users") return ["/users/profile/settings", params({}, 4)];
+ }
+ if (sc1 === "z" && sc2 === "y" && sc3 === "x")
+ return ["/z/y/x", params({}, 4)];
+ if (sc1 === "foo" && sc2 === "bar")
+ return ["/foo/bar/$id", params({ id: s3 }, 4)];
+ }
+ if (l >= 3) {
+ if (length(3)) {
+ if (sc2 === "profile") {
+ if (sc1 === "a") return ["/a/profile", params({}, 3)];
+ if (sc1 === "b") return ["/b/profile", params({}, 3)];
+ }
+ if (sc1 === "beep" && sc2 === "boop") return ["/beep/boop", params({}, 3)];
+ if (sc1 === "one" && sc2 === "two") return ["/one/two", params({}, 3)];
+ if (sc1 === "users" && sc2 === "profile")
+ return ["/users/profile", params({}, 3)];
+ }
+ if (length(4) && sc1 === "foo") {
+ if (sc3 === "bar") return ["/foo/$id/bar", params({ id: s2 }, 4)];
+ if (sc3 === "qux") return ["/foo/{-$bar}/qux", params({ bar: s2 }, 4)];
+ }
+ if (length(3)) {
+ if (sc1 === "foo" && sc2 === "qux")
+ return ["/foo/{-$bar}/qux", params({}, 3)];
+ if (sc1 === "a" && sc2?.startsWith("user-"))
+ return ["/a/user-{$id}", params({ id: s2.slice(5) }, 3)];
+ if (sc1 === "api" && sc2?.startsWith("user-"))
+ return ["/api/user-{$id}", params({ id: s2.slice(5) }, 3)];
+ if (sc1 === "b" && sc2?.startsWith("user-"))
+ return ["/b/user-{$id}", params({ id: s2.slice(5) }, 3)];
+ }
+ if (length(4) && sc1 === "foo" && sc3 === "/")
+ return ["/foo/$bar/", params({ bar: s2 }, 4)];
+ if (length(3)) {
+ if (sc1 === "foo") return ["/foo/$bar/", params({ bar: s2 }, 3)];
+ if (sc1 === "a") return ["/a/$id", params({ id: s2 }, 3)];
+ if (sc1 === "b") return ["/b/$id", params({ id: s2 }, 3)];
+ if (sc1 === "users") return ["/users/$id", params({ id: s2 }, 3)];
+ }
+ if (length(4) && sc1 === "momomo")
+ return ["/momomo/{-$one}/$two", params({ one: s2, two: s3 }, 4)];
+ if (length(3) && sc1 === "momomo")
+ return ["/momomo/{-$one}/$two", params({ two: s2 }, 3)];
+ }
+ if (l >= 2) {
+ if (length(2)) {
+ if (sc1 === "a") return ["/a/{-$slug}", params({}, 2)];
+ if (sc1 === "b") return ["/b/{-$slug}", params({}, 2)];
+ }
+ if (length(3) && sc1 === "posts")
+ return ["/posts/{-$slug}", params({ slug: s2 }, 3)];
+ if (length(2) && sc1 === "posts") return ["/posts/{-$slug}", params({}, 2)];
+ if (l >= 3) {
+ if (
+ sc1 === "cache" &&
+ sc2.startsWith("temp_") &&
+ last.toLowerCase().endsWith(".log")
+ )
+ return [
+ "/cache/temp_{$}.log",
+ {
+ _splat: s.slice(2).join("/").slice(5, -4),
+ "*": s.slice(2).join("/").slice(5, -4),
+ },
+ ];
+ if (sc1 === "images" && sc2.startsWith("thumb_"))
+ return [
+ "/images/thumb_{$}",
+ {
+ _splat: s.slice(2).join("/").slice(6),
+ "*": s.slice(2).join("/").slice(6),
+ },
+ ];
+ if (sc1 === "logs" && last.toLowerCase().endsWith(".txt"))
+ return [
+ "/logs/{$}.txt",
+ {
+ _splat: s.slice(2).join("/").slice(0, -4),
+ "*": s.slice(2).join("/").slice(0, -4),
+ },
+ ];
+ }
+ if (sc1 === "a")
+ return [
+ "/a/$",
+ { _splat: s.slice(2).join("/"), "*": s.slice(2).join("/") },
+ ];
+ if (sc1 === "b")
+ return [
+ "/b/$",
+ { _splat: s.slice(2).join("/"), "*": s.slice(2).join("/") },
+ ];
+ if (sc1 === "files")
+ return [
+ "/files/$",
+ { _splat: s.slice(2).join("/"), "*": s.slice(2).join("/") },
+ ];
+ if (length(2) && sc1 === "about") return ["/about", params({}, 2)];
+ if (length(2) && sc1 === "one") return ["/one", params({}, 2)];
+ }
+ if (length(1)) return ["/", params({}, 1)];
+ if (length(4) && sc2 === "bar" && sc3 === "foo")
+ return ["/$id/bar/foo", params({ id: s1 }, 4)];
+ if (length(4) && sc2 === "foo" && sc3 === "bar")
+ return ["/$id/foo/bar", params({ id: s1 }, 4)];
+ "
+ `)
+ })
+
+ const buildMatcher = new Function(
+ 'parsePathname',
+ 'from',
+ 'fuzzy',
+ 'cache',
+ fn,
+ ) as CompiledMatcher
+
+ test.each([
+ '',
+ '/',
+ '/users/profile/settings',
+ '/foo/123',
+ '/FOO/123',
+ '/foo/123/',
+ '/b/123',
+ '/foo/qux',
+ '/foo/123/qux',
+ '/a/user-123',
+ '/a/123',
+ '/a/123/more',
+ '/files',
+ '/files/hello-world.txt',
+ '/something/foo/bar',
+ '/files/deep/nested/file.json',
+ '/files/',
+ '/images/thumb_200x300.jpg',
+ '/logs/2020/01/01/error.txt',
+ '/cache/temp_user456.log',
+ '/a/b/c/d/e',
+ '/momomo/1111/2222',
+ '/momomo/2222',
+ ])('matching %s', (s) => {
+ const originalMatch = originalMatcher(s)
+ const buildMatch = buildMatcher(parsePathname, s)
+ console.log(
+ `matching: ${s}, originalMatch: ${originalMatch?.[0]}, buildMatch: ${buildMatch?.[0]}`,
+ )
+ expect(buildMatch).toEqual(originalMatch)
+ })
+
+ test.each([
+ '/users/profile/settings/hello',
+ '/a/b/c/d/e/f/g',
+ '/foo/bar/baz',
+ '/foo/bar/baz/qux',
+ ])('fuzzy matching %s', (s) => {
+ const originalMatch = originalMatcher(s, true)
+ const buildMatch = buildMatcher(parsePathname, s, true)
+ console.log(
+ `fuzzy matching: ${s}, originalMatch: ${originalMatch?.[0]}, buildMatch: ${buildMatch?.[0]} ${JSON.stringify(buildMatch?.[1])}`,
+ )
+ expect(buildMatch).toEqual(originalMatch)
+ })
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f72812ba0be..1b6d76b5812 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -626,6 +626,64 @@ importers:
specifier: 6.3.5
version: 6.3.5(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0)
+ e2e/react-router/compiled-matcher:
+ dependencies:
+ '@tanstack/react-router':
+ specifier: workspace:*
+ version: link:../../../packages/react-router
+ '@tanstack/react-router-devtools':
+ specifier: workspace:^
+ version: link:../../../packages/react-router-devtools
+ '@tanstack/router-plugin':
+ specifier: workspace:*
+ version: link:../../../packages/router-plugin
+ '@tanstack/zod-adapter':
+ specifier: workspace:*
+ version: link:../../../packages/zod-adapter
+ autoprefixer:
+ specifier: ^10.4.20
+ version: 10.4.20(postcss@8.5.3)
+ postcss:
+ specifier: ^8.5.1
+ version: 8.5.3
+ react:
+ specifier: ^19.0.0
+ version: 19.0.0
+ react-dom:
+ specifier: ^19.0.0
+ version: 19.0.0(react@19.0.0)
+ redaxios:
+ specifier: ^0.5.1
+ version: 0.5.1
+ tailwindcss:
+ specifier: ^3.4.17
+ version: 3.4.17
+ zod:
+ specifier: ^3.24.2
+ version: 3.25.57
+ devDependencies:
+ '@playwright/test':
+ specifier: ^1.52.0
+ version: 1.52.0
+ '@tanstack/router-e2e-utils':
+ specifier: workspace:^
+ version: link:../../e2e-utils
+ '@types/react':
+ specifier: ^19.0.8
+ version: 19.0.8
+ '@types/react-dom':
+ specifier: ^19.0.3
+ version: 19.0.3(@types/react@19.0.8)
+ '@vitejs/plugin-react':
+ specifier: ^4.3.4
+ version: 4.6.0(vite@6.3.5(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0))
+ combinate:
+ specifier: ^1.1.11
+ version: 1.1.11
+ vite:
+ specifier: 6.3.5
+ version: 6.3.5(@types/node@22.13.4)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.7.0)
+
e2e/react-router/generator-cli-only:
dependencies:
'@tanstack/react-router':