Skip to content

bun-types: declare FileSystemRouter.origin as string | null and MatchedRoute.query values as string | string[] - #39282

Open
robobun wants to merge 2 commits into
mainfrom
farm/baa31bea/fsrouter-origin-query-types
Open

bun-types: declare FileSystemRouter.origin as string | null and MatchedRoute.query values as string | string[]#39282
robobun wants to merge 2 commits into
mainfrom
farm/baa31bea/fsrouter-origin-query-types

Conversation

@robobun

@robobun robobun commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • bun-types declares FileSystemRouter.origin as string, but the getter returns null whenever the router was constructed without an origin or with origin: "" (get_origin in src/runtime/api/filesystem_router.rs:662; the constructor only stores a non-empty origin, :325). router.origin.length type-checks and throws at runtime.
  • bun-types declares MatchedRoute.query as Record<string, string>, but a query string name that appears more than once is reported as an array of its values: match("/?a=1&a=2&b=3").query is { a: ["1", "2"], b: "3" } (create_query_object at :883 hands every value for a name to putRecord, which builds a JSArray when there is more than one, src/jsc/bindings/bindings.cpp:2808). match.query.a.toUpperCase() type-checks and throws on a request the client controls.
  • Both behaviors date from the commit that introduced Bun.FileSystemRouter (d21aee5); the declarations have never matched them. Bun.FileSystemRouter router match unknown behaviour #12206 (mainly about the param-leak bug) also asks for the array case to be "properly defined".

Fix

Background

  • Bun.FileSystemRouter resolves URL paths against a Next.js-style pages directory. match() returns a MatchedRoute whose params are the values of the route's dynamic segments and whose query is the parsed query string merged with those params. Both objects are built by the same native helper, which turns a name with one value into a string and a name with several values into an array.
  • origin is an optional constructor option used to build MatchedRoute.src; the getter reports the configured value back.
  • test/integration/bun-types/bun-types.test.ts packs packages/bun-types, installs it into a copy of test/integration/bun-types/fixture/, and type-checks the fixture; fixture/*.ts files hold the assertions, with expectType(x).is<T>() failing unless the type of x is exactly T. CI runs this file with a release bun (.github/workflows/bun-types.yml; Buildkite excludes it), which is where the fixture assertions are checked.
Runtime probe (bun 1.4.0 release, unchanged by this PR)
$ mkdir -p pages/blog && echo 'export default 1' > 'pages/blog/[slug].js' && echo 'export default 1' > pages/settings.js
$ bun -e '
const o = { dir: "pages", style: "nextjs" };
console.log(new Bun.FileSystemRouter(o).origin, new Bun.FileSystemRouter({ ...o, origin: "" }).origin, new Bun.FileSystemRouter({ ...o, origin: "https://x.test" }).origin);
const r = new Bun.FileSystemRouter(o);
console.log(JSON.stringify(r.match("/settings?foo=bar&foo=baz&page=2").query));
console.log(JSON.stringify(r.match("/blog/my-cool-post?slug=other&page=2").query));
console.log(JSON.stringify(r.match("/blog/hello?tag=a&tag=b&page=2").query));'
null null https://x.test
{"foo":["bar","baz"],"page":"2"}
{"slug":"my-cool-post","page":"2"}
{"slug":"hello","tag":["a","b"],"page":"2"}

params probes: cat/[...rest].js matched against /cat/a/b/c gives { rest: "a/b/c" }; dup/[a]/[a].js against /dup/x/y gives { a: ["x", "y"] } (the duplicate-name shape discussed above).

Fixture failure against the unchanged declarations
fsrouter.ts(10,30): error TS2344: Type 'string | null' does not satisfy the constraint 'string'.
fsrouter.ts(16,30): error TS2344: Type 'Record<string, string | string[]>' does not satisfy the constraint 'Record<string, string>'.
fsrouter.ts(18,50): error TS2344: Type 'string[]' does not satisfy the constraint 'string & any[]'.

bun test test/integration/bun-types/bun-types.test.ts (release bun): 9 fail / 6 pass with packages/ at main, 15 pass with this change. Same three errors, then a pass, from bun bd test on this file when #39270's bun-types.test.ts is checked out next to it.

Earlier revision

The first push also added a describe("Bun.FileSystemRouter") case to bun-types.test.ts that spawned tsc over fixture/fsrouter.ts so the file had a non-skipped case under debug builds. It duplicated what every release case already checks, and #39270 replaces that pattern with one whole-fixture case plus a lint holding the file at a single spawn site, which the extra case would have tripped once both landed. It was removed; the assertions only ever lived in the fixture.

…ry values can be string[]

The origin getter returns null when the router was constructed without
an origin (or with an empty one), and a query string name that appears
more than once is reported as an array of its values. Declare both the
way the runtime behaves, document the query shape, and assert the
declarations in the bun-types fixture.
@robobun
robobun requested a review from alii as a code owner August 16, 2026 03:37
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 16 minutes

Limit details: You’ve used all 5 included reviews currently available under your plan.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f174b549-af39-4ad4-94c8-eb880599fd1e

📥 Commits

Reviewing files that changed from the base of the PR and between 8c5296a and 511713e.

📒 Files selected for processing (3)
  • docs/runtime/file-system-router.mdx
  • packages/bun-types/bun.d.ts
  • test/integration/bun-types/fixture/fsrouter.ts

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

@robobun

robobun commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 12:41 AM PT - Aug 16th, 2026

@robobun, your commit 511713e is building: #99303

@robobun

robobun commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced on the released bun 1.4.0 (new Bun.FileSystemRouter({ dir, style: "nextjs" }).origin is null; match("/?a=1&a=2").query is { a: ["1", "2"] }). Declarations and docs updated to match; no runtime change.

Current shape after self-review: bun.d.ts + docs/runtime/file-system-router.mdx + fixture/fsrouter.ts only. The first push also added a per-API tsc case to bun-types.test.ts; it duplicated what the release cases already check and would trip the single-spawn-site lint #39270 adds, so it was dropped. Merges cleanly with #39278 and #39270 in either order.

Verification: bun test test/integration/bun-types/bun-types.test.ts with a release bun is 9 fail / 6 pass with packages/ at main (three TS2344 errors in fixture/fsrouter.ts) and 15 pass with this branch. Under a debug build this file's type-checking cases are skipped on main today, so bun bd test on it does not distinguish the two; with #39270's test file checked out alongside it does (same three errors, then a pass).

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

LGTM — declarations now match the runtime.

Checked get_origin in src/runtime/api/filesystem_router.rs (returns JSValue::NULL when unset) and JSC__JSObject__putRecord in bindings.cpp (builds a JSArray when valuesLen != 1) — both new types are accurate.
The new test case mirrors the existing Bun.mmap debug-build tsc case exactly, and utilities.ts is copied alongside fsrouter.ts so the relative import resolves.
Docs examples match the runtime probe output in the PR description; params deliberately left as Record<string, string> per the stated rationale.

Extended reasoning...

Overview

This PR widens two type declarations in packages/bun-types/bun.d.ts to match long-standing runtime behavior: FileSystemRouter.origin becomes string | null and MatchedRoute.query becomes Record<string, string | string[]>. It also updates docs/runtime/file-system-router.mdx with two new examples and the corrected reference type, tightens the fixture assertions in test/integration/bun-types/fixture/fsrouter.ts to use exact-type equality (.is<T>()), and adds a debug-build-friendly tsc case to bun-types.test.ts that follows the existing Bun.mmap pattern verbatim. No runtime code is touched.

Security risks

None. Pure .d.ts, docs, and test-fixture changes.

Level of scrutiny

Low. This is a docs/types correction with no compiled-code impact. I verified both runtime claims directly against source: get_origin at filesystem_router.rs:662 returns JSValue::NULL when this.origin is None, and putRecord at bindings.cpp:2808 sets a JSArray when more than one value is passed. The widened types are strictly more accurate than what they replace and match Next.js's own query typing.

Other factors

The new test case is a near-copy of the adjacent Bun.mmap block (same typeRoots wiring, same spawn-tsc shape, same stdout/stderr/exitCode assertion order). The fixture copies utilities.ts into the check dir so the ./utilities import resolves even though tsconfig.include lists only fsrouter.ts — tsc pulls it in transitively. The PR description documents that the fixture fails against the old declarations and passes against the new ones under both release and debug builds, and explicitly leaves params and the assetPrefix/scriptSrc gaps to their own PRs. The bug-hunting system found no issues.

@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 reviewed this PR and didn't find any bugs. The declarations match the runtime (get_origin returns JSValue::NULL when unset; putRecord builds a JSArray for repeated names), the docs examples reproduce, and the fixture assertions pin the exact widened types. Deferring only because packages/bun-types/ has a CODEOWNERS entry and the widening is a user-visible TypeScript break — worth a human sign-off on the API-design call (widen the types vs. change the getter/query builder).

What was reviewed

  • get_origin in src/runtime/api/filesystem_router.rs and JSC__JSObject__putRecord in bindings.cpp — both confirm the declared shapes.
  • params deliberately left as Record<string, string> — catch-all is a joined string; the only array case is the duplicate-segment shape the PR argues is out of scope.
  • Fixture edit is additive around the existing params line; no conflict with the neighboring #39278 append point.
Extended reasoning...

Overview

This PR touches three files: packages/bun-types/bun.d.ts (widens FileSystemRouter.origin to string | null and MatchedRoute.query to Record<string, string | string[]>, adds JSDoc), docs/runtime/file-system-router.mdx (matching reference-block type plus two new examples), and test/integration/bun-types/fixture/fsrouter.ts (exact-type assertions via expectType(...).is<T>() and an Array.isArray narrowing check). No runtime code changes.

Verification

I read the native sources the PR cites. get_origin (filesystem_router.rs:662) returns JSValue::NULL when this.origin is None, and the constructor only stores non-empty origins, so null is the actual return value for the no-origin / empty-origin case. create_query_object calls putRecord, and JSC__JSObject__putRecord (bindings.cpp:2808) sets a jsString when valuesLen == 1 and a JSArray otherwise — so repeated query names do produce arrays. The PR's decision to leave MatchedRoute.params as Record<string, string> is defensible: catch-all params are joined strings, and the duplicate-[a]/[a] array case is a route-authoring error Next.js rejects.

Security risks

None. Type declarations and documentation only; no code path, parsing, or validation is altered.

Level of scrutiny

Low-to-medium on correctness (mechanical, source-verified), but packages/bun-types/ is under a CODEOWNERS entry and widening a public declaration is a breaking change for downstream TypeScript consumers — existing router.origin.length or match.query.foo.toUpperCase() call sites will start failing tsc. That is the intended outcome (those call sites already throw at runtime), and it mirrors Next.js's ParsedUrlQuery typing, but the widen-vs-change-runtime trade-off is an API design decision the code owner should confirm.

Other factors

The fixture uses the strict .is<T>() form so a future accidental narrowing or further widening will fail the bun-types test. The docs additions were spot-checked against the runtime probe in the PR description. The bug-hunting pass surfaced no findings. No prior review from me on this PR.

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