Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion docs/runtime/file-system-router.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ router.match("/settings?foo=bar");
}
```

A name that appears more than once in the query string maps to an array of its values.

```ts
router.match("/settings?foo=bar&foo=baz&page=2").query;

// =>
{
foo: ["bar", "baz"],
page: "2"
}
```

The router parses URL parameters and returns them in the `params` property:

```ts
Expand All @@ -77,6 +89,18 @@ router.match("/blog/my-cool-post");
}
```

`query` also contains the route parameters. A route parameter wins over a query string entry with the same name.

```ts
router.match("/blog/my-cool-post?slug=other&page=2").query;

// =>
{
slug: "my-cool-post",
page: "2"
}
```

The `.match()` method also accepts `Request` and `Response` objects; the router uses their `url` property to resolve the route.

```ts
Expand Down Expand Up @@ -111,7 +135,7 @@ interface Bun {
pathname: string;
src: string;
params?: Record<string, string>;
query?: Record<string, string>;
query?: Record<string, string | string[]>;
} | null
}
}
Expand Down
25 changes: 23 additions & 2 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8390,7 +8390,11 @@ declare module "bun" {
match(input: string | Request | Response): MatchedRoute | null;

readonly assetPrefix: string;
readonly origin: string;
/**
* The `origin` passed to the constructor. `null` when the router was
* created without one (or with `""`).
*/
readonly origin: string | null;
readonly style: string;
readonly routes: Record<string, string>;

Expand All @@ -8417,7 +8421,24 @@ declare module "bun" {
readonly params: Record<string, string>;
readonly filePath: string;
readonly pathname: string;
readonly query: Record<string, string>;
/**
* The parsed query string, merged with {@link MatchedRoute.params}. A
* route parameter wins over a query string entry of the same name. A
* name that appears more than once in the query string maps to an array
* of its values.
*
* @example
* ```ts
* // with a pages/blog/[slug].tsx route:
* const router = new FileSystemRouter({
* dir: "/path/to/pages",
* style: "nextjs",
* });
* router.match("/blog/hello?tag=a&tag=b&page=2")?.query;
* // { slug: "hello", tag: ["a", "b"], page: "2" }
* ```
*/
readonly query: Record<string, string | string[]>;
readonly name: string;
readonly kind: "exact" | "catch-all" | "optional-catch-all" | "dynamic";
readonly src: string;
Expand Down
31 changes: 31 additions & 0 deletions test/integration/bun-types/bun-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,37 @@ describe("@types/bun integration test", () => {
});
});

// Also runs on debug builds, where the typeTest cases above are skipped. The assertions
// live in fixture/fsrouter.ts; this only points tsc at that one file.
describe("Bun.FileSystemRouter", () => {
test("fixture/fsrouter.ts type-checks against the packed declarations", async () => {
const checkDir = join(TEMP_DIR, "fsrouter-check");
const tsconfig = structuredClone(sourceTsconfig);
tsconfig.include = ["fsrouter.ts"];
tsconfig.compilerOptions.typeRoots = [join(BASE_FIXTURE_DIR, "node_modules", "@types")];
await mkdir(checkDir, { recursive: true });
await makeTree(checkDir, {
"tsconfig.json": JSON.stringify(tsconfig, null, 2),
"fsrouter.ts": await Bun.file(join(BASE_FIXTURE_DIR, "fsrouter.ts")).text(),
"utilities.ts": await Bun.file(join(BASE_FIXTURE_DIR, "utilities.ts")).text(),
});

await using proc = Bun.spawn({
cmd: [bunExe(), join(BASE_FIXTURE_DIR, "node_modules", "typescript", "bin", "tsc"), "-p", "."],
env: bunEnv,
cwd: checkDir,
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr.trim()).toBe("");
expect(stdout.trim()).toBe("");
expect(exitCode).toBe(0);
});
});

describe("Test Globals", () => {
const code = `
const test_shouldBeAFunction: Function = test;
Expand Down
13 changes: 11 additions & 2 deletions test/integration/bun-types/fixture/fsrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,17 @@ const router = new FileSystemRouter({
style: "nextjs",
});

// null when the router was constructed without an origin.
expectType(router.origin).is<string | null>();

const match = router.match("/");
expectType<string>(match?.name!);
expectType<string>(match?.pathname!);
expectType<Record<string, string>>(match?.query!);
expectType<Record<string, string>>(match?.params!);
// A query string name given more than once maps to an array of its values.
expectType(match?.query!).is<Record<string, string | string[]>>();
for (const value of Object.values(match!.query)) {
if (Array.isArray(value)) expectType(value).is<string[]>();
else expectType(value).is<string>();
}
// Route parameters stay single strings.
expectType(match?.params!).is<Record<string, string>>();
Loading