From 7f46900f608586fd8909c2ae811645213e1d7972 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 19 Mar 2026 16:49:42 +0000 Subject: [PATCH 1/4] 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 --- packages/vinext/src/build/report.ts | 2 +- packages/vinext/src/build/run-prerender.ts | 4 +- packages/vinext/src/cli.ts | 13 +- packages/vinext/src/routing/pages-router.ts | 7 +- tests/build-report.test.ts | 139 +++++++++++++++++++- tests/page-extensions-routing.test.ts | 24 ++++ 6 files changed, 179 insertions(+), 10 deletions(-) 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..b79754c49 100644 --- a/packages/vinext/src/build/run-prerender.ts +++ b/packages/vinext/src/build/run-prerender.ts @@ -224,8 +224,8 @@ 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..6151c6d3e 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,135 @@ describe("formatBuildReport", () => { expect(out).toContain("λ API ƒ Dynamic ◐ ISR ○ Static"); }); }); + +// ─── printBuildReport with pageExtensions ───────────────────────────────────── + +describe("printBuildReport respects pageExtensions", () => { + let tmpRoot: string; + + afterEach(async () => { + if (tmpRoot) { + await fs.rm(tmpRoot, { recursive: true, force: true }); + invalidateAppRouteCache(); + } + }); + + 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"); From da203cd6001887e0cec73fdf5e0b8f0f688d0c0f Mon Sep 17 00:00:00 2001 From: "ask-bonk[bot]" Date: Thu, 19 Mar 2026 18:02:30 +0000 Subject: [PATCH 2/4] PR #591 reviewed: 1 bug found, rest LGTM Co-authored-by: james-elicx --- .../fumadocs-docs-template/.source/browser.ts | 12 ++++++++++ .../fumadocs-docs-template/.source/dynamic.ts | 8 +++++++ .../fumadocs-docs-template/.source/server.ts | 12 ++++++++++ .../.source/source.config.mjs | 24 +++++++++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 examples/fumadocs-docs-template/.source/browser.ts create mode 100644 examples/fumadocs-docs-template/.source/dynamic.ts create mode 100644 examples/fumadocs-docs-template/.source/server.ts create mode 100644 examples/fumadocs-docs-template/.source/source.config.mjs diff --git a/examples/fumadocs-docs-template/.source/browser.ts b/examples/fumadocs-docs-template/.source/browser.ts new file mode 100644 index 000000000..83189ec31 --- /dev/null +++ b/examples/fumadocs-docs-template/.source/browser.ts @@ -0,0 +1,12 @@ +// @ts-nocheck +import { browser } from 'fumadocs-mdx/runtime/browser'; +import type * as Config from '../source.config'; + +const create = browser(); +const browserCollections = { + docs: create.doc("docs", {"index.mdx": () => import("../content/docs/index.mdx?collection=docs"), "test.mdx": () => import("../content/docs/test.mdx?collection=docs"), }), +}; +export default browserCollections; \ No newline at end of file diff --git a/examples/fumadocs-docs-template/.source/dynamic.ts b/examples/fumadocs-docs-template/.source/dynamic.ts new file mode 100644 index 000000000..7dd9c10a6 --- /dev/null +++ b/examples/fumadocs-docs-template/.source/dynamic.ts @@ -0,0 +1,8 @@ +// @ts-nocheck +import { dynamic } from 'fumadocs-mdx/runtime/dynamic'; +import * as Config from '../source.config'; + +const create = await dynamic(Config, {"configPath":"source.config.ts","environment":"next","outDir":".source"}, {"doc":{"passthroughs":["extractedReferences"]}}); \ No newline at end of file diff --git a/examples/fumadocs-docs-template/.source/server.ts b/examples/fumadocs-docs-template/.source/server.ts new file mode 100644 index 000000000..b00d94bc9 --- /dev/null +++ b/examples/fumadocs-docs-template/.source/server.ts @@ -0,0 +1,12 @@ +// @ts-nocheck +import * as __fd_glob_1 from "../content/docs/test.mdx?collection=docs" +import * as __fd_glob_0 from "../content/docs/index.mdx?collection=docs" +import { server } from 'fumadocs-mdx/runtime/server'; +import type * as Config from '../source.config'; + +const create = server({"doc":{"passthroughs":["extractedReferences"]}}); + +export const docs = await create.docs("docs", "content/docs", {}, {"index.mdx": __fd_glob_0, "test.mdx": __fd_glob_1, }); \ No newline at end of file diff --git a/examples/fumadocs-docs-template/.source/source.config.mjs b/examples/fumadocs-docs-template/.source/source.config.mjs new file mode 100644 index 000000000..23a070bc9 --- /dev/null +++ b/examples/fumadocs-docs-template/.source/source.config.mjs @@ -0,0 +1,24 @@ +// source.config.ts +import { defineConfig, defineDocs } from "fumadocs-mdx/config"; +import { metaSchema, pageSchema } from "fumadocs-core/source/schema"; +var docs = defineDocs({ + dir: "content/docs", + docs: { + schema: pageSchema, + postprocess: { + includeProcessedMarkdown: true + } + }, + meta: { + schema: metaSchema + } +}); +var source_config_default = defineConfig({ + mdxOptions: { + // MDX options + } +}); +export { + source_config_default as default, + docs +}; From 0657cf582154589e36954ea646817d640d45b791 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 19 Mar 2026 18:03:18 +0000 Subject: [PATCH 3/4] remove files --- .../fumadocs-docs-template/.source/browser.ts | 12 ---------- .../fumadocs-docs-template/.source/dynamic.ts | 8 ------- .../fumadocs-docs-template/.source/server.ts | 12 ---------- .../.source/source.config.mjs | 24 ------------------- 4 files changed, 56 deletions(-) delete mode 100644 examples/fumadocs-docs-template/.source/browser.ts delete mode 100644 examples/fumadocs-docs-template/.source/dynamic.ts delete mode 100644 examples/fumadocs-docs-template/.source/server.ts delete mode 100644 examples/fumadocs-docs-template/.source/source.config.mjs diff --git a/examples/fumadocs-docs-template/.source/browser.ts b/examples/fumadocs-docs-template/.source/browser.ts deleted file mode 100644 index 83189ec31..000000000 --- a/examples/fumadocs-docs-template/.source/browser.ts +++ /dev/null @@ -1,12 +0,0 @@ -// @ts-nocheck -import { browser } from 'fumadocs-mdx/runtime/browser'; -import type * as Config from '../source.config'; - -const create = browser(); -const browserCollections = { - docs: create.doc("docs", {"index.mdx": () => import("../content/docs/index.mdx?collection=docs"), "test.mdx": () => import("../content/docs/test.mdx?collection=docs"), }), -}; -export default browserCollections; \ No newline at end of file diff --git a/examples/fumadocs-docs-template/.source/dynamic.ts b/examples/fumadocs-docs-template/.source/dynamic.ts deleted file mode 100644 index 7dd9c10a6..000000000 --- a/examples/fumadocs-docs-template/.source/dynamic.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -import { dynamic } from 'fumadocs-mdx/runtime/dynamic'; -import * as Config from '../source.config'; - -const create = await dynamic(Config, {"configPath":"source.config.ts","environment":"next","outDir":".source"}, {"doc":{"passthroughs":["extractedReferences"]}}); \ No newline at end of file diff --git a/examples/fumadocs-docs-template/.source/server.ts b/examples/fumadocs-docs-template/.source/server.ts deleted file mode 100644 index b00d94bc9..000000000 --- a/examples/fumadocs-docs-template/.source/server.ts +++ /dev/null @@ -1,12 +0,0 @@ -// @ts-nocheck -import * as __fd_glob_1 from "../content/docs/test.mdx?collection=docs" -import * as __fd_glob_0 from "../content/docs/index.mdx?collection=docs" -import { server } from 'fumadocs-mdx/runtime/server'; -import type * as Config from '../source.config'; - -const create = server({"doc":{"passthroughs":["extractedReferences"]}}); - -export const docs = await create.docs("docs", "content/docs", {}, {"index.mdx": __fd_glob_0, "test.mdx": __fd_glob_1, }); \ No newline at end of file diff --git a/examples/fumadocs-docs-template/.source/source.config.mjs b/examples/fumadocs-docs-template/.source/source.config.mjs deleted file mode 100644 index 23a070bc9..000000000 --- a/examples/fumadocs-docs-template/.source/source.config.mjs +++ /dev/null @@ -1,24 +0,0 @@ -// source.config.ts -import { defineConfig, defineDocs } from "fumadocs-mdx/config"; -import { metaSchema, pageSchema } from "fumadocs-core/source/schema"; -var docs = defineDocs({ - dir: "content/docs", - docs: { - schema: pageSchema, - postprocess: { - includeProcessedMarkdown: true - } - }, - meta: { - schema: metaSchema - } -}); -var source_config_default = defineConfig({ - mdxOptions: { - // MDX options - } -}); -export { - source_config_default as default, - docs -}; From 7c668a8e9ecbd5812d7de98e64129f1526176d44 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 19 Mar 2026 18:03:50 +0000 Subject: [PATCH 4/4] fix: pass pageExtensions to appRouter in prerender; harden afterEach cache cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- packages/vinext/src/build/run-prerender.ts | 2 +- tests/build-report.test.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/vinext/src/build/run-prerender.ts b/packages/vinext/src/build/run-prerender.ts index b79754c49..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 { afterEach(async () => { if (tmpRoot) { - await fs.rm(tmpRoot, { recursive: true, force: true }); + // 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 }); } });