Skip to content

FileSystemRouter, bake: do not load TypeScript declaration files as routes - #45466

Open
robobun wants to merge 2 commits into
mainfrom
robobun/c7e1b51b/fsrouter-skip-declaration-files
Open

robobun wants to merge 2 commits into
mainfrom
robobun/c7e1b51b/fsrouter-skip-declaration-files

Conversation

@robobun

@robobun robobun commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Related to #7210

Problem

  • Bun.FileSystemRouter and the bake pages scan load TypeScript declaration files as routes: about.d.ts becomes /about.d.
  • Both loaders compare only the last dot-suffix with the extension list (src/router/lib.rs:602, src/runtime/bake/router/mod.rs:622), so .d.ts passes as .ts.
  • The route name docs/[...slug].d is one the parser rejects, so the constructor throws Catch-all route must be at the end of the path.

Fix

  • Each loader skips a .d.ts, .d.mts or .d.cts file (bun_paths::is_typescript_declaration) after the extension list selects it. Bake extensions: "*" keeps every file.
  • Next.js skips .d.ts pages (ignore .d.ts files inside pages folder vercel/next.js#30728). fileExtensions: [".d.ts"] selects no file today, so no opt-in is lost.
  • Verified: test/js/bun/util/filesystem_router.test.ts (3 new tests), test/bake/framework-router.test.ts (2 new). All 5 fail on main (116dd64, release) and pass on this branch.
  • Self-reviewed: 16 concerns raised, all addressed except two requested tracking issues (see Notes).

Background

  • fileExtensions selects the page files of a FileSystemRouter. The default list has .ts.
  • Bake is the experimental Bun.serve({ app }) server. Only a custom framework lists .ts.
  • Considered a skip in RouteLoader::load only: the bake scan keeps the fault. Suffix matching (FileSystemRouter extensions with prefix (like .page.tsx) #5822) needs a maintainer decision.

Downsides

  • Behaviour change with no flag: /about.d is no longer a route, also under fileExtensions: [".ts"].
  • Each page file costs 28 more instructions per load (main: 5,300), 31 in the bake scan. .text grows 256 bytes.
  • Other names with text after a ] still fail: [...slug].test.tsx throws, [id].x.tsx takes /7 from a catch-all page.
Notes

Repro (empty files are enough, 1.4.2 and main at 116dd64)

const router = new Bun.FileSystemRouter({ style: "nextjs", dir });
Object.keys(router.routes);
// about.tsx, about.d.ts                     -> "/about", "/about.d"
// docs/[...slug].tsx, docs/[...slug].d.ts   -> throws "Catch-all route must be at the end of the path"
// users/[...rest].tsx, users/[id].d.ts      -> match("/users/7") returns users/[id].d.ts

Bake, with the test-only FrameworkRouter class (nextjs-pages): about.d.ts is the page /about.d, and [id].d.ts fails the scan with Parameters must take up the entire file name.

What is specific to declaration files, and what is not

Bake scope

  • The scan has the same last-suffix compare. It selects a declaration file only when a router type lists .ts, .mts or .cts. That is the default list of a custom framework (src/runtime/bake/config/framework.rs:496). The built-in React framework lists .tsx and .jsx and never had the fault.
  • app and bun build --app are experimental (canary build, debug build, or BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE=1).
  • Effect of [id].d.ts on main, per entry point: the dev server prints "routes/[id].d.ts" is not a valid route and continues. The test-only class throws an AggregateError. bun build --app aborts on each route name that the scan rejects. bake: report route scan errors instead of aborting, and fail bun build --app on them #42836 replaces that abort, and this change does not touch it.
  • extensions: [] parses to the same empty list as "*", on main and on this branch. So declaration files stay pages there. The self-review asked for a tracking issue on what [] means. It is not filed.

Landing order and related PRs

Sites with a similar test that this change leaves alone

  • src/resolver/resolver.rs is_type_only_tsconfig_path: case-insensitive, pinned by its tests.
  • src/runtime/cli/pm_diff_normalize.rs kind_for: the same three suffixes today, for a different rule (a file that holds no statements). With a shared helper, a change made for the routers would also change bun pm diff, and no test covers that branch.
  • src/lint/eslint/rules/no_shadow.rs: a port of ESLint's isDeclareInDTSFile.
  • src/sema/resolve.rs SUPPORTED_DECLARATION_EXTENSIONS: the type checker's table. It follows TypeScript and also needs the matched suffix.
  • src/runtime/cli/run_command.rs completions: a contains test.
  • x.d.tsx, x.d.css.ts and x.D.ts stay routes. Next.js tests endsWith(".d.ts") only.

Measurements

Release builds of main at 116dd64 (A) and of this change on top of it (B), same checkout path. Instruction counts are user-space instructions of the main thread from a hardware counter (perf_event_open), read around the measured loop through bun:ffi. The container has no perf, valgrind, strace or bloaty.

measure main this change
FileSystemRouter load, per page file (.tsx) 5,240 to 5,490 +28
FileSystemRouter load, per non-page file (.css) 1,139 to 1,203 +3
FileSystemRouter load, per declaration file 5,253 to 5,590 1,076 to 1,141
bake pages scan, per listed file (.tsx) 12,362 (500 files) +31
bake pages scan, per unlisted file (.css) 775 to 781 +0
syscalls per declaration file per load 3 (openat, readlink, close) 0
Route::parse calls per declaration file per load 1 0
match(), instructions per call (median of 7 runs) 1,022.01 1,022.02
.text (size -A, stripped bun) 80,779,237 80,779,493 (+256)
.rodata 22,520,556 22,520,556
  • Trees: 500 and 1000 flat files, construct then 20 reload(), median of 5 runs. The spread between runs is at most 0.008 %.
  • The +3 for a non-page file is a register spill around the Entry::kind call in the changed loop. The predicate does not run for these files.
  • Two symbols differ in size out of 139,687: RouteLoader::load (+81 bytes) and FrameworkRouter::scan_inner (+128 bytes). The predicate is inlined.
  • Syscalls and Route::parse calls: gdb catchpoints and a breakpoint with ignore counts, 50 pages plus 20 declaration files, construct and 5 reloads. A tree without declaration files gives equal counts on both builds.

Self-review

  • Added after the review: the dev-server test for the list and "*" cases of the bake extensions option, the /users/7/d assertion, the per-entry wording in bake.d.ts, and one-line comments.
  • Removed after the review: a change of kind_for in bun pm diff to the new helper. It was a refactor in an unrelated command.
  • Not done: a tracking issue for names with text after a ] in Bun.FileSystemRouter, and a tracking issue for bake extensions: []. Both cases are described above.

