Files
cloudflare__vinext/tests/page-extensions-routing.test.ts
T
James Anderson ea08183af2 fix: respect pageExtensions in prerender and exclude _ prefixed api files (#591)
* fix: respect pageExtensions in prerender and exclude _ prefixed api files

- cli.ts: hoist resolveNextConfig so pageExtensions is passed to printBuildReport
- run-prerender.ts: pass config.pageExtensions to pagesRouter/apiRouter (previously defaulted to ["tsx","ts","jsx","js"] regardless of next.config.js)
- pages-router.ts (scanApiRoutes): add _ prefix exclude, matching pages/ behaviour
- report.ts: tighten printBuildReport signature (pageExtensions required, not optional)
- tests: add coverage for _ prefix exclusion in pages/api and pageExtensions filtering

* PR #591 reviewed: 1 bug found, rest LGTM

Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>

* remove files

* fix: pass pageExtensions to appRouter in prerender; harden afterEach cache cleanup

- run-prerender.ts: appRouter(appDir) was missing config.pageExtensions, same
  class of bug as the pagesRouter/apiRouter fix — App Router prerender would
  scan with default extensions regardless of next.config.js
- build-report.test.ts: afterEach now invalidates both app and pages route caches
  so a test failure can't leak cached routes into subsequent tests

---------

Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com>
Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>
2026-03-19 18:10:31 +00:00

121 lines
4.8 KiB
TypeScript

import path from "node:path";
import os from "node:os";
import fs from "node:fs/promises";
import { describe, expect, it } from "vite-plus/test";
import { appRouter, invalidateAppRouteCache } from "../packages/vinext/src/routing/app-router.js";
import {
apiRouter,
invalidateRouteCache,
pagesRouter,
} from "../packages/vinext/src/routing/pages-router.js";
async function makeTempDir(prefix: string): Promise<string> {
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
}
describe("pageExtensions route discovery", () => {
it("discovers app/page.mdx routes when pageExtensions includes mdx", async () => {
// Ported from Next.js MDX e2e setup:
// test/e2e/app-dir/mdx/next.config.ts
// https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/mdx/next.config.ts
const tmpRoot = await makeTempDir("vinext-app-ext-mdx-");
const appDir = path.join(tmpRoot, "app");
try {
await fs.mkdir(path.join(appDir, "about"), { recursive: true });
await fs.writeFile(
path.join(appDir, "layout.tsx"),
"export default function Layout({ children }: { children: React.ReactNode }) { return <html><body>{children}</body></html>; }",
);
await fs.writeFile(
path.join(appDir, "page.tsx"),
"export default function Page() { return <div>home</div>; }",
);
await fs.writeFile(path.join(appDir, "about", "page.mdx"), "# About");
invalidateAppRouteCache();
const routes = await appRouter(appDir, ["tsx", "ts", "jsx", "js", "mdx"]);
const patterns = routes.filter((r) => r.pagePath).map((r) => r.pattern);
expect(patterns).toContain("/about");
} finally {
await fs.rm(tmpRoot, { recursive: true, force: true });
invalidateAppRouteCache();
}
});
it("does not discover app/page.mdx routes when mdx is not configured", async () => {
const tmpRoot = await makeTempDir("vinext-app-ext-no-mdx-");
const appDir = path.join(tmpRoot, "app");
try {
await fs.mkdir(path.join(appDir, "about"), { recursive: true });
await fs.writeFile(
path.join(appDir, "layout.tsx"),
"export default function Layout({ children }: { children: React.ReactNode }) { return <html><body>{children}</body></html>; }",
);
await fs.writeFile(
path.join(appDir, "page.tsx"),
"export default function Page() { return <div>home</div>; }",
);
await fs.writeFile(path.join(appDir, "about", "page.mdx"), "# About");
invalidateAppRouteCache();
const routes = await appRouter(appDir, ["tsx", "ts", "jsx", "js"]);
const patterns = routes.filter((r) => r.pagePath).map((r) => r.pattern);
expect(patterns).not.toContain("/about");
} finally {
await fs.rm(tmpRoot, { recursive: true, force: true });
invalidateAppRouteCache();
}
});
it("excludes _ prefixed files from api routes", async () => {
// Next.js ignores _-prefixed files in pages/api/ the same way it does in pages/.
const tmpRoot = await makeTempDir("vinext-api-underscore-");
const pagesDir = path.join(tmpRoot, "pages");
try {
await fs.mkdir(path.join(pagesDir, "api"), { recursive: true });
await fs.writeFile(
path.join(pagesDir, "api", "hello.ts"),
"export async function GET() { return Response.json({}); }",
);
await fs.writeFile(path.join(pagesDir, "api", "_helpers.ts"), "// internal helper");
invalidateRouteCache(pagesDir);
const apiRoutes = await apiRouter(pagesDir);
const patterns = apiRoutes.map((r) => r.pattern);
expect(patterns).toContain("/api/hello");
expect(patterns).not.toContain("/api/_helpers");
} finally {
await fs.rm(tmpRoot, { recursive: true, force: true });
invalidateRouteCache(pagesDir);
}
});
it("discovers pages and api files using configured pageExtensions", async () => {
const tmpRoot = await makeTempDir("vinext-pages-ext-mdx-");
const pagesDir = path.join(tmpRoot, "pages");
try {
await fs.mkdir(path.join(pagesDir, "api"), { recursive: true });
await fs.writeFile(
path.join(pagesDir, "index.tsx"),
"export default function Page() { return <div>home</div>; }",
);
await fs.writeFile(path.join(pagesDir, "about.mdx"), "# About");
await fs.writeFile(
path.join(pagesDir, "api", "hello.mdx"),
"export default function handler() {}",
);
invalidateRouteCache(pagesDir);
const pageRoutes = await pagesRouter(pagesDir, ["tsx", "ts", "jsx", "js", "mdx"]);
const apiRoutes = await apiRouter(pagesDir, ["tsx", "ts", "jsx", "js", "mdx"]);
expect(pageRoutes.map((r) => r.pattern)).toContain("/about");
expect(apiRoutes.map((r) => r.pattern)).toContain("/api/hello");
} finally {
await fs.rm(tmpRoot, { recursive: true, force: true });
invalidateRouteCache(pagesDir);
}
});
});