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`);
}
});
at/streaming-rendering
feat/style-obj
feat/test-utils
feat/xray-improvements
fetch-astro-pages-mvp
fix-408
fix-all-pages-key
fix-beta-ref
fix-create-ref
fix-netlify-edge
fix-next-basics
fix-nullish-slot-name
fix-s-island-fallback
fix-vite-asset
fix/actions-cookies
fix/actions-pending-timeout
fix/assets-types
fix/astro-config-refresh
fix/astro-html-escape-bug
fix/build-subpaths
fix/client-only-component-css
fix/client-scripts-windows
fix/config-migration-defaults
fix/container-directives
fix/dates
fix/db-integration-with-missing-config
fix/devtoolbar-data-unset
fix/empty-slots
fix/filepath-layer
fix/frontmatter-file-url
fix/head-propagation
fix/hmr-css-deps
fix/import-ts-errors
fix/main-build-failure
fix/map-file-404-logs
fix/mdx-named-slots
fix/middleware-import
fix/multi-images
fix/nested-get-collection-call
fix/preact-package-build-failure
fix/primary-key-optional
fix/regex-flags
fix/server-headers
fix/stable-renderer-order
fix/transaction-type
fix/vue-nested
fix/webapi-dev
fork/markdoc-poc-with-md-support
fork/markdoc-poc-with-parser
format-imports-run
formatting
forward-button
framework-agnostic-astro-components
fryuni/db-pluggable-backend
fryuni/test-route-setup-hook
fryuni/tracing-hooks
hippotastic/legitimate-bat
hoisted-script-ts
host-ssr-example-2
hostfornode
image-non-node
improve-base-handling
inline-hoisted-scripts-now
jn.convert-assertions-to-query-params
latest
live-loaders
main
mandar1jn/ci-repo-check
markdoc-embed-prototyping
markdown
markdown-poc
mdx-path
mk/render-slot-template-backup
move-default-md-code-component
mt/lit-DSD
mt/lit-regen
mt/parse-DSD
mt/router_refactoring
nate/new-blog-template
netlify-1
netlify-preview
new-adapter-api
next
next-render
no-more-vite-postprocess
no-more-vite-postprocess2
old-build
plt-1006/unified-and-mdx
plt-1768-trailing-slash-object
preact-shared-signals
process-env-override
progress-log
re-export-drivers
react-fast-refresh
redirects-priority2
redirects-ssg-object
refactor-how-client-directives-work
refactor/image-internals
refactor/markdoc-renderer
refactor/rendere-queue
refactor/sitemap
refactor/ssr-size
release/0.17
release/0.18
remote-cdn-link
remove-fs-abstraction
remove-start
restart-on-lock
revert-13008-renovate/all-minor-patch
revert-lockfile
route-manifest-adapter
sarah11918-image-errors
sarah11918-patch-2
sb-tests2
seroval
server-islands-children
session-docs
single-file-build-2
slash-404-hint
slot-bug-1
solid-ecosystem-pkg
spike/app-setup
spike/autonav
spike/codehike
spike/context
spike/csr
spike/default-content
spike/incremental
spike/incremental-ii
spike/markdown-wasm
spike/render
spike/streaming
spike/svg
sqlite-test
squeal
ssr-redirect
stream-buffer
streaming
telemetry-audit-1
test/new-integrations-demo
test/new-ssr-demo
top-level-exports-integrations
ts-in-hoisted-script
ts-no-err
upd-vite-vendored
upgrade-deps
v1-beta
vercel-test
vite-fork
vscode-astro-global
vt-follow-redirects
warn-exp-flag
win
windows-tests-beta
wip-assets
wip-component-api-2
wip-docs-components
wip-docs-reference-gen
wip-fetch-cache
wip-fun-flags
wip-icons
wip-logging
wip-logging-saved
wip-mdc
wip-mdx-to-astro-js
wip-preview-command-integrations
wip-setup-content
wip-smoke
wip-speed-up-markdown
wip-stage
wip/react-19-test
Unnamed repository; edit this file 'description' to name the repository.
Age Commit message (Collapse ) Author Files Lines
server in preview mode (#10208)
* fix(node): add user specified headers to preview server responses
* docs: clarify comment
* style: new line
* test: remove test
* chore: add changeset