135 lines
5.2 KiB
TypeScript
135 lines
5.2 KiB
TypeScript
import { test, expect } from "bun:test";
|
|
import {
|
|
buildRouter,
|
|
compileRoutePattern,
|
|
fileToRoute,
|
|
generateRoutesFile,
|
|
matchRoute,
|
|
sortRoutes,
|
|
type Route,
|
|
} from "../src/index.ts";
|
|
|
|
function route(raw: string): Route {
|
|
return { raw, file: raw, ...compileRoutePattern(raw) };
|
|
}
|
|
|
|
test("generateRoutesFile emits a Routes map with param types", () => {
|
|
const code = generateRoutesFile([
|
|
route("/"),
|
|
route("/about"),
|
|
route("/users/[id]"),
|
|
route("/docs/[[...slug]]"),
|
|
]);
|
|
expect(code).toContain('"/": Record<string, never>;');
|
|
expect(code).toContain('"/about": Record<string, never>;');
|
|
expect(code).toContain('"/users/[id]": { "id": string };');
|
|
expect(code).toContain('"/docs/[[...slug]]": { "slug"?: string | readonly string[] };');
|
|
expect(code).toContain("export function href");
|
|
});
|
|
|
|
test("generated href() fills required, optional and catch-all params", async () => {
|
|
const code = generateRoutesFile([
|
|
route("/"),
|
|
route("/users/[id]"),
|
|
route("/org/[org]/team/[team]"),
|
|
route("/blog/[[page]]"),
|
|
route("/docs/[...slug]"),
|
|
]);
|
|
const { tmpdir } = await import("node:os");
|
|
const { writeFileSync, mkdirSync } = await import("node:fs");
|
|
const { join } = await import("node:path");
|
|
const { pathToFileURL } = await import("node:url");
|
|
const dir = join(tmpdir(), "wrnexus-routes-test");
|
|
mkdirSync(dir, { recursive: true });
|
|
const file = join(dir, `r${Date.now()}.ts`);
|
|
writeFileSync(file, code);
|
|
const mod = (await import(pathToFileURL(file).href)) as {
|
|
href: (p: string, params?: Record<string, string | string[]>) => string;
|
|
};
|
|
expect(mod.href("/")).toBe("/");
|
|
expect(mod.href("/users/[id]", { id: "42" })).toBe("/users/42");
|
|
expect(mod.href("/org/[org]/team/[team]", { org: "acme", team: "core" })).toBe(
|
|
"/org/acme/team/core",
|
|
);
|
|
expect(mod.href("/users/[id]", { id: "a b" })).toBe("/users/a%20b");
|
|
expect(mod.href("/blog/[[page]]")).toBe("/blog");
|
|
expect(mod.href("/docs/[...slug]", { slug: ["guide", "start here"] })).toBe(
|
|
"/docs/guide/start%20here",
|
|
);
|
|
});
|
|
|
|
test("matches optional and catch-all routes", () => {
|
|
expect(matchRoute([route("/blog/[[page]]")], "/blog")?.params).toEqual({});
|
|
expect(matchRoute([route("/blog/[[page]]")], "/blog/2")?.params).toEqual({ page: "2" });
|
|
expect(matchRoute([route("/docs/[...slug]")], "/docs/a/b")?.params).toEqual({ slug: "a/b" });
|
|
expect(matchRoute([route("/docs/[[...slug]]")], "/docs")?.params).toEqual({});
|
|
});
|
|
|
|
test("static routes sort ahead of dynamic and catch-all routes", () => {
|
|
const sorted = sortRoutes([route("/docs/[...slug]"), route("/docs/[id]"), route("/docs/new")]);
|
|
expect(sorted.map((item) => item.raw)).toEqual(["/docs/new", "/docs/[id]", "/docs/[...slug]"]);
|
|
});
|
|
|
|
test("route groups and page/index filenames do not affect URLs", () => {
|
|
expect(fileToRoute("(marketing)/pricing/page.wrn")).toBe("/pricing");
|
|
expect(fileToRoute("(dashboard)/index.wrn")).toBe("/");
|
|
});
|
|
|
|
test("malformed encoded route params return no match instead of throwing", () => {
|
|
expect(matchRoute([route("/users/[id]")], "/users/%E0%A4%A")).toBeNull();
|
|
});
|
|
|
|
test("application routes override package routes with the same path", async () => {
|
|
const { mkdtempSync, mkdirSync, rmSync, writeFileSync } = await import("node:fs");
|
|
const { tmpdir } = await import("node:os");
|
|
const { join } = await import("node:path");
|
|
const root = mkdtempSync(join(tmpdir(), "wrnexus-router-override-"));
|
|
|
|
try {
|
|
const appDir = join(root, "app");
|
|
const appRoute = join(appDir, "pages", "account.ts");
|
|
const packageRoute = join(root, "package-account.ts");
|
|
mkdirSync(join(appDir, "pages"), { recursive: true });
|
|
writeFileSync(appRoute, "export default function account() {}\n");
|
|
writeFileSync(packageRoute, "export default function packageAccount() {}\n");
|
|
|
|
const router = buildRouter(appDir, {
|
|
externalRoutes: [{ kind: "page", path: "/account", entry: packageRoute }],
|
|
});
|
|
|
|
expect(router.matchPage("/account")?.route.file).toBe(appRoute);
|
|
expect(router.pages.filter((route) => route.raw === "/account")).toHaveLength(1);
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("components use declaration names with case-insensitive overrides", async () => {
|
|
const { mkdtempSync, mkdirSync, rmSync, writeFileSync } = await import("node:fs");
|
|
const { tmpdir } = await import("node:os");
|
|
const { join } = await import("node:path");
|
|
const root = mkdtempSync(join(tmpdir(), "wrnexus-router-components-"));
|
|
|
|
try {
|
|
const appDir = join(root, "app");
|
|
const componentsDir = join(root, "library-components");
|
|
mkdirSync(join(appDir, "pages"), { recursive: true });
|
|
mkdirSync(componentsDir, { recursive: true });
|
|
writeFileSync(join(appDir, "pages", "index.wrn"), "page Home { view { <p>Home</p> } }\n");
|
|
writeFileSync(
|
|
join(componentsDir, "button.wrn"),
|
|
'component Button { view { <button type="button">Button</button> } }\n',
|
|
);
|
|
|
|
const router = buildRouter(appDir, { componentDirs: [componentsDir] });
|
|
|
|
expect(router.components).toHaveLength(1);
|
|
expect(router.components[0]).toEqual({
|
|
name: "Button",
|
|
file: join(componentsDir, "button.wrn"),
|
|
});
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|