From 0c15c4a82c7742499f21e5ba87dfa5c5e8444e8e Mon Sep 17 00:00:00 2001 From: Sunil Pai Date: Tue, 24 Feb 2026 16:36:10 +0000 Subject: [PATCH 1/2] fix: exclude vinext from optimizeDeps to prevent virtual module resolution errors When vinext is installed from npm (not symlinked), esbuild's dependency optimization scans vinext/dist/ and hits virtual:vinext-* imports that only exist at Vite plugin resolution time. This causes build failures in all three environments (client, rsc, ssr). Adding optimizeDeps.exclude: ['vinext'] at the top level and in each environment prevents esbuild from scanning vinext's dist files entirely. Fixes #1 --- packages/vinext/src/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) 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 From 2f3c2bf179c0ee12ba9ea57ab262d86eb6311a39 Mon Sep 17 00:00:00 2001 From: Sunil Pai Date: Tue, 24 Feb 2026 16:40:41 +0000 Subject: [PATCH 2/2] test: verify optimizeDeps.exclude contains vinext for all environments --- tests/build-optimization.test.ts | 87 ++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) 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", () => {