import { FileSystemRouter } from "bun"; import { it, expect } from "bun:test"; import path, { dirname, resolve } from "path"; import fs, { mkdirSync, realpathSync, rmSync } from "fs"; import { tmpdir } from "os"; const tempdir = realpathSync(tmpdir()) + "/"; function createTree(basedir, paths) { 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) { const dir = tempdir + `fs-router-test-${count++}`; 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`, ]); const router = new FileSystemRouter({ dir, fileExtensions: [".tsx"], style: "nextjs", }); const routes = router.routes; const fixture = { "/": `${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`, }; 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 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/"]) { expect(router.match(fixture)?.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: "/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: "/posts/hello-world" }), new Request({ url: "https://nextjs.org/posts/hello-world" }), ]) { const { name, src, filePath, 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, {}], ]) { const { name, src, filePath, 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(".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" }], ]) { const { name, src, filePath, 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`); } });