import { FileSystemRouter } from "bun"; import { expect, it } from "bun:test"; import fs, { mkdirSync, rmSync } from "fs"; import { tmpdirSync } from "harness"; import path, { dirname } from "path"; function createTree(basedir: string, paths: string[]) { for (const end of paths) { const abs = path.join(basedir, end); try { const dir = dirname(abs); if (dir.length > 0 && dir !== "/") fs.mkdirSync(dir, { recursive: true }); } catch (e) {} fs.writeFileSync(abs, "export default " + JSON.stringify(end) + ";\n"); } } var count = 0; function make(files: string[]) { const dir = tmpdirSync().replaceAll("\\", "/"); rmSync(dir, { recursive: true, force: true, }); createTree(dir, files); if (files.length === 0) mkdirSync(dir, { recursive: true }); return { dir, }; } it("should find files", () => { const { dir } = make([ `index.tsx`, `[id].tsx`, `a.tsx`, `abc/index.tsx`, `abc/[id].tsx`, `abc/def/[id].tsx`, `abc/def/ghi/index.tsx`, `abc/def/ghi/[id].tsx`, `abc/def/ghi/jkl/index.tsx`, `abc/def/ghi/jkl/[id].tsx`, `abc/def/index.tsx`, `b.tsx`, `foo/[id].tsx`, `catch-all/[[...id]].tsx`, // https://github.com/oven-sh/bun/issues/8276 // https://github.com/oven-sh/bun/issues/8278 ...Array.from({ length: 65 }, (_, i) => `files/a${i}.tsx`), ]); const router = new FileSystemRouter({ dir, fileExtensions: [".tsx"], style: "nextjs", }); const routes = router.routes; const fixture: Record = { "/": `${dir}/index.tsx`, "/[id]": `${dir}/[id].tsx`, "/a": `${dir}/a.tsx`, "/abc": `${dir}/abc/index.tsx`, "/abc/[id]": `${dir}/abc/[id].tsx`, "/abc/def/[id]": `${dir}/abc/def/[id].tsx`, "/abc/def/ghi": `${dir}/abc/def/ghi/index.tsx`, "/abc/def/ghi/[id]": `${dir}/abc/def/ghi/[id].tsx`, "/abc/def/ghi/jkl": `${dir}/abc/def/ghi/jkl/index.tsx`, "/abc/def/ghi/jkl/[id]": `${dir}/abc/def/ghi/jkl/[id].tsx`, "/abc/def": `${dir}/abc/def/index.tsx`, "/b": `${dir}/b.tsx`, "/foo/[id]": `${dir}/foo/[id].tsx`, "/catch-all/[[...id]]": `${dir}/catch-all/[[...id]].tsx`, // https://github.com/oven-sh/bun/issues/8276 // https://github.com/oven-sh/bun/issues/8278 ...Object.fromEntries(Array.from({ length: 65 }, (_, i) => [`/files/a${i}`, `${dir}/files/a${i}.tsx`])), }; for (const route in fixture) { if (!(route in routes)) { throw new Error(`Route ${route} not found`); } expect(routes[route]).toBe(fixture[route]); } expect(Object.keys(routes).length).toBe(Object.keys(fixture).length); expect(Object.values(routes).length).toBe(Object.values(fixture).length); }); it("should handle empty dirs", () => { const { dir } = make([]); const router = new FileSystemRouter({ dir, fileExtensions: [".tsx"], style: "nextjs", }); // assert this doesn't crash // @ts-ignore expect(router.bar).toBeUndefined(); const routes = router.routes; expect(Object.keys(routes).length).toBe(0); expect(Object.values(routes).length).toBe(0); }); it("should match dynamic routes", () => { // set up the test const { dir } = make(["index.tsx", "posts/[id].tsx", "posts.tsx"]); const router = new Bun.FileSystemRouter({ dir, style: "nextjs", }); const { name, filePath } = router.match("/posts/hello-world")!; expect(name).toBe("/posts/[id]"); expect(filePath).toBe(`${dir}/posts/[id].tsx`); }); it(".params works on dynamic routes", () => { // set up the test const { dir } = make(["index.tsx", "posts/[id].tsx", "posts.tsx"]); const router = new Bun.FileSystemRouter({ dir, style: "nextjs", }); const { params: { id }, } = router.match("/posts/hello-world")!; expect(id).toBe("hello-world"); }); it("should support static routes", () => { // set up the test const { dir } = make(["index.tsx", "posts/[id].tsx", "posts.tsx", "posts/hey.tsx"]); const router = new Bun.FileSystemRouter({ dir, style: "nextjs", }); const { name, params, filePath } = router.match("/posts/hey")!; expect(name).toBe("/posts/hey"); expect(filePath).toBe(`${dir}/posts/hey.tsx`); }); it("should support optional catch-all routes", () => { // set up the test const { dir } = make(["index.tsx", "posts/[id].tsx", "posts.tsx", "posts/hey.tsx", "posts/[[...id]].tsx"]); const router = new Bun.FileSystemRouter({ dir, style: "nextjs", }); for (let fixture of ["/posts/123", "/posts/hey", "/posts/zorp", "/posts", "/index", "/posts/"]) { expect(router.match(fixture)?.name).not.toBe("/posts/[[...id]]"); } for (let fixture of ["/posts/hey/there", "/posts/hey/there/you", "/posts/zorp/123"]) { const { name, params, filePath } = router.match(fixture)!; expect(name).toBe("/posts/[[...id]]"); expect(filePath).toBe(`${dir}/posts/[[...id]].tsx`); expect(params.id).toBe(fixture.split("/").slice(2).join("/")); } }); it("should support catch-all routes", () => { // set up the test const { dir } = make([ "index.tsx", "posts/[id].tsx", "posts.tsx", "posts/hey.tsx", "posts/[...id].tsx", "posts/wow/[[...id]].tsx", ]); const router = new Bun.FileSystemRouter({ dir, style: "nextjs", }); for (let fixture of ["/posts/123", "/posts/hey", "/posts/zorp", "/posts", "/index", "/posts/"]) { const match = router.match(fixture); expect(match?.name).not.toBe("/posts/[...id]"); } for (let fixture of ["/posts/hey/there", "/posts/hey/there/you", "/posts/zorp/123", "/posts/wow/hey/there"]) { const { name, params, filePath } = router.match(fixture)!; expect(name).toBe("/posts/[...id]"); expect(filePath).toBe(`${dir}/posts/[...id].tsx`); expect(params.id).toBe(fixture.split("/").slice(2).join("/")); } }); it("should support index routes", () => { // set up the test const { dir } = make(["index.tsx", "posts/[id].tsx", "posts.tsx", "posts/hey.tsx"]); const router = new Bun.FileSystemRouter({ dir, style: "nextjs", }); for (let route of ["/", "/index"]) { const { name, params, filePath } = router.match(route)!; expect(name).toBe("/"); expect(filePath).toBe(`${dir}/index.tsx`); expect(Object.keys(params).length).toBe(0); } for (let route of ["/posts", "/posts/index", "/posts/"]) { const { name, params, filePath } = router.match(route)!; expect(name).toBe("/posts"); expect(filePath).toBe(`${dir}/posts.tsx`); expect(Object.keys(params).length).toBe(0); } }); it("should support Request", async () => { // set up the test const { dir } = make(["index.tsx", "posts/[id].tsx", "posts.tsx"]); const router = new Bun.FileSystemRouter({ dir, style: "nextjs", }); for (let current of [ new Request({ url: "https://example.com123/posts/hello-world" }), new Request({ url: "http://example.com/posts/hello-world" }), ]) { const { name, params: { id }, filePath, } = router.match(current)!; expect(name).toBe("/posts/[id]"); expect(filePath).toBe(`${dir}/posts/[id].tsx`); expect(id).toBe("hello-world"); } }); it("assetPrefix, src, and origin", async () => { // set up the test const { dir } = make(["index.tsx", "posts/[id].tsx", "posts.tsx"]); const router = new Bun.FileSystemRouter({ dir, style: "nextjs", assetPrefix: "/_next/static/", origin: "https://nextjs.org", }); for (let current of [ // Reuqest new Request({ url: "http://helloooo.com/posts/hello-world" }), new Request({ url: "https://nextjs.org/posts/hello-world" }), ]) { const { name, src, filePath, // @ts-ignore checkThisDoesntCrash, } = router.match(current)!; expect(name).toBe("/posts/[id]"); // check nothing is weird on the MatchedRoute object expect(checkThisDoesntCrash).toBeUndefined(); expect(src).toBe("https://nextjs.org/_next/static/posts/[id].tsx"); expect(filePath).toBe(`${dir}/posts/[id].tsx`); } }); it(".query works", () => { // set up the test const { dir } = make(["posts.tsx"]); const router = new Bun.FileSystemRouter({ dir, style: "nextjs", assetPrefix: "/_next/static/", origin: "https://nextjs.org", }); for (let [current, object] of [ [new URL("https://example.com/posts?hello=world").href, { hello: "world" }], [new URL("https://example.com/posts?hello=world&second=2").href, { hello: "world", second: "2" }], [ new URL("https://example.com/posts?hello=world&second=2&third=3").href, { hello: "world", second: "2", third: "3" }, ], [new URL("https://example.com/posts").href, {}], ] as const) { const { name, src, filePath, // @ts-ignore checkThisDoesntCrash, query, } = router.match(current)!; expect(name).toBe("/posts"); // check nothing is weird on the MatchedRoute object expect(checkThisDoesntCrash).toBeUndefined(); expect(JSON.stringify(query)).toBe(JSON.stringify(object)); expect(filePath).toBe(`${dir}/posts.tsx`); } }); it("reload() works", () => { // set up the test const { dir } = make(["posts.tsx"]); const router = new Bun.FileSystemRouter({ dir, style: "nextjs", assetPrefix: "/_next/static/", origin: "https://nextjs.org", }); expect(router.match("/posts")!.name).toBe("/posts"); router.reload(); expect(router.match("/posts")!.name).toBe("/posts"); }); it("reload() works with new dirs/files", () => { const { dir } = make(["posts.tsx"]); const router = new Bun.FileSystemRouter({ dir, style: "nextjs", assetPrefix: "/_next/static/", origin: "https://nextjs.org", }); expect(router.match("/posts")!.name).toBe("/posts"); createTree(dir, ["test/recursive/index.ts"]); router.reload(); expect(router.match("/test/recursive")!.name).toBe("/test/recursive"); rmSync(`${dir}/test/recursive`, { recursive: true, force: true, }); router.reload(); expect(router.match("/test/recursive")).toBe(null); createTree(dir, ["test/test2/index.ts"]); router.reload(); expect(router.match("/test/test2")!.name).toBe("/test/test2"); }); it(".query works with dynamic routes, including params", () => { // set up the test const { dir } = make(["posts/[id].tsx"]); const router = new Bun.FileSystemRouter({ dir, style: "nextjs", assetPrefix: "/_next/static/", origin: "https://nextjs.org", }); for (let [current, object] of [ [new URL("https://example.com/posts/123?hello=world").href, { id: "123", hello: "world" }], [new URL("https://example.com/posts/123?hello=world&second=2").href, { id: "123", hello: "world", second: "2" }], [ new URL("https://example.com/posts/123?hello=world&second=2&third=3").href, { id: "123", hello: "world", second: "2", third: "3" }, ], [new URL("https://example.com/posts/123").href, { id: "123" }], ] as const) { const { name, src, filePath, // @ts-ignore checkThisDoesntCrash, query, } = router.match(current)!; expect(name).toBe("/posts/[id]"); // check nothing is weird on the MatchedRoute object expect(checkThisDoesntCrash).toBeUndefined(); expect(JSON.stringify(query)).toBe(JSON.stringify(object)); expect(filePath).toBe(`${dir}/posts/[id].tsx`); } }); it("dir should be validated", async () => { expect(() => { //@ts-ignore new Bun.FileSystemRouter({ style: "nextjs", }); }).toThrow("Expected dir to be a string"); expect(() => { new Bun.FileSystemRouter({ //@ts-ignore dir: undefined, style: "nextjs", }); }).toThrow("Expected dir to be a string"); expect(() => { new Bun.FileSystemRouter({ //@ts-ignore dir: 123, style: "nextjs", }); }).toThrow("Expected dir to be a string"); }); it("origin should be validated", async () => { const { dir } = make(["posts.tsx"]); expect(() => { new Bun.FileSystemRouter({ dir, //@ts-ignore origin: 123, style: "nextjs", }); }).toThrow("Expected origin to be a string"); });