Suites

  • Debug ASAN build of this branch: filesystem_router.test.ts 40 pass, test/bake/framework-router.test.ts 37 pass.
  • Release build of this branch: 40 pass and 37 pass. Release build of main at 116dd64: only the 5 new tests fail. bun 1.4.2: the 3 new router tests fail (and 2 tests for later fixes).
  • cargo test -p bun_paths --lib: 32 pass, with the new typescript_declaration_suffixes. CI runs this crate in the Miri job.
  • test/integration/bun-types/bun-types.test.ts: 22 pass.

no test proof · iteration 0 · 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

@robobun
robobun requested a review from alii as a code owner October 16, 2026 06:00
@robobun

robobun commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 4:35 AM PT - Oct 18th, 2026

❌ @robobun, your commit 2db5879 has 2 failures in Build #128109 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 45466

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

bun-45466 --bun

@robobun

robobun commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. The tests of this change pass on every lane. CI is red on tests that this change does not touch.

How I reproduced it (bun 1.4.2 and main at 116dd64, empty files are enough):

const router = new Bun.FileSystemRouter({ style: "nextjs", dir });
console.log(Object.keys(router.routes));
  • about.tsx and about.d.ts: the routes are /about and /about.d.
  • docs/[...slug].tsx and docs/[...slug].d.ts: the constructor throws Catch-all route must be at the end of the path.

On this branch the declaration files are not routes, and the constructor does not throw.

CI

  • Build 128109 (2db5879): 179 of 181 jobs passed. The two red jobs are test/bake/deinitialization.test.ts on Windows 2019 x64 and test/js/web/workers/worker-terminate-lifetime.test.ts on darwin x64.
  • The Windows failure is Segmentation fault at address 0x1 in the dev-server teardown fixture. That fixture uses HTML routes and HMR WebSockets, and no file-system router. The same job passed on the commit before, which has the same code (build 127139).
  • Build 127139 (563e100): 178 jobs passed. The darwin aarch64 test job could not download the build artifact (timeout after 120 s), and 2 jobs expired.
  • Build 126452 (0fb60bb): 180 of 181 jobs passed. The red job was worker-terminate-lifetime.test.ts on darwin x64.
  • No build lists a failure or a retry in the two test files of this PR (test/js/bun/util/filesystem_router.test.ts, test/bake/framework-router.test.ts).
  • The last commit is empty. It only started CI again.

PR: #45466

@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 7 seconds.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 402f928f-5b57-4a62-9ca5-f2f2458d1fd9
📥 Commits

Reviewing files that changed from the base of the PR and between 0fb60bb and 2db5879.

📒 Files selected for processing: 2
  • packages/bun-types/bun.d.ts
  • src/runtime/bake/router/mod.rs

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 61e6a1b0-9f05-45e2-8a01-6d3110a22657
📥 Commits

Reviewing files that changed from the base of the PR and between 86da172 and 0fb60bb.

📒 Files selected for processing: 8
  • docs/runtime/file-system-router.mdx
  • packages/bun-types/bun.d.ts
  • src/paths/lib.rs
  • src/router/lib.rs
  • src/runtime/bake/FrameworkRouter.rs
  • src/runtime/bake/bake.d.ts
  • test/bake/framework-router.test.ts
  • test/js/bun/util/filesystem_router.test.ts

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

Walkthrough

Both filesystem router implementations now skip TypeScript declaration files ending in .d.ts, .d.mts, or .d.cts during route discovery. Tests cover suffix matching, extension settings, reloads, and serving behavior.

Changes

Router declaration-file filtering

Layer / File(s) Summary
Declaration-file suffix predicate
src/paths/lib.rs
A shared predicate matches .d.ts, .d.mts, and .d.cts suffixes. Tests cover exact matches and near-matches.
Filesystem router filtering
docs/runtime/file-system-router.mdx, packages/bun-types/bun.d.ts, src/router/lib.rs, test/js/bun/util/filesystem_router.test.ts
The route loader skips declaration files after extension matching. Tests cover initial loading, reloads, configured extensions, and similarly named files. Documentation describes the exclusion.
Framework router filtering and serving
src/runtime/bake/FrameworkRouter.rs, src/runtime/bake/bake.d.ts, test/bake/framework-router.test.ts
The framework router scan skips declaration files after extension filtering. Tests cover route discovery and serving with default and wildcard extensions.

Suggested reviewers: jarred-sumner

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 0fb60

The declaration-file filtering appears ready to merge after normal checks.

Pre-merge checks: Passed 4
Check name Status Explanation
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.
Title check Passed The title clearly and concisely describes the main change: both FileSystemRouter and bake no longer load TypeScript declaration files as routes.
Description check Passed The description explains the problem, the fix, scope, downsides, related work, and verification results. It does not use the exact template headings, but it provides the required change summary and ve…
Full details: Description check

Explanation

The description explains the problem, the fix, scope, downsides, related work, and verification results. It does not use the exact template headings, but it provides the required change summary and verification details in equivalent sections.

✨ Finishing Touches: 💡 1
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • 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.

@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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline findings, I also checked the placement of both skips: in src/router/lib.rs the new check sits inside the matched-extension branch, so dotfiles and files with unlisted extensions take exactly the pre-existing path, and reload() goes through the same RouteLoader::load, so there is no second scan site to patch. In src/runtime/bake/FrameworkRouter.rs the skip runs only after the found check inside the non-empty-extensions block, matching the stated "*" behavior.

Extended reasoning...

The change adds a byte-exact .d.ts/.d.mts/.d.cts predicate in src/paths/lib.rs and calls it from the two route scanners (Bun.FileSystemRouter and bake FrameworkRouter) so declaration files are dropped after the extension list selects them, plus docs, JSDoc and tests. It touches no security-sensitive surface. It is an unflagged user-facing behavior change (routes like /about.d disappear even under an explicit fileExtensions: [".ts"]), and inline findings were posted, so a human should weigh the API-design call rather than this being approved automatically.

Comment thread src/paths/lib.rs Resolved
Comment thread src/runtime/bake/FrameworkRouter.rs Resolved
Comment thread src/paths/lib.rs Resolved
…outes

Both route loaders compare only the last dot-suffix of a file name with
the extension list. So `about.d.ts` is selected as `.ts` and becomes the
route `/about.d`.

Add `bun_paths::is_typescript_declaration` (`.d.ts`, `.d.mts`, `.d.cts`).
`RouteLoader::load` and `FrameworkRouter::scan_inner` skip such a file
after the extension list selects it. Next.js does not make a `.d.ts`
file a page.

A declaration file beside a dynamic page also has a route name with text
after the `]`, for example `docs/[...slug].d`. The route-name parsers do
not accept that name: `new Bun.FileSystemRouter()` throws "Catch-all
route must be at the end of the path", and the bake scan reports
"Parameters must take up the entire file name". The skip removes these
failures for declaration files only. Other names with text after a `]`
behave as before.
@robobun
robobun force-pushed the robobun/c7e1b51b/fsrouter-skip-declaration-files branch from 0fb60bb to 563e100 Compare October 17, 2026 01:56

@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 completed

Nothing new to post: everything this review found is already covered by existing comments on this pull request or didn't merit a separate one.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant