diff --git a/packages/vinext/src/build/report.ts b/packages/vinext/src/build/report.ts index b4d2c6135..d41e3760a 100644 --- a/packages/vinext/src/build/report.ts +++ b/packages/vinext/src/build/report.ts @@ -379,7 +379,7 @@ export function findDir(root: string, ...candidates: string[]): string | null { */ export async function printBuildReport(options: { root: string; - pageExtensions?: string[]; + pageExtensions: string[]; prerenderResult?: PrerenderResult; }): Promise { const { root } = options; diff --git a/packages/vinext/src/build/run-prerender.ts b/packages/vinext/src/build/run-prerender.ts index c84ae2b81..90745bfaa 100644 --- a/packages/vinext/src/build/run-prerender.ts +++ b/packages/vinext/src/build/run-prerender.ts @@ -192,7 +192,7 @@ export async function runPrerender(options: RunPrerenderOptions): Promise name.startsWith("_"), + )) { files.push(file); } } catch { diff --git a/tests/build-report.test.ts b/tests/build-report.test.ts index 76f79b4b7..3cd60950b 100644 --- a/tests/build-report.test.ts +++ b/tests/build-report.test.ts @@ -5,8 +5,10 @@ * logic for both Pages Router and App Router routes, using real fixture files * where integration testing is needed. */ -import { describe, it, expect } from "vite-plus/test"; +import { describe, it, expect, afterEach } from "vite-plus/test"; import path from "node:path"; +import os from "node:os"; +import fs from "node:fs/promises"; import { hasNamedExport, extractExportConstString, @@ -16,7 +18,10 @@ import { classifyAppRoute, buildReportRows, formatBuildReport, + printBuildReport, } from "../packages/vinext/src/build/report.js"; +import { invalidateAppRouteCache } from "../packages/vinext/src/routing/app-router.js"; +import { invalidateRouteCache } from "../packages/vinext/src/routing/pages-router.js"; const FIXTURES_PAGES = path.resolve("tests/fixtures/pages-basic/pages"); const FIXTURES_APP = path.resolve("tests/fixtures/app-basic/app"); @@ -453,3 +458,139 @@ describe("formatBuildReport", () => { expect(out).toContain("λ API ƒ Dynamic ◐ ISR ○ Static"); }); }); + +// ─── printBuildReport with pageExtensions ───────────────────────────────────── + +describe("printBuildReport respects pageExtensions", () => { + let tmpRoot: string; + + afterEach(async () => { + if (tmpRoot) { + // Invalidate both routers' caches — pages router tests set pagesDir at + // tmpRoot/pages, so we invalidate that path too. This ensures a failing + // test that skips its own finally-block cleanup doesn't pollute later tests. + invalidateAppRouteCache(); + invalidateRouteCache(path.join(tmpRoot, "pages")); + await fs.rm(tmpRoot, { recursive: true, force: true }); + } + }); + + it("app router: only reports routes matching configured pageExtensions", async () => { + // Ported from Next.js MDX e2e pageExtensions behaviour: + // test/e2e/app-dir/mdx/next.config.ts + // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/mdx/next.config.ts + tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "vinext-report-app-")); + const appDir = path.join(tmpRoot, "app"); + 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 {children}; }", + ); + await fs.writeFile( + path.join(appDir, "page.tsx"), + "export default function Page() { return
home
; }", + ); + // This .mdx page should be excluded when mdx is not in pageExtensions + await fs.writeFile(path.join(appDir, "about", "page.mdx"), "# About"); + + // Capture stdout output from printBuildReport + const lines: string[] = []; + const origLog = console.log; + console.log = (msg: string) => lines.push(msg); + try { + invalidateAppRouteCache(); + await printBuildReport({ root: tmpRoot, pageExtensions: ["tsx", "ts", "jsx", "js"] }); + } finally { + console.log = origLog; + } + + const output = lines.join("\n"); + // / should appear (page.tsx matches) + expect(output).toContain("/"); + // /about should NOT appear (page.mdx excluded — mdx not in pageExtensions) + expect(output).not.toContain("/about"); + }); + + it("app router: reports mdx routes when pageExtensions includes mdx", async () => { + tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "vinext-report-app-mdx-")); + const appDir = path.join(tmpRoot, "app"); + 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 {children}; }", + ); + await fs.writeFile( + path.join(appDir, "page.tsx"), + "export default function Page() { return
home
; }", + ); + await fs.writeFile(path.join(appDir, "about", "page.mdx"), "# About"); + + const lines: string[] = []; + const origLog = console.log; + console.log = (msg: string) => lines.push(msg); + try { + invalidateAppRouteCache(); + await printBuildReport({ root: tmpRoot, pageExtensions: ["tsx", "ts", "jsx", "js", "mdx"] }); + } finally { + console.log = origLog; + } + + const output = lines.join("\n"); + expect(output).toContain("/about"); + }); + + it("pages router: only reports routes matching configured pageExtensions", async () => { + tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "vinext-report-pages-")); + const pagesDir = path.join(tmpRoot, "pages"); + await fs.mkdir(pagesDir, { recursive: true }); + await fs.writeFile( + path.join(pagesDir, "index.tsx"), + "export default function Page() { return
home
; }", + ); + // This .mdx page should be excluded when mdx is not in pageExtensions + await fs.writeFile(path.join(pagesDir, "about.mdx"), "# About"); + + const lines: string[] = []; + const origLog = console.log; + console.log = (msg: string) => lines.push(msg); + try { + invalidateRouteCache(pagesDir); + await printBuildReport({ root: tmpRoot, pageExtensions: ["tsx", "ts", "jsx", "js"] }); + } finally { + console.log = origLog; + invalidateRouteCache(pagesDir); + } + + const output = lines.join("\n"); + expect(output).toContain("/"); + expect(output).not.toContain("/about"); + }); + + it("pages router: reports mdx routes when pageExtensions includes mdx", async () => { + tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "vinext-report-pages-mdx-")); + const pagesDir = path.join(tmpRoot, "pages"); + await fs.mkdir(pagesDir, { recursive: true }); + await fs.writeFile( + path.join(pagesDir, "index.tsx"), + "export default function Page() { return
home
; }", + ); + await fs.writeFile(path.join(pagesDir, "about.mdx"), "# About"); + + const lines: string[] = []; + const origLog = console.log; + console.log = (msg: string) => lines.push(msg); + try { + invalidateRouteCache(pagesDir); + await printBuildReport({ + root: tmpRoot, + pageExtensions: ["tsx", "ts", "jsx", "js", "mdx"], + }); + } finally { + console.log = origLog; + invalidateRouteCache(pagesDir); + } + + const output = lines.join("\n"); + expect(output).toContain("/about"); + }); +}); diff --git a/tests/page-extensions-routing.test.ts b/tests/page-extensions-routing.test.ts index 9176e3fbc..fa9d17d48 100644 --- a/tests/page-extensions-routing.test.ts +++ b/tests/page-extensions-routing.test.ts @@ -67,6 +67,30 @@ describe("pageExtensions route discovery", () => { } }); + 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");