Skip to content

FileSystemRouter: decide route precedence and the route for / at load, reject empty [param] segments - #47206

Open
robobun wants to merge 4 commits into
mainfrom
robobun/27d4f0a3/fsrouter-nextjs-precedence
Open

robobun wants to merge 4 commits into
mainfrom
robobun/27d4f0a3/fsrouter-nextjs-precedence

Conversation

@robobun

@robobun robobun commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • Bun.FileSystemRouter gives the wrong route or null, with no error. On random route trees, 710 of 39,538 lookups differ from next@16.4.3. No user reported it.
  • sorter::sort_by_name (src/router/lib.rs) orders dynamic routes by their most dynamic segment, and match() takes the first that accepts. /docs/intro goes to /[org]/[repo], not /docs/[...path].
  • / reads only the index route, and docs/[page] matches /docs with page: "".

Fix

  • load_all sorts dynamic routes by a key built once per route with the matcher's tokenizer, Pattern::init. Static text beats [x] beats [...x] beats [[...x]], as in Next.js. Of two names that differ only in case, the lowercase one goes first.
  • Routes.root holds the index route, else the first root-level [[...x]]. Pattern::match_ rejects an empty [param].
  • Verified: test/js/bun/util/filesystem_router.test.ts (23 of 80 fail on main). Also 18242.test.ts and resolve.test.ts.
  • Self-reviewed: 24 concerns raised, 21 addressed.

Background

Downsides

  • Breaking, with no error. A URL one segment short of a [param] route no longer matches it. One public site serves /procedures from procedures/[category].tsx. @Jarred-Sumner, which release takes this?
  • Another file wins for some URLs. On 63 route trees of public repos this changes no lookup.
  • A [param] hit now also tries the catch-alls sorted before it: in a tree of 400 routes, 201 against 100 on main (a catch-all hit: 199 against 250.5). Construction costs up to 6.4% more instructions.
Notes

This supersedes #39290 and #39291. #46021 took the crash fix of each and left both open for the routing half. This PR brings that half to today's main as one change. From #39291 it keeps the order rule and the removal of the dead index_id branch. From #39290 it keeps the [param] rule. The key has the shape of precedence_key in #39488.

Repro (bun repro.mjs, main prints 5 faults, this PR prints none):

import fs from "node:fs"; import os from "node:os"; import path from "node:path";
const d = fs.mkdtempSync(path.join(os.tmpdir(), "p-")); let k = 0;
function m(files, url) { const dir = `${d}/r${k++}`; for (const f of files) { fs.mkdirSync(path.dirname(`${dir}/${f}`), { recursive: true }); fs.writeFileSync(`${dir}/${f}`, "export default 1"); }
  return new Bun.FileSystemRouter({ dir, style: "nextjs" }).match(url)?.name ?? "null"; }
const W = [ // files, url, the route that next@16.4.3 picks
  [["[[...slug]].tsx"], "/", "/[[...slug]]"], [["[[...slug]].tsx"], "/?q=1", "/[[...slug]]"],
  [["[slug].js", "opt/[[...rest]].js"], "/opt", "/opt/[[...rest]]"],
  [["posts/[...id].js", "posts/wow/[[...id]].js"], "/posts/wow", "/posts/wow/[[...id]]"],
  [["[team]/[...rest].js", "[team]/docs/[[...path]].js"], "/acme/docs", "/[team]/docs/[[...path]]"],
];
for (const [f, u, want] of W) { const g = m(f, u); console.log(u, g === want ? "ok" : `got ${g}, want ${want}`); }

