Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/vinext/src/build/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
const { root } = options;
Expand Down
6 changes: 3 additions & 3 deletions packages/vinext/src/build/run-prerender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export async function runPrerender(options: RunPrerenderOptions): Promise<Preren

// ── App Router phase ──────────────────────────────────────────────────────
if (appDir) {
const routes = await appRouter(appDir);
const routes = await appRouter(appDir, config.pageExtensions);

// We don't know the exact render-queue size until prerenderApp starts, so
// use the progress callback's `total` to update our combined total on the
Expand Down Expand Up @@ -224,8 +224,8 @@ export async function runPrerender(options: RunPrerenderOptions): Promise<Preren
// ── Pages Router phase ────────────────────────────────────────────────────
if (pagesDir) {
const [pageRoutes, apiRoutes] = await Promise.all([
pagesRouter(pagesDir),
apiRouter(pagesDir),
pagesRouter(pagesDir, config.pageExtensions),
apiRouter(pagesDir, config.pageExtensions),
]);

let pagesTotal = 0;
Expand Down
13 changes: 8 additions & 5 deletions packages/vinext/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,11 +495,10 @@ async function buildApp() {
);
}

const nextConfig = await resolveNextConfig(await loadNextConfig(process.cwd()), process.cwd());

let prerenderResult;
const shouldPrerender =
parsed.prerenderAll ||
(await resolveNextConfig(await loadNextConfig(process.cwd()), process.cwd())).output ===
"export";
const shouldPrerender = parsed.prerenderAll || nextConfig.output === "export";

if (shouldPrerender) {
const label = parsed.prerenderAll
Expand All @@ -511,7 +510,11 @@ async function buildApp() {
}

process.stdout.write("\x1b[0m");
await printBuildReport({ root: process.cwd(), prerenderResult: prerenderResult ?? undefined });
await printBuildReport({
root: process.cwd(),
pageExtensions: nextConfig.pageExtensions,
prerenderResult: prerenderResult ?? undefined,
});

console.log("\n Build complete. Run `vinext start` to start the production server.\n");
process.exit(0);
Expand Down
7 changes: 6 additions & 1 deletion packages/vinext/src/routing/pages-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,12 @@ async function scanApiRoutes(pagesDir: string, matcher: ValidFileMatcher): Promi
let files: string[];
try {
files = [];
for await (const file of scanWithExtensions("**/*", apiDir, matcher.extensions)) {
for await (const file of scanWithExtensions(
"**/*",
apiDir,
matcher.extensions,
(name: string) => name.startsWith("_"),
)) {
files.push(file);
}
} catch {
Expand Down
143 changes: 142 additions & 1 deletion tests/build-report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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");
Expand Down Expand Up @@ -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 <html><body>{children}</body></html>; }",
);
await fs.writeFile(
path.join(appDir, "page.tsx"),
"export default function Page() { return <div>home</div>; }",
);
// 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 <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");

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 <div>home</div>; }",
);
// 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 <div>home</div>; }",
);
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");
});
});
24 changes: 24 additions & 0 deletions tests/page-extensions-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading