diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 347d7d6775..ce4a9e54fd 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -1960,6 +1960,12 @@ hydrate(); "react/jsx-dev-runtime", ], }, + // Exclude vinext from dependency optimization so esbuild doesn't + // scan dist files containing virtual module imports (virtual:vinext-*) + // that only resolve at Vite plugin time, not during pre-bundling. + optimizeDeps: { + exclude: ["vinext"], + }, // Enable JSX in .tsx/.jsx files // Vite 7 uses `esbuild` for transforms, Vite 8+ uses `oxc` ...(getViteMajorVersion() >= 8 @@ -2004,6 +2010,7 @@ hydrate(); }, }), optimizeDeps: { + exclude: ["vinext"], entries: appEntries, }, build: { @@ -2015,6 +2022,7 @@ hydrate(); }, ssr: { optimizeDeps: { + exclude: ["vinext"], entries: appEntries, }, build: { @@ -2026,6 +2034,7 @@ hydrate(); }, client: { optimizeDeps: { + exclude: ["vinext"], // react and react-dom are framework dependencies used for // hydration. They aren't crawled from app/ source files so // must be pre-included to prevent late discovery and page diff --git a/tests/build-optimization.test.ts b/tests/build-optimization.test.ts index 1c41cc378a..0968f2016d 100644 --- a/tests/build-optimization.test.ts +++ b/tests/build-optimization.test.ts @@ -65,6 +65,93 @@ describe("clientManualChunks", () => { }); }); +// ─── optimizeDeps.exclude — prevents esbuild scanning virtual module imports ─ + +describe("optimizeDeps.exclude for vinext", () => { + it("excludes vinext at top level for Pages Router builds", async () => { + const vinext = (await import("../packages/vinext/src/index.js")).default; + const plugins = vinext(); + + const mainPlugin = plugins.find( + (p: any) => p.name === "vinext:config" && typeof p.config === "function", + ); + expect(mainPlugin).toBeDefined(); + + const os = await import("node:os"); + const fsp = await import("node:fs/promises"); + const path = await import("node:path"); + + const tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-ts-test-optdeps-")); + const rootNodeModules = path.resolve(import.meta.dirname, "../node_modules"); + await fsp.symlink(rootNodeModules, path.join(tmpDir, "node_modules"), "junction"); + + await fsp.mkdir(path.join(tmpDir, "pages"), { recursive: true }); + await fsp.writeFile( + path.join(tmpDir, "pages", "index.tsx"), + `export default function Home() { return

Home

; }`, + ); + await fsp.writeFile( + path.join(tmpDir, "next.config.mjs"), + `export default {};`, + ); + + try { + const mockConfig = { root: tmpDir, build: {}, plugins: [] }; + const result = await (mainPlugin as any).config(mockConfig, { command: "build" }); + + expect(result.optimizeDeps?.exclude).toContain("vinext"); + } finally { + await fsp.rm(tmpDir, { recursive: true, force: true }).catch(() => {}); + } + }, 15000); + + it("excludes vinext in all environments for App Router builds", async () => { + const vinext = (await import("../packages/vinext/src/index.js")).default; + const plugins = vinext(); + + const mainPlugin = plugins.find( + (p: any) => p.name === "vinext:config" && typeof p.config === "function", + ); + expect(mainPlugin).toBeDefined(); + + const os = await import("node:os"); + const fsp = await import("node:fs/promises"); + const path = await import("node:path"); + + const tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-ts-test-optdeps-app-")); + const rootNodeModules = path.resolve(import.meta.dirname, "../node_modules"); + await fsp.symlink(rootNodeModules, path.join(tmpDir, "node_modules"), "junction"); + + await fsp.mkdir(path.join(tmpDir, "app"), { recursive: true }); + await fsp.writeFile( + path.join(tmpDir, "app", "layout.tsx"), + `export default function RootLayout({ children }: { children: React.ReactNode }) { return {children}; }`, + ); + await fsp.writeFile( + path.join(tmpDir, "app", "page.tsx"), + `export default function Home() { return

Home

; }`, + ); + await fsp.writeFile( + path.join(tmpDir, "next.config.mjs"), + `export default {};`, + ); + + try { + const mockConfig = { root: tmpDir, build: {}, plugins: [] }; + const result = await (mainPlugin as any).config(mockConfig, { command: "build" }); + + // Top-level + expect(result.optimizeDeps?.exclude).toContain("vinext"); + // Per-environment + expect(result.environments.rsc.optimizeDeps?.exclude).toContain("vinext"); + expect(result.environments.ssr.optimizeDeps?.exclude).toContain("vinext"); + expect(result.environments.client.optimizeDeps?.exclude).toContain("vinext"); + } finally { + await fsp.rm(tmpDir, { recursive: true, force: true }).catch(() => {}); + } + }, 15000); +}); + // ─── Treeshake config applied to Vite builds ────────────────────────────────── describe("treeshake config integration", () => {