Behavior changes (for the release notes). None of them gives an error or a warning.

  1. A [param] takes one segment that is not empty. docs/[page].tsx no longer matches /docs, and blog/[slug]/[id].tsx no longer matches /blog//42. The URL goes to the next route that accepts it, or to null.
    • To keep /docs served, add docs/index.tsx (its params are {}, not { page: "" }), or name the file docs/[[...page]].tsx, which also takes deeper paths.
    • A real case of null: alexandre-dosreis.me has procedures/[category].tsx, no procedures/index.tsx, and /procedures in its prerender list. The page handles an empty category. The site answers a null match with a 404, and its Dockerfile builds on oven/bun:1.
    • A real case of another route: keeper.sh has api/sources/[id].ts and api/sources/callback/[provider].ts. /api/sources/callback was /api/sources/callback/[provider] with provider: "". It is now /api/sources/[id] with id: "callback", as in Next.js.
  2. Of two dynamic routes that accept a URL, the winner is decided at the first token where they differ: static text, then [x], then [...x], then [[...x]]. A token is a piece of a route name as the matcher's Pattern::init cuts it. A route comes before the routes under it. No option restores the old order. Routes of the same shape keep the order they had.
  3. A root-level [[...slug]] serves / when there is no index route. The index route still wins when both exist.
  4. Two dynamic routes whose names differ only in case no longer tie. The name with the lowercase letter at the first difference wins: /[id] over /[Id], and /[org]/docs over /[org]/Docs. For the second pair next@16.4.3 serves /acme/docs from the same file. On main the winner of such a pair changes with the number of other routes. With [id].tsx beside [Id].js and 0 to 80 other routes, main gives /x to [Id].js at 20 of the 81 counts, first at 32 other routes, and in all 10 creation orders of 52 files that I tried. The sort is unstable: two equal items can change places, and whether they do depends on the number of items. So the winner can differ from the one that main gives for the same tree.
  5. router.routes lists the dynamic routes in the new order. The static routes keep their order.

One existing assertion changes. "should support catch-all routes" (b249ed7, 2022) asserted that /posts/wow/hey/there goes to /posts/[...id] although posts/wow/[[...id]].tsx exists. It now goes to /posts/wow/[[...id]], which is what next@16.4.3 picks for that tree. The sort that this PR replaces is from 8e0230b (2021).

How far each change reaches

Lookups that change 400 random trees (44,067 lookups) 63 route trees of 49 public repos (5,835 lookups)
1. [param] rule 322: 194 to null, 128 to another route 231: 225 to null, 6 to another route
2. Order 876 0
3. / 159 0
  • The public trees are route directories of repos that use Bun.FileSystemRouter. The URLs come from each tree's own route names: every route, every prefix of it, one more segment, a trailing slash, and an empty segment in each [param] place.
  • On those trees only the [param] rule changes a lookup. In every one of the 231, main had bound a [param] to "". The order of router.routes differs on 5 of the 63 trees and moves no lookup there.
  • So the measured benefit of the order and of / on these public trees is zero. They make the router agree with its documented reference. The [param] rule is the one that can break a site.
  • These runs are from before the step for names that differ only in case. Only such a pair reaches that step: two dynamic routes with the same name but for case, such as [id].tsx beside [Id].js. I did not search the 63 trees for such a pair, and the random trees have none.
  • On the random trees, of the 1,357 lookups that change, 810 are on trees that Next.js accepts and now agree with it. None moves away from it. 404 are on trees that Next.js rejects, and 143 are // and /index spellings.

Who asked

Why in place, and which release

How the reference was checked

The expected routes come from the published next@16.4.3 package. getSortedRoutes (dist/shared/lib/router/utils/sorted-routes.js) orders the pages, and the first page whose route regex matches wins, as in dist/server/lib/router-utils/filesystem.js. The server builds the regex with getNamedRouteRegex and three options. The runs below use getRouteRegex. The two give the same route and params on 301,888 lookups over 2,848 generated trees.

A differential run builds random trees of 1 to 4 route files, keeps the trees that Next.js accepts, and compares the route name and the params of 106 URLs per tree.

Seeds Trees that Next.js accepts Lookups Wrong on main Wrong with this PR
1 373 39,538 710 0
2, 7, 11, 12, 13 1,895 200,870 3,976 0
101 to 108 (3,000 trees each) 22,546 2,389,876 45,925 0
  • The generator draws catch-alls far more often than real trees have them, and it uses static names of two or more lowercase characters. So the zero holds for such names only. The bugs under "Not in this PR" are outside it.
  • On a hand-written tree of 26 page files (blog/[slug], blog/archive/[[...date]], docs/[...path], api/[...proxy], [org]/[repo]/...), main gives 2 of 26 URLs to another file than next@16.4.3: /docs/intro goes to /[org]/[repo], and /blog/archive goes to /blog/[slug]. This PR gives 0 of 26.
  • Every row of the first test table is a tree that Next.js accepts, and its expected values are the output of that oracle. The second table has trees that Next.js refuses to build. Bun accepts them, so the table pins one order for them.

The key

sorter::Keys holds one byte string per dynamic route. Per token it has the rank of the tag (1 static text, 2 [x], 3 [...x], 4 [[...x]]), and for static text also the text and a 0. A route name cannot contain a NUL byte, so the byte order of two keys is the order of their tokens, and a route that ends first sorts first. Routes with the same key fall back to the order they had before. Two names that are still equal then differ only in case. The last step compares them as written, in descending byte order, so the lowercase spelling goes first. After that step no two dynamic routes compare equal, so the sort has one result for any order of the files.

The tokens come from Pattern::tokens, which validate now uses too. It calls the matcher's own Pattern::init, so the key and the matcher cut a name at the same places.

Deleted

  • sorter::sort_by_name (the order by Route::kind and param_count) and its adapter sort_by_name_cmp.
  • The index_id branch of load_all. The index route has kind == Static, so it was never in the dynamic part of the list.
  • The empty-path branch of Routes::match_. match_page_with_allocator trims every trailing slash first, so it never passed a path that trims to nothing. It answers the empty path itself, from root.

Measurements (release builds of main 9b8347a and of that commit with this PR. They were taken with the last comparison step in the other direction, which is the same code with the two operands swapped. No measured tree reaches that step.)

What main this PR How
match(), index route hit 49 52 instructions inside Routes::match_page_with_allocator, gdb stepi, third call
match(), static route hit 171 174 same
match(), first dynamic route hit 736 746 same
match(), miss over 300 dynamic routes 72,760 72,765 same
match("/"), no index route, root [[...slug]], 10 / 31 / 301 dynamic routes 41 / 41 / 41 (null) 52 / 52 / 52 (the route) same
match("/"), no index route, no root [[...slug]], 300 dynamic routes 41 46 same
constructor, 201 static + 300 dynamic routes 2,502,494 2,539,895 (+1.5%) instructions:u of 400 constructions minus 200, per construction
constructor, 501 static routes 1,878,555 1,725,342 (-8.2%) same
constructor, 100 static + 300 dynamic routes 1,401,271 1,490,441 (+6.4%) same
constructor, 300 routes [p]/sNNN 944,662 890,433 (-5.7%) same
allocations per constructor (501 routes) 2,090 2,092 gdb breakpoint hits on the mimalloc entry points, 2 constructions minus 1
allocations per match(), dynamic hit 1.09 1.09 same, 2,000 calls minus 1,000
RSS per live router (501 routes) 91.9 KB 91.9 KB RSS delta over 2,000 routers, 3 runs each
text of the release binary 108,863,485 108,864,765 (+1,280) size
bun_router symbols 18,678 19,813 (+1,135) nm -S, sizes summed
  • The 3 instructions on the index hit and the static hit are not new work. The same source statements run. An instruction trace of both builds shows one more register save in the prologue and another encoding of the loop that trims trailing slashes, in the one function that also holds the inlined walk.
  • The first dynamic hit pays the [param] rule once per [param] token that it reaches.
  • The constructor pays one more walk over the name of each dynamic route (Keys::new, about 400 instructions per dynamic route). A comparison is cheaper than before (88 instructions against 108 on the first tree), because the old adapter called sort_by_name up to twice per comparison. So a router with few dynamic routes gets cheaper and a router with many gets dearer.
  • match() walks the dynamic list in the new order, so the cost of a hit moves with the place of its route. Main tries every route that has only [param] segments before any catch-all. This PR sorts by path, so a catch-all under a static name comes before a [param] beside it. In a tree of 100 folders that each hold [id], [id]/[sub], all/[...rest] and opt/[[...rest]] (400 dynamic routes), the mean place of the winner goes from 100 to 201 for [id], from 101 to 202 for [id]/[sub], from 250.5 to 199 for [...rest] and from 350.5 to 200 for [[...rest]]. The mean over the 400 routes is 200.5 on both builds, because the list holds the same routes. A rejected candidate costs about 243 instructions (the miss above).
  • On random trees the walk grows with the size of the tree. On 60 trees of 26 to 200 route files with one URL per route (7,126 lookups), the 2,680 lookups whose route is the same dynamic route on both builds visit 48.0% more candidates: 55.8% more for a [param] route, 17.4% fewer for a catch-all route. The 888 lookups that go to another route on this PR visit 42.1% fewer. Over all lookups the walk is 12.7% longer. On 400 trees of 1 to 5 route files (1,145 lookups) it is 1.6% longer. An earlier version of this bullet gave only the small-tree figure of the differential run (8.0% for the lookups that keep their dynamic route, on trees of 1 to 4 files).
  • Each clause of the fix is pinned by tests. Without the [param] rule 5 tests fail, without the slot for / 5, without the key 9, with plain byte order in place of the old order for ties 2, without the last step on the name as written 3.
  • perf, valgrind and bloaty are not on the machine. The instruction counts use perf_event_open directly and gdb.
  • On a debug build, the existing test "reload() while Bun.build() resolves the same directory" is slow on my machine, and its time follows the load of the disk: 28 to 86 s over the runs, against a timeout of 60 s, with this PR and on main (11c3671) without it. On a release build it takes 2 s. In the last run with this PR all 80 tests of the file pass on the debug build. On a debug build of main 23 fail for the reasons that this PR fixes. In that run of main two more tests ran past their timeouts while the disk was busy: the slow test above, and one that passes in 1.7 s when it runs alone.

Other designs

Open pull requests on the same file

Not in this PR

Self-review

24 concerns survived. 21 are addressed: the breaking label and the wording above, the reach of each change, two table rows for a [param] that hands a URL on, the foo[id] test (removed), the exclusions with their trackers, and the notes on #46286, #46584, #39488 and Bun.serve. Three are declined:

  • Split the [param] rule into a first PR. The order gives wrong answers without it, and the rule is a six-line hunk that can be carved out if the order has to wait.
  • Fold the fix for Bun.FileSystemRouter router match unknown behaviour #12206 in. It is a second cause in the tokenizer with its own behavior changes, so it is a PR on top of this one.
  • Cite outside implementations of the same order as support. I did not verify them.

The self-review also had a step removed that broke a tie between names that differ only in case: it put the uppercase spelling first and no test pinned it. A later review asked for a decided winner. The step is back in the other direction (lowercase first, which is the file that Next.js serves), with mirrored test rows.


no test proof · iteration 4 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/util/filesystem_router.test.ts

…, reject empty [param] segments

match() takes the first dynamic route that accepts a path, and the
dynamic routes were ordered by their most dynamic segment and by the
bytes of parameter names. So /docs/intro went to /[org]/[repo] when
docs/[...path] existed.

load_all now sorts dynamic routes by a key that it builds once per route
from the matcher's own tokens. At the first token where two routes
differ, static text beats [x] beats [...x] beats [[...x]], and a route
comes before the routes under it. This is the order of Next.js's
getSortedRoutes. Static routes keep their order, and routes of the same
shape keep their winner.

Routes.index becomes Routes.root: the index route, else the first
root-level [[...x]] in match order. Such a route now serves / when there
is no index route, and match("/") stays one pointer read.

A [param] no longer accepts a missing or empty segment.
@coderabbitai

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 4c64b2c and 6d0bfb2.

Files selected for processing: 1
  • src/router/lib.rs

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

Walkthrough

The router orders routes by pattern precedence and uses a root-level optional catch-all for empty-path matching when no index route exists. Tests cover route ordering, matching, root fallback selection, and reload behavior.

Changes

Filesystem router precedence and root matching

Layer / File(s) Summary
Pattern precedence and matching
src/router/lib.rs, test/js/bun/util/filesystem_router.test.ts
Pattern tokens now define route ordering by precedence. Dynamic matching rejects empty path segments. Tests cover route-tree precedence, case-only name conflicts, catch-all matching, and exposed route order.
Root route selection and matching
src/router/lib.rs, test/js/bun/util/filesystem_router.test.ts
The router selects a root-level optional catch-all when no index route exists and uses the selected route for empty-path matching. Tests cover path variants, unmatched paths, and route changes after reload().

Suggested reviewers: jarred-sumner, alii

Priority: Normal

Merge Risk: Minimal · up to 6d0bf

No merge-blocking issue was identified. Root fallback and reload behavior are supported by source inspection; merge readiness remains subject to normal build and test checks.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly identifies the main changes: route precedence, root-route selection, and rejection of empty [param] segments.
Description check Passed The description is highly detailed and covers the problem, implementation, behavior changes, verification results, performance impact, and known limitations. It does not use the exact template heading…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description is highly detailed and covers the problem, implementation, behavior changes, verification results, performance impact, and known limitations. It does not use the exact template headings, but it provides the required information in equivalent sections.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Finishing Touches: 💡 1
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR

@robobun

robobun commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 12:51 AM PT - Oct 29th, 2026

❌ @robobun, your commit 6d0bfb2 has 10 failures in Build #134830 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 47206

That installs a local version of the PR into your bun-47206 executable, so you can run:

bun-47206 --bun

@robobun

robobun commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

  • Reproduced on main (11c3671, debug build) and on 1.4.3. With pages/[[...slug]].tsx alone, match("/") is null. /opt goes to /[slug], not to /opt/[[...rest]]. /posts/wow goes to /posts/[...id], not to /posts/wow/[[...id]]. /acme/docs goes to /[team]/[...rest], not to /[team]/docs/[[...path]]. docs/[page].tsx matches /docs with page: "".
  • next@16.4.3 gives the other answer in each case (getSortedRoutes, then the first route whose regex matches).
  • Names that differ only in case: with [id].tsx beside [Id].js and 0 to 80 other routes, main gives /x to [Id].js at 20 of the 81 counts. This PR gives /[id] at all 81.
  • test/js/bun/util/filesystem_router.test.ts: 23 of 80 tests fail on a debug build of main, and all 80 pass with this PR on a debug build.
  • This pull request: FileSystemRouter: decide route precedence and the route for / at load, reject empty [param] segments #47206. It supersedes FileSystemRouter: match / against a root-level optional catch-all route #39290 and FileSystemRouter: order dynamic routes by per-segment precedence like Next.js #39291.
  • CI (build 134830, head 6d0bfb2, an empty commit on top of 6fce751): no lane reports a failure in filesystem_router.test.ts. The red jobs do not come from this PR:
    • test/cli/check/check.test.ts: the case "finishes within 10 s on conditions nested deeper than the stack allows" ran out of its 10 s on macOS x64 and on Ubuntu 25.04 x64. The same case fails on other pull requests (builds 134817 and 134795). bun check cannot reach the code of this PR: only src/runtime/api/filesystem_router.rs uses the router crate.
    • One macOS aarch64 job: the test runner crashed (uv_os_get_passwd gave ENOENT, and test files were gone from the checkout).
    • Six jobs were canceled because CI could not create agents (CapacityLimitExceeded).
    • test-aot-drawn is a soft-fail lane that fails on main too (builds 134661 and 134576).
  • Open for the maintainer: the PR changes behavior (label breaking). The first Downsides bullet asks which release takes it.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline findings, I also checked the new Routes::root raw pointer (the sorted position from order.iter().position(..) is exactly the slot apply_permutation_in_place fills and route_list.route[i] is that same Box, so it does not dangle), the debug_assert!(!pathname.is_empty()) in match_ (its only caller, match_page_with_allocator, has already collapsed "", //, /index and . to the root path before calling it), and the removal of the dynamic_len -= 1 exclusion (the index route's name yields no tokens, so its kind is Static and it never sat in the dynamic range) — none of these is a problem.

Extended reasoning...

The change rewrites dynamic-route precedence in src/router/lib.rs (new tokenized sort key, root-level optional catch-all serving /, rejection of empty [param] segments) and adds a large Next.js-parity test matrix in test/js/bun/util/filesystem_router.test.ts; it touches no auth, crypto or injection surface. Inline findings already flag the intentional breaking [param] change and two ordering edge cases, so a human needs to look regardless; this note only records the raw-pointer, debug-assert and index-route concerns that were verified sound.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread src/router/lib.rs Resolved
Comment thread src/router/lib.rs Resolved
Comment thread src/router/lib.rs Resolved
Two dynamic routes whose names differ only in case, such as [id].tsx
beside [Id].js, compared equal. The sort is unstable, so either one
could come first, and match() takes the first route that accepts. The
winner changed with the number of other routes and with the order in
which the files were created.

The comparator gets a last step on the name as written, in descending
byte order, so the lowercase spelling goes first. A URL reaches static
text only by its lowercase spelling, and for that URL Next.js serves the
file with the lowercase name.
Comment thread src/router/lib.rs Resolved
Comment thread src/router/lib.rs Resolved
Comment thread src/router/lib.rs Resolved
Comment thread src/router/lib.rs Resolved
Comment thread src/router/lib.rs Resolved

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-reviewed the follow-up commit and found no new bugs; because this is a labeled-breaking change to route precedence whose release train is still an open question, a maintainer's look is still the next step.

What was reviewed this pass:

  • The new name tie-break in Keys::compare plus the 6-tree test closes the case-only nondeterminism raised last round.
  • root is taken from route_list.route[i] after the permutation is applied and the Boxes are moved; this.index stays valid across apply_permutation_in_place since only the Box pointers move, not the heap Route.
  • The debug_assert!(!pathname.is_empty()) in match_ has one caller, which trims trailing slashes and answers the empty path from root first, so the precondition holds structurally in release too.
  • position(|&i| keys.takes_empty_path(i)) and iter().cmp() are predicate/ordering calls, not byte-literal searches, so the byte-search lints don't apply.
Extended reasoning...

The PR rewrites the dynamic-route sort in src/router/lib.rs as a precomputed token key, moves the / decision to load time (index route, else first root-level [[...x]]), and makes [x] reject an empty or missing segment, with ~380 lines of new tests in test/js/bun/util/filesystem_router.test.ts. It touches raw-pointer plumbing (Routes::root, static_) across a new permutation step but no security-sensitive surface. The follow-up commit addressed the case-tie finding from the prior review; the remaining open thread is the silent-breaking-change question, which the PR description itself escalates to a maintainer for release placement. The bug hunt exited on dry_streak with no findings, but a breaking user-facing behavior change with an unresolved release decision is not something to approve automatically.

The comment on the co-owned pointer goes back to the field it was on.
No code changes.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

@robobun

robobun commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator Author

@Jarred-Sumner, one more question for the same function. It is the item under "Not in this PR" about a [param] that does not span its path segment.

Today (1.4.3 and main) the tokenizer cuts such a name inside the segment. match() then wants a / at the cut.

  • [id]-edit.tsx serves /7/edit, not /7-edit. Beside [id]/edit.tsx it takes /7/edit from that file.
  • users/[id].test.ts serves /users/7/test.
  • foo[id].tsx serves /foo/1.

No issue reports this. I did not open a code PR, because each fix is a visible change and the choice is yours.

Which behavior do you want for such a name?

  1. Literal text. The file keeps loading and matches only its own text (/[id]-edit). router.routes keeps its keys. The match() answers above change with no error, so this belongs on the breaking list.
  2. A load error, as in the bake router ("Parameters must take up the entire file name"). Here the constructor throws for the whole directory. That stops trees that load today: versia-pub/server keeps [name].test.ts beside its route files and reads only router.routes.
  3. No change to the router. docs: a FileSystemRouter parameter is the whole name of a file or folder #47254 adds the rule to the docs and the JSDoc: a parameter is the whole name of a file or folder.

In 166 public repositories that use the router, I found none that calls match() with such a name. So option 1 changes no caller that I know of.

#47254 is true for each answer. Its notes have the table of today's answers and the public trees with such names.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants