From 3fb524c354fb07437f8e6375458a00240c0eee08 Mon Sep 17 00:00:00 2001 From: MD YUNUS Date: Sun, 15 Mar 2026 15:38:36 +0530 Subject: [PATCH 01/11] fix(vite-8): remove deprecated Rollup config options for Vite 8/Rolldown --- packages/vinext/src/cli.ts | 7 +++-- packages/vinext/src/index.ts | 54 +++++++++++++++++++++++++++++++----- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/packages/vinext/src/cli.ts b/packages/vinext/src/cli.ts index 88598bb0a..608dc82f9 100644 --- a/packages/vinext/src/cli.ts +++ b/packages/vinext/src/cli.ts @@ -13,7 +13,7 @@ * needed for most Next.js apps. */ -import vinext, { clientOutputConfig, clientTreeshakeConfig } from "./index.js"; +import vinext, { getClientOutputConfig, getClientTreeshakeConfig } from "./index.js"; import { printBuildReport } from "./build/report.js"; import path from "node:path"; import fs from "node:fs"; @@ -350,6 +350,7 @@ async function buildApp() { console.log(`\n vinext build (Vite ${getViteVersion()})\n`); const isApp = hasAppDir(); + const viteMajorVersion = parseInt(getViteVersion().split(".")[0], 10); // In verbose mode, skip the custom logger so raw Vite/Rollup output is shown. const logger = parsed.verbose ? vite.createLogger("info", { allowClearScreen: false }) @@ -387,8 +388,8 @@ async function buildApp() { ssrManifest: true, rollupOptions: { input: "virtual:vinext-client-entry", - output: clientOutputConfig, - treeshake: clientTreeshakeConfig, + output: getClientOutputConfig(viteMajorVersion), + treeshake: getClientTreeshakeConfig(viteMajorVersion), }, }, }, diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 50da4b418..513258836 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -525,6 +525,37 @@ const clientTreeshakeConfig = { moduleSideEffects: "no-external" as const, }; +/** + * Get Rollup-compatible output config for client builds. + * Returns config without Vite 8/Rolldown-incompatible options. + */ +function getClientOutputConfig(viteVersion: number) { + if (viteVersion >= 8) { + // Vite 8+ uses Rolldown which doesn't support experimentalMinChunkSize + return { + manualChunks: clientManualChunks, + }; + } + // Vite 7 uses Rollup with experimentalMinChunkSize support + return clientOutputConfig; +} + +/** + * Get Rollup-compatible treeshake config for client builds. + * Returns config without Vite 8/Rolldown-incompatible options. + */ +function getClientTreeshakeConfig(viteVersion: number) { + if (viteVersion >= 8) { + // Vite 8+ uses Rolldown which doesn't support `preset` option + // moduleSideEffects is still supported in Rolldown + return { + moduleSideEffects: "no-external" as const, + }; + } + // Vite 7 uses Rollup with preset support + return clientTreeshakeConfig; +} + type BuildManifestChunk = { file: string; isEntry?: boolean; @@ -1232,7 +1263,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // avoid leaking into RSC/SSR environments where // moduleSideEffects: 'no-external' could drop server packages // that rely on module-level side effects. - ...(!isSSR && !isMultiEnv ? { treeshake: clientTreeshakeConfig } : {}), + ...(!isSSR && !isMultiEnv + ? { treeshake: getClientTreeshakeConfig(viteMajorVersion) } + : {}), // Code-split client bundles: separate framework (React/ReactDOM), // vinext runtime (shims), and vendor packages into their own // chunks so pages only load the JS they need. @@ -1240,7 +1273,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Router). For multi-environment builds (App Router, Cloudflare), // manualChunks is set per-environment on the client env below // to avoid leaking into RSC/SSR environments. - ...(!isSSR && !isMultiEnv ? { output: clientOutputConfig } : {}), + ...(!isSSR && !isMultiEnv ? { output: getClientOutputConfig(viteMajorVersion) } : {}), }, }, // Let OPTIONS requests pass through Vite's CORS middleware to our @@ -1444,8 +1477,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { ...(hasCloudflarePlugin ? { manifest: true } : {}), rollupOptions: { input: { index: VIRTUAL_APP_BROWSER_ENTRY }, - output: clientOutputConfig, - treeshake: clientTreeshakeConfig, + output: getClientOutputConfig(viteMajorVersion), + treeshake: getClientTreeshakeConfig(viteMajorVersion), }, }, }, @@ -1463,8 +1496,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { ssrManifest: true, rollupOptions: { input: { index: VIRTUAL_CLIENT_ENTRY }, - output: clientOutputConfig, - treeshake: clientTreeshakeConfig, + output: getClientOutputConfig(viteMajorVersion), + treeshake: getClientTreeshakeConfig(viteMajorVersion), }, }, }, @@ -3752,7 +3785,14 @@ export type { export type { NextConfig } from "./config/next-config.js"; // Exported for CLI and testing -export { clientManualChunks, clientOutputConfig, clientTreeshakeConfig, computeLazyChunks }; +export { + clientManualChunks, + clientOutputConfig, + clientTreeshakeConfig, + computeLazyChunks, + getClientOutputConfig, + getClientTreeshakeConfig, +}; export { augmentSsrManifestFromBundle as _augmentSsrManifestFromBundle }; export { resolvePostcssStringPlugins as _resolvePostcssStringPlugins }; export { _postcssCache }; From 109ac934693022ba3367c69492155e6222a653ad Mon Sep 17 00:00:00 2001 From: MD YUNUS Date: Sun, 15 Mar 2026 16:17:00 +0530 Subject: [PATCH 02/11] fix: address bonk review comments --- packages/vinext/src/cli.ts | 8 ++++-- packages/vinext/src/index.ts | 10 +++++-- tests/build-optimization.test.ts | 47 ++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/packages/vinext/src/cli.ts b/packages/vinext/src/cli.ts index 608dc82f9..dff14ef3c 100644 --- a/packages/vinext/src/cli.ts +++ b/packages/vinext/src/cli.ts @@ -13,7 +13,11 @@ * needed for most Next.js apps. */ -import vinext, { getClientOutputConfig, getClientTreeshakeConfig } from "./index.js"; +import vinext, { + getClientOutputConfig, + getClientTreeshakeConfig, + getViteMajorVersion, +} from "./index.js"; import { printBuildReport } from "./build/report.js"; import path from "node:path"; import fs from "node:fs"; @@ -350,7 +354,7 @@ async function buildApp() { console.log(`\n vinext build (Vite ${getViteVersion()})\n`); const isApp = hasAppDir(); - const viteMajorVersion = parseInt(getViteVersion().split(".")[0], 10); + const viteMajorVersion = getViteMajorVersion(); // In verbose mode, skip the custom logger so raw Vite/Rollup output is shown. const logger = parsed.verbose ? vite.createLogger("info", { allowClearScreen: false }) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 513258836..ab547ec57 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -241,7 +241,7 @@ function extractStaticValue(node: any): unknown { * by a project that has Vite 8 — so we resolve from cwd, not from * the plugin's own location. */ -function getViteMajorVersion(): number { +export function getViteMajorVersion(): number { try { const require = createRequire(path.join(process.cwd(), "package.json")); const vitePkg = require("vite/package.json"); @@ -529,7 +529,9 @@ const clientTreeshakeConfig = { * Get Rollup-compatible output config for client builds. * Returns config without Vite 8/Rolldown-incompatible options. */ -function getClientOutputConfig(viteVersion: number) { +function getClientOutputConfig( + viteVersion: number, +): typeof clientOutputConfig | { manualChunks: typeof clientManualChunks } { if (viteVersion >= 8) { // Vite 8+ uses Rolldown which doesn't support experimentalMinChunkSize return { @@ -544,7 +546,9 @@ function getClientOutputConfig(viteVersion: number) { * Get Rollup-compatible treeshake config for client builds. * Returns config without Vite 8/Rolldown-incompatible options. */ -function getClientTreeshakeConfig(viteVersion: number) { +function getClientTreeshakeConfig( + viteVersion: number, +): typeof clientTreeshakeConfig | { moduleSideEffects: "no-external" } { if (viteVersion >= 8) { // Vite 8+ uses Rolldown which doesn't support `preset` option // moduleSideEffects is still supported in Rolldown diff --git a/tests/build-optimization.test.ts b/tests/build-optimization.test.ts index 845acc67f..049135638 100644 --- a/tests/build-optimization.test.ts +++ b/tests/build-optimization.test.ts @@ -8,11 +8,14 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { clientManualChunks, + clientOutputConfig, clientTreeshakeConfig, computeLazyChunks, _augmentSsrManifestFromBundle, _stripServerExports, _asyncHooksStubPlugin, + getClientOutputConfig, + getClientTreeshakeConfig, } from "../packages/vinext/src/index.js"; // The vinext config hook mutates process.env.NODE_ENV as a side effect (matching @@ -47,6 +50,50 @@ describe("clientTreeshakeConfig", () => { }); }); +// ─── getClientOutputConfig / getClientTreeshakeConfig ──────────────────────── + +describe("getClientOutputConfig", () => { + it("returns full config with experimentalMinChunkSize for Vite 7", () => { + const result = getClientOutputConfig(7); + expect(result).toEqual(clientOutputConfig); + expect((result as any).experimentalMinChunkSize).toBe(10_000); + expect(result.manualChunks).toBe(clientManualChunks); + }); + + it("returns config without experimentalMinChunkSize for Vite 8", () => { + const result = getClientOutputConfig(8); + expect(result).toEqual({ manualChunks: clientManualChunks }); + expect(result).not.toHaveProperty("experimentalMinChunkSize"); + }); + + it("returns config without experimentalMinChunkSize for Vite 9", () => { + const result = getClientOutputConfig(9); + expect(result).toEqual({ manualChunks: clientManualChunks }); + expect(result).not.toHaveProperty("experimentalMinChunkSize"); + }); +}); + +describe("getClientTreeshakeConfig", () => { + it("returns full config with preset for Vite 7", () => { + const result = getClientTreeshakeConfig(7); + expect(result).toEqual(clientTreeshakeConfig); + expect((result as any).preset).toBe("recommended"); + expect(result.moduleSideEffects).toBe("no-external"); + }); + + it("returns config without preset for Vite 8", () => { + const result = getClientTreeshakeConfig(8); + expect(result).toEqual({ moduleSideEffects: "no-external" }); + expect(result).not.toHaveProperty("preset"); + }); + + it("returns config without preset for Vite 9", () => { + const result = getClientTreeshakeConfig(9); + expect(result).toEqual({ moduleSideEffects: "no-external" }); + expect(result).not.toHaveProperty("preset"); + }); +}); + // ─── clientManualChunks ─────────────────────────────────────────────────────── describe("clientManualChunks", () => { From 6beaa474931545af3c616b257bc06474c047ddce Mon Sep 17 00:00:00 2001 From: MD YUNUS Date: Sun, 15 Mar 2026 19:43:40 +0530 Subject: [PATCH 03/11] fix(vite-8): use rolldownOptions for Vite 8+ treeshake config --- packages/vinext/src/cli.ts | 14 +- packages/vinext/src/index.ts | 224 ++++++++++++++++++++++--------- tests/build-optimization.test.ts | 4 +- 3 files changed, 168 insertions(+), 74 deletions(-) diff --git a/packages/vinext/src/cli.ts b/packages/vinext/src/cli.ts index dff14ef3c..914aae24c 100644 --- a/packages/vinext/src/cli.ts +++ b/packages/vinext/src/cli.ts @@ -13,11 +13,7 @@ * needed for most Next.js apps. */ -import vinext, { - getClientOutputConfig, - getClientTreeshakeConfig, - getViteMajorVersion, -} from "./index.js"; +import vinext, { getClientBuildOptionsWithInput, getViteMajorVersion } from "./index.js"; import { printBuildReport } from "./build/report.js"; import path from "node:path"; import fs from "node:fs"; @@ -390,11 +386,9 @@ async function buildApp() { outDir: "dist/client", manifest: true, ssrManifest: true, - rollupOptions: { - input: "virtual:vinext-client-entry", - output: getClientOutputConfig(viteMajorVersion), - treeshake: getClientTreeshakeConfig(viteMajorVersion), - }, + ...getClientBuildOptionsWithInput(viteMajorVersion, { + index: "virtual:vinext-client-entry", + }), }, }, logger, diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index ab547ec57..3ec381e35 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -489,6 +489,8 @@ function clientManualChunks(id: string): string | undefined { * their importers. This reduces HTTP request count and improves gzip * compression efficiency — small files restart the compression dictionary, * adding ~5-15% wire overhead vs fewer larger chunks. + * + * @deprecated Use `getClientOutputConfig()` instead — applies version-gated config. */ const clientOutputConfig = { manualChunks: clientManualChunks, @@ -519,6 +521,8 @@ const clientOutputConfig = { * tryCatchDeoptimization: false, which can break specific libraries * that rely on property access side effects or try/catch for feature detection * - 'recommended' + 'no-external' gives most of the benefit with less risk + * + * @deprecated Use `getClientTreeshakeConfig()` instead — applies version-gated config. */ const clientTreeshakeConfig = { preset: "recommended" as const, @@ -529,9 +533,10 @@ const clientTreeshakeConfig = { * Get Rollup-compatible output config for client builds. * Returns config without Vite 8/Rolldown-incompatible options. */ -function getClientOutputConfig( - viteVersion: number, -): typeof clientOutputConfig | { manualChunks: typeof clientManualChunks } { +function getClientOutputConfig(viteVersion: number): { + manualChunks: typeof clientManualChunks; + experimentalMinChunkSize?: number; +} { if (viteVersion >= 8) { // Vite 8+ uses Rolldown which doesn't support experimentalMinChunkSize return { @@ -546,9 +551,10 @@ function getClientOutputConfig( * Get Rollup-compatible treeshake config for client builds. * Returns config without Vite 8/Rolldown-incompatible options. */ -function getClientTreeshakeConfig( - viteVersion: number, -): typeof clientTreeshakeConfig | { moduleSideEffects: "no-external" } { +function getClientTreeshakeConfig(viteVersion: number): { + preset?: "recommended"; + moduleSideEffects: "no-external"; +} { if (viteVersion >= 8) { // Vite 8+ uses Rolldown which doesn't support `preset` option // moduleSideEffects is still supported in Rolldown @@ -560,6 +566,79 @@ function getClientTreeshakeConfig( return clientTreeshakeConfig; } +/** + * Get build options config for client builds, version-gated for Vite 8/Rolldown. + * Vite 7 uses build.rollupOptions, Vite 8+ uses build.rolldownOptions. + */ +function getClientBuildOptions(viteVersion: number): { + rollupOptions?: { + input?: Record; + output: { manualChunks: typeof clientManualChunks; experimentalMinChunkSize?: number }; + treeshake: { preset?: "recommended"; moduleSideEffects: "no-external" }; + }; + rolldownOptions?: { + input?: Record; + output: { manualChunks: typeof clientManualChunks }; + treeshake: { moduleSideEffects: "no-external" }; + }; +} { + if (viteVersion >= 8) { + // Vite 8+ uses Rolldown - config goes under rolldownOptions + return { + rolldownOptions: { + output: getClientOutputConfig(viteVersion), + treeshake: getClientTreeshakeConfig(viteVersion), + }, + }; + } + // Vite 7 uses Rollup - config goes under rollupOptions + return { + rollupOptions: { + output: getClientOutputConfig(viteVersion), + treeshake: getClientTreeshakeConfig(viteVersion), + }, + }; +} + +/** + * Get build options config for client builds with custom input, version-gated for Vite 8/Rolldown. + * Vite 7 uses build.rollupOptions, Vite 8+ uses build.rolldownOptions. + */ +function getClientBuildOptionsWithInput( + viteVersion: number, + input: Record, +): { + rollupOptions?: { + input: Record; + output: { manualChunks: typeof clientManualChunks; experimentalMinChunkSize?: number }; + treeshake: { preset?: "recommended"; moduleSideEffects: "no-external" }; + }; + rolldownOptions?: { + input: Record; + output: { manualChunks: typeof clientManualChunks }; + treeshake: { moduleSideEffects: "no-external" }; + }; +} { + if (viteVersion >= 8) { + // Vite 8+ uses Rolldown - config goes under rolldownOptions + return { + rolldownOptions: { + input, + output: getClientOutputConfig(viteVersion), + treeshake: getClientTreeshakeConfig(viteVersion), + }, + }; + } + // Vite 7 uses Rollup - config goes under rollupOptions + return { + rollupOptions: { + input, + output: getClientOutputConfig(viteVersion), + treeshake: getClientTreeshakeConfig(viteVersion), + }, + }; +} + type BuildManifestChunk = { file: string; isEntry?: boolean; @@ -1234,52 +1313,75 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { const viteConfig: UserConfig = { // Disable Vite's default HTML serving - we handle all routing appType: "custom", - build: { - rollupOptions: { - // Suppress "Module level directives cause errors when bundled" - // warnings for "use client" / "use server" directives. Our shims - // and third-party libraries legitimately use these directives; - // they are handled by the RSC plugin and are harmless in the - // final bundle. We preserve any user-supplied onwarn so custom - // warning handling is not lost. - onwarn: (() => { - const userOnwarn = config.build?.rollupOptions?.onwarn; - return (warning, defaultHandler) => { - if ( - warning.code === "MODULE_LEVEL_DIRECTIVE" && - (warning.message?.includes('"use client"') || - warning.message?.includes('"use server"')) - ) { - return; - } - if (userOnwarn) { - userOnwarn(warning, defaultHandler); - } else { - defaultHandler(warning); - } - }; - })(), - // Enable aggressive tree-shaking for client builds. - // See clientTreeshakeConfig for rationale. - // Only apply globally for standalone client builds (Pages Router - // CLI). For multi-environment builds (App Router, Cloudflare), - // treeshake is set per-environment on the client env below to - // avoid leaking into RSC/SSR environments where - // moduleSideEffects: 'no-external' could drop server packages - // that rely on module-level side effects. - ...(!isSSR && !isMultiEnv - ? { treeshake: getClientTreeshakeConfig(viteMajorVersion) } - : {}), - // Code-split client bundles: separate framework (React/ReactDOM), - // vinext runtime (shims), and vendor packages into their own - // chunks so pages only load the JS they need. - // Only apply globally for standalone client builds (CLI Pages - // Router). For multi-environment builds (App Router, Cloudflare), - // manualChunks is set per-environment on the client env below - // to avoid leaking into RSC/SSR environments. - ...(!isSSR && !isMultiEnv ? { output: getClientOutputConfig(viteMajorVersion) } : {}), - }, - }, + // For standalone client builds (Pages Router CLI), apply version-gated + // rollup/rolldown options. Vite 7 uses rollupOptions, Vite 8+ uses rolldownOptions. + // Multi-environment builds (App Router, Cloudflare) set these per-environment + // on the client env below to avoid leaking into RSC/SSR environments. + ...(isSSR || isMultiEnv + ? { + build: {}, + } + : viteMajorVersion >= 8 + ? { + build: { + rolldownOptions: { + ...getClientBuildOptions(viteMajorVersion).rolldownOptions, + // Suppress "Module level directives cause errors when bundled" + // warnings for "use client" / "use server" directives. Our shims + // and third-party libraries legitimately use these directives; + // they are handled by the RSC plugin and are harmless in the + // final bundle. We preserve any user-supplied onwarn so custom + // warning handling is not lost. + onwarn: (() => { + const userOnwarn = config.build?.rollupOptions?.onwarn; + return (warning: any, defaultHandler: (warning: any) => void) => { + if ( + warning.code === "MODULE_LEVEL_DIRECTIVE" && + (warning.message?.includes('"use client"') || + warning.message?.includes('"use server"')) + ) { + return; + } + if (userOnwarn) { + userOnwarn(warning, defaultHandler); + } else { + defaultHandler(warning); + } + }; + })(), + }, + } as any, + } + : { + build: { + rollupOptions: { + ...getClientBuildOptions(viteMajorVersion).rollupOptions, + // Suppress "Module level directives cause errors when bundled" + // warnings for "use client" / "use server" directives. Our shims + // and third-party libraries legitimately use these directives; + // they are handled by the RSC plugin and are harmless in the + // final bundle. We preserve any user-supplied onwarn so custom + // warning handling is not lost. + onwarn: (() => { + const userOnwarn = config.build?.rollupOptions?.onwarn; + return (warning: any, defaultHandler: (warning: any) => void) => { + if ( + warning.code === "MODULE_LEVEL_DIRECTIVE" && + (warning.message?.includes('"use client"') || + warning.message?.includes('"use server"')) + ) { + return; + } + if (userOnwarn) { + userOnwarn(warning, defaultHandler); + } else { + defaultHandler(warning); + } + }; + })(), + }, + }, + }), // Let OPTIONS requests pass through Vite's CORS middleware to our // route handlers so they can set the Allow header and run user-defined // OPTIONS handlers. Without this, Vite's CORS middleware responds to @@ -1479,11 +1581,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // on every page — defeating code-splitting for React.lazy() and // next/dynamic boundaries. ...(hasCloudflarePlugin ? { manifest: true } : {}), - rollupOptions: { - input: { index: VIRTUAL_APP_BROWSER_ENTRY }, - output: getClientOutputConfig(viteMajorVersion), - treeshake: getClientTreeshakeConfig(viteMajorVersion), - }, + ...getClientBuildOptionsWithInput(viteMajorVersion, { + index: VIRTUAL_APP_BROWSER_ENTRY, + }), }, }, }; @@ -1498,11 +1598,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { build: { manifest: true, ssrManifest: true, - rollupOptions: { - input: { index: VIRTUAL_CLIENT_ENTRY }, - output: getClientOutputConfig(viteMajorVersion), - treeshake: getClientTreeshakeConfig(viteMajorVersion), - }, + ...getClientBuildOptionsWithInput(viteMajorVersion, { + index: VIRTUAL_CLIENT_ENTRY, + }), }, }, }; @@ -3794,6 +3892,8 @@ export { clientOutputConfig, clientTreeshakeConfig, computeLazyChunks, + getClientBuildOptions, + getClientBuildOptionsWithInput, getClientOutputConfig, getClientTreeshakeConfig, }; diff --git a/tests/build-optimization.test.ts b/tests/build-optimization.test.ts index 049135638..0f8efc0f1 100644 --- a/tests/build-optimization.test.ts +++ b/tests/build-optimization.test.ts @@ -383,7 +383,7 @@ describe("treeshake config integration", () => { const result = await (mainPlugin as any).config(mockConfig, { command: "build" }); // treeshake should NOT be set for SSR builds - expect(result.build.rollupOptions.treeshake).toBeUndefined(); + expect(result.build.rollupOptions?.treeshake).toBeUndefined(); } finally { await fsp.rm(tmpDir, { recursive: true, force: true }).catch(() => {}); } @@ -430,7 +430,7 @@ describe("treeshake config integration", () => { const result = await (mainPlugin as any).config(mockConfig, { command: "build" }); // Global rollupOptions should NOT have treeshake (would leak into RSC/SSR) - expect(result.build.rollupOptions.treeshake).toBeUndefined(); + expect(result.build.rollupOptions?.treeshake).toBeUndefined(); // Client environment should have treeshake expect(result.environments.client.build.rollupOptions.treeshake).toEqual({ From 1e6744ee939b81035e4a18be61fe3c1eb9639a10 Mon Sep 17 00:00:00 2001 From: Md Yunus Date: Sun, 22 Mar 2026 20:06:43 +0530 Subject: [PATCH 04/11] fix: address bonk review comments for Vite 8/Rolldown --- packages/vinext/src/index.ts | 57 +++++++++++++++--------------------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 3ec381e35..ed6e76c72 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -822,6 +822,27 @@ export interface VinextOptions { react?: VitePluginReactOptions | boolean; } +/** + * Helper to suppress "Module level directives cause errors when bundled" + * warnings for "use client" / "use server" directives. + */ +function createDirectiveOnwarn(userOnwarn?: (warning: any, handler: (w: any) => void) => void) { + return (warning: any, defaultHandler: (warning: any) => void) => { + if ( + warning.code === "MODULE_LEVEL_DIRECTIVE" && + (warning.message?.includes('"use client"') || + warning.message?.includes('"use server"')) + ) { + return; + } + if (userOnwarn) { + userOnwarn(warning, defaultHandler); + } else { + defaultHandler(warning); + } + }; +} + export default function vinext(options: VinextOptions = {}): PluginOption[] { const viteMajorVersion = getViteMajorVersion(); let root: string; @@ -1332,23 +1353,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // they are handled by the RSC plugin and are harmless in the // final bundle. We preserve any user-supplied onwarn so custom // warning handling is not lost. - onwarn: (() => { - const userOnwarn = config.build?.rollupOptions?.onwarn; - return (warning: any, defaultHandler: (warning: any) => void) => { - if ( - warning.code === "MODULE_LEVEL_DIRECTIVE" && - (warning.message?.includes('"use client"') || - warning.message?.includes('"use server"')) - ) { - return; - } - if (userOnwarn) { - userOnwarn(warning, defaultHandler); - } else { - defaultHandler(warning); - } - }; - })(), + onwarn: createDirectiveOnwarn(config.build?.rolldownOptions?.onwarn ?? config.build?.rollupOptions?.onwarn), }, } as any, } @@ -1362,23 +1367,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // they are handled by the RSC plugin and are harmless in the // final bundle. We preserve any user-supplied onwarn so custom // warning handling is not lost. - onwarn: (() => { - const userOnwarn = config.build?.rollupOptions?.onwarn; - return (warning: any, defaultHandler: (warning: any) => void) => { - if ( - warning.code === "MODULE_LEVEL_DIRECTIVE" && - (warning.message?.includes('"use client"') || - warning.message?.includes('"use server"')) - ) { - return; - } - if (userOnwarn) { - userOnwarn(warning, defaultHandler); - } else { - defaultHandler(warning); - } - }; - })(), + onwarn: createDirectiveOnwarn(config.build?.rolldownOptions?.onwarn ?? config.build?.rollupOptions?.onwarn), }, }, }), From 6b9d5ef07e8e1444922e31c5019860fa6c819002 Mon Sep 17 00:00:00 2001 From: Md Yunus Date: Sun, 22 Mar 2026 22:36:58 +0530 Subject: [PATCH 05/11] fix: apply final conflict resolutions and formatting for Vite 8 --- packages/vinext/src/index.ts | 803 ++++++++++++++++++++++++++--------- 1 file changed, 608 insertions(+), 195 deletions(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index dba42fd1b..deda5bc98 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -29,8 +29,14 @@ import { import { findMiddlewareFile, runMiddleware } from "./server/middleware.js"; import { logRequest, now } from "./server/request-log.js"; import { normalizePath } from "./server/normalize-path.js"; -import { findInstrumentationFile, runInstrumentation } from "./server/instrumentation.js"; -import { PHASE_PRODUCTION_BUILD, PHASE_DEVELOPMENT_SERVER } from "./shims/constants.js"; +import { + findInstrumentationFile, + runInstrumentation, +} from "./server/instrumentation.js"; +import { + PHASE_PRODUCTION_BUILD, + PHASE_DEVELOPMENT_SERVER, +} from "./shims/constants.js"; import { validateDevRequest } from "./server/dev-origin-check.js"; import { isExternalUrl, @@ -53,7 +59,10 @@ import { import { hasBasePath } from "./utils/base-path.js"; import { asyncHooksStubPlugin } from "./plugins/async-hooks-stub.js"; import { clientReferenceDedupPlugin } from "./plugins/client-reference-dedup.js"; -import { hasWranglerConfig, formatMissingCloudflarePluginError } from "./deploy.js"; +import { + hasWranglerConfig, + formatMissingCloudflarePluginError, +} from "./deploy.js"; import tsconfigPaths from "vite-tsconfig-paths"; import type { Options as VitePluginReactOptions } from "@vitejs/plugin-react"; import MagicString from "magic-string"; @@ -67,9 +76,14 @@ import commonjs from "vite-plugin-commonjs"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); type VitePluginReactModule = typeof import("@vitejs/plugin-react"); -function resolveOptionalDependency(projectRoot: string, specifier: string): string | null { +function resolveOptionalDependency( + projectRoot: string, + specifier: string, +): string | null { try { - const projectRequire = createRequire(path.join(projectRoot, "package.json")); + const projectRequire = createRequire( + path.join(projectRoot, "package.json"), + ); return projectRequire.resolve(specifier); } catch {} @@ -110,7 +124,10 @@ async function fetchAndCacheFont( // Use a hash of the URL for the cache key const { createHash } = await import("node:crypto"); const urlHash = createHash("md5").update(cssUrl).digest("hex").slice(0, 12); - const fontDir = path.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${urlHash}`); + const fontDir = path.join( + cacheDir, + `${family.toLowerCase().replace(/\s+/g, "-")}-${urlHash}`, + ); // Check if already cached const cachedCSSPath = path.join(fontDir, "style.css"); @@ -142,8 +159,14 @@ async function fetchAndCacheFont( : fontUrl.includes(".woff") ? ".woff" : ".ttf"; - const fileHash = createHash("md5").update(fontUrl).digest("hex").slice(0, 8); - urls.set(fontUrl, `${family.toLowerCase().replace(/\s+/g, "-")}-${fileHash}${ext}`); + const fileHash = createHash("md5") + .update(fontUrl) + .digest("hex") + .slice(0, 8); + urls.set( + fontUrl, + `${family.toLowerCase().replace(/\s+/g, "-")}-${fileHash}${ext}`, + ); } } @@ -176,7 +199,9 @@ async function fetchAndCacheFont( * Supports: string literals, numeric literals, boolean literals, * arrays of the above, and nested object literals. */ -function parseStaticObjectLiteral(objectStr: string): Record | null { +function parseStaticObjectLiteral( + objectStr: string, +): Record | null { let ast: ReturnType; try { // Wrap in parens so the parser treats `{…}` as an expression, not a block @@ -243,7 +268,10 @@ function extractStaticValue(node: any): unknown { let key: string; if (prop.key.type === "Identifier") { key = prop.key.name; - } else if (prop.key.type === "Literal" && typeof prop.key.value === "string") { + } else if ( + prop.key.type === "Literal" && + typeof prop.key.value === "string" + ) { key = prop.key.value; } else { return undefined; @@ -291,7 +319,9 @@ export function getViteMajorVersion(): number { return 7; } catch (error) { const message = error instanceof Error ? error.message : String(error); - console.warn(`[vinext] Failed to resolve vite/package.json (${message}); assuming Vite 7`); + console.warn( + `[vinext] Failed to resolve vite/package.json (${message}); assuming Vite 7`, + ); return 7; } } @@ -328,7 +358,10 @@ const POSTCSS_CONFIG_FILES = [ * Stores the Promise itself so concurrent calls (RSC/SSR/Client config() hooks firing in * parallel) all await the same in-flight scan rather than each starting their own. */ -const _postcssCache = new Map>(); +const _postcssCache = new Map< + string, + Promise<{ plugins: any[] } | undefined> +>(); /** * Resolve PostCSS string plugin names in a project's PostCSS config. @@ -342,7 +375,9 @@ const _postcssCache = new Map>() * Returns the resolved PostCSS config object to inject into Vite's * `css.postcss`, or `undefined` if no resolution is needed. */ -function resolvePostcssStringPlugins(projectRoot: string): Promise<{ plugins: any[] } | undefined> { +function resolvePostcssStringPlugins( + projectRoot: string, +): Promise<{ plugins: any[] } | undefined> { if (_postcssCache.has(projectRoot)) return _postcssCache.get(projectRoot)!; const promise = _resolvePostcssStringPluginsUncached(projectRoot); @@ -398,7 +433,8 @@ async function _resolvePostcssStringPluginsUncached( return undefined; } const hasStringPlugins = config.plugins.some( - (p: any) => typeof p === "string" || (Array.isArray(p) && typeof p[0] === "string"), + (p: any) => + typeof p === "string" || (Array.isArray(p) && typeof p[0] === "string"), ); if (!hasStringPlugins) { return undefined; @@ -618,7 +654,10 @@ function getClientTreeshakeConfig(viteVersion: number): { function getClientBuildOptions(viteVersion: number): { rollupOptions?: { input?: Record; - output: { manualChunks: typeof clientManualChunks; experimentalMinChunkSize?: number }; + output: { + manualChunks: typeof clientManualChunks; + experimentalMinChunkSize?: number; + }; treeshake: { preset?: "recommended"; moduleSideEffects: "no-external" }; }; rolldownOptions?: { @@ -655,7 +694,10 @@ function getClientBuildOptionsWithInput( ): { rollupOptions?: { input: Record; - output: { manualChunks: typeof clientManualChunks; experimentalMinChunkSize?: number }; + output: { + manualChunks: typeof clientManualChunks; + experimentalMinChunkSize?: number; + }; treeshake: { preset?: "recommended"; moduleSideEffects: "no-external" }; }; rolldownOptions?: { @@ -711,7 +753,9 @@ type BuildManifestChunk = { * @returns Array of chunk filenames (e.g. "assets/mermaid-NOHMQCX5.js") that * should be excluded from modulepreload hints. */ -function computeLazyChunks(buildManifest: Record): string[] { +function computeLazyChunks( + buildManifest: Record, +): string[] { // Collect all chunk files that are statically reachable from entries const eagerFiles = new Set(); const visited = new Set(); @@ -793,20 +837,27 @@ function isWindowsAbsolutePath(candidate: string): boolean { } function relativeWithinRoot(root: string, moduleId: string): string | null { - const useWindowsPath = isWindowsAbsolutePath(root) || isWindowsAbsolutePath(moduleId); + const useWindowsPath = + isWindowsAbsolutePath(root) || isWindowsAbsolutePath(moduleId); const relativeId = ( - useWindowsPath ? path.win32.relative(root, moduleId) : path.relative(root, moduleId) + useWindowsPath + ? path.win32.relative(root, moduleId) + : path.relative(root, moduleId) ).replace(/\\/g, "/"); // path.relative(root, root) returns "", which is not a usable manifest key and should be // treated the same as "outside root" for this helper. - if (!relativeId || relativeId === ".." || relativeId.startsWith("../")) return null; + if (!relativeId || relativeId === ".." || relativeId.startsWith("../")) + return null; return relativeId; } function normalizeManifestModuleId(moduleId: string, root: string): string { const normalizedId = moduleId.replace(/\\/g, "/"); if (normalizedId.startsWith("\0")) return normalizedId; - if (normalizedId.startsWith("node_modules/") || normalizedId.includes("/node_modules/")) { + if ( + normalizedId.startsWith("node_modules/") || + normalizedId.includes("/node_modules/") + ) { return normalizedId; } @@ -857,7 +908,8 @@ function augmentSsrManifestFromBundle( for (const [key, files] of Object.entries(ssrManifest)) { const normalizedKey = normalizeManifestModuleId(key, root); - if (!nextManifest[normalizedKey]) nextManifest[normalizedKey] = new Set(); + if (!nextManifest[normalizedKey]) + nextManifest[normalizedKey] = new Set(); for (const file of files) { nextManifest[normalizedKey].add(normalizeManifestFile(file)); } @@ -881,7 +933,8 @@ function augmentSsrManifestFromBundle( for (const moduleId of Object.keys(chunk.modules ?? {})) { const key = normalizeManifestModuleId(moduleId, root); - if (key.startsWith("node_modules/") || key.includes("/node_modules/")) continue; + if (key.startsWith("node_modules/") || key.includes("/node_modules/")) + continue; if (key.startsWith("\0")) continue; if (!nextManifest[key]) nextManifest[key] = new Set(); for (const file of files) { @@ -963,7 +1016,9 @@ export interface VinextOptions { * Helper to suppress "Module level directives cause errors when bundled" * warnings for "use client" / "use server" directives. */ -function createDirectiveOnwarn(userOnwarn?: (warning: any, handler: (w: any) => void) => void) { +function createDirectiveOnwarn( + userOnwarn?: (warning: any, handler: (w: any) => void) => void, +) { return (warning: any, defaultHandler: (warning: any) => void) => { if ( warning.code === "MODULE_LEVEL_DIRECTIVE" && @@ -1060,8 +1115,14 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Prefer the user's project graph so vinext shares the app's Vite/plugin // instances. In source/workspace development, test fixtures may not declare // peer deps explicitly, so fall back to vinext's own install location. - resolvedReactPath = resolveOptionalDependency(earlyBaseDir, "@vitejs/plugin-react"); - resolvedRscPath = resolveOptionalDependency(earlyBaseDir, "@vitejs/plugin-rsc"); + resolvedReactPath = resolveOptionalDependency( + earlyBaseDir, + "@vitejs/plugin-react", + ); + resolvedRscPath = resolveOptionalDependency( + earlyBaseDir, + "@vitejs/plugin-rsc", + ); resolvedRscTransformsPath = resolveOptionalDependency( earlyBaseDir, "@vitejs/plugin-rsc/transforms", @@ -1093,11 +1154,14 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { }); }) .catch((cause) => { - throw new Error("vinext: Failed to load @vitejs/plugin-rsc.", { cause }); + throw new Error("vinext: Failed to load @vitejs/plugin-rsc.", { + cause, + }); }); } - const reactOptions = options.react && options.react !== true ? options.react : undefined; + const reactOptions = + options.react && options.react !== true ? options.react : undefined; let reactPluginPromise: Promise | null = null; if (options.react !== false) { @@ -1113,11 +1177,16 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { reactPluginPromise = reactImport .then((mod) => (mod as VitePluginReactModule).default(reactOptions)) .catch((cause) => { - throw new Error("vinext: Failed to load @vitejs/plugin-react.", { cause }); + throw new Error("vinext: Failed to load @vitejs/plugin-react.", { + cause, + }); }); } - const imageImportDimCache = new Map(); + const imageImportDimCache = new Map< + string, + { width: number; height: number } + >(); // Shared state for the MDX proxy plugin. Populated during config() if MDX // files are detected and @mdx-js/rollup is installed. @@ -1181,7 +1250,10 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { for (const elem of pattern.elements) { collectPatternNames(elem, names); } - } else if (pattern.type === "RestElement" || pattern.type === "AssignmentPattern") { + } else if ( + pattern.type === "RestElement" || + pattern.type === "AssignmentPattern" + ) { collectPatternNames(pattern.left ?? pattern.argument, names); } } @@ -1250,7 +1322,11 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // of this node (e.g. top-level Program statements, or the direct body of // a BlockStatement) — those ARE in scope for everything in the same block. const immediateStmts: any[] = - node.type === "Program" ? node.body : node.type === "BlockStatement" ? node.body : []; + node.type === "Program" + ? node.body + : node.type === "BlockStatement" + ? node.body + : []; for (const stmt of immediateStmts) { if (stmt?.type === "VariableDeclaration") { for (const decl of stmt.declarations) @@ -1281,10 +1357,12 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Build the ancestor name set visible inside this function: // everything the parent saw, plus this function's own params. const namesForBody = new Set(ancestorNames); - for (const p of node.params ?? []) collectPatternNames(p, namesForBody); + for (const p of node.params ?? []) + collectPatternNames(p, namesForBody); // Check whether the body has the 'use server' directive. - const bodyStmts: any[] = node.body?.type === "BlockStatement" ? node.body.body : []; + const bodyStmts: any[] = + node.body?.type === "BlockStatement" ? node.body.body : []; const isServerFn = hasUseServerDirective(bodyStmts); if (isServerFn) { @@ -1363,7 +1441,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { collectFunctionScopedNames(node.body, namesForChildren); for (const stmt of bodyStmts) { if (stmt?.type === "VariableDeclaration" && stmt.kind !== "var") { - for (const decl of stmt.declarations) collectPatternNames(decl.id, namesForChildren); + for (const decl of stmt.declarations) + collectPatternNames(decl.id, namesForChildren); } } @@ -1391,7 +1470,12 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // - Never call s.update() on the same source range twice // // `parent` is the direct parent AST node, used to detect property contexts. - function renamingWalk(node: any, from: string, to: string, parent?: any) { + function renamingWalk( + node: any, + from: string, + to: string, + parent?: any, + ) { if (!node || typeof node !== "object") return; if (node.type === "Identifier" && node.name === from) { @@ -1405,7 +1489,11 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { } // Non-computed property key in an object literal - if (parent?.type === "Property" && parent.key === node && !parent.computed) { + if ( + parent?.type === "Property" && + parent.key === node && + !parent.computed + ) { if (parent.shorthand) { // { cookies } — key and value are the same AST node. // Expand to { cookies: __local_cookies } by rewriting at the key @@ -1421,7 +1509,11 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { } // Value side of a shorthand property — same node as key, already handled - if (parent?.type === "Property" && parent.shorthand && parent.value === node) { + if ( + parent?.type === "Property" && + parent.shorthand && + parent.value === node + ) { return; } @@ -1432,7 +1524,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // break/continue label: `break cookies` / `continue cookies` — not a variable reference if ( - (parent?.type === "BreakStatement" || parent?.type === "ContinueStatement") && + (parent?.type === "BreakStatement" || + parent?.type === "ContinueStatement") && parent.label === node ) { return; @@ -1457,7 +1550,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { ) { const nestedDecls = new Set(); // Params - for (const p of node.params ?? []) collectPatternNames(p, nestedDecls); + for (const p of node.params ?? []) + collectPatternNames(p, nestedDecls); // Recursively find all var/const/let declarations in the body, // including those nested inside if/for/while/etc. collectAllDeclaredNames(node.body, nestedDecls); @@ -1465,7 +1559,10 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Also stop at nested 'use server' functions — visitNode will handle them // independently with the correct collision set, preventing double-rewrites. - if (node.body?.type === "BlockStatement" && hasUseServerDirective(node.body.body)) { + if ( + node.body?.type === "BlockStatement" && + hasUseServerDirective(node.body.body) + ) { return; } } @@ -1516,14 +1613,18 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { } // FunctionExpression / ArrowFunctionExpression names are only in scope // inside their own body, not the enclosing scope — skip entirely. - if (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") { + if ( + node.type === "FunctionExpression" || + node.type === "ArrowFunctionExpression" + ) { return; } // var declarations are function-scoped — collect them wherever they appear. // let/const at a nested block level are block-scoped and NOT visible to // sibling or outer function declarations, so skip them here. if (node.type === "VariableDeclaration" && node.kind === "var") { - for (const decl of node.declarations) collectPatternNames(decl.id, names); + for (const decl of node.declarations) + collectPatternNames(decl.id, names); } for (const key of Object.keys(node)) { if (key === "type") continue; @@ -1545,7 +1646,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { function collectAllDeclaredNames(node: any, names: Set) { if (!node || typeof node !== "object") return; if (node.type === "VariableDeclaration") { - for (const decl of node.declarations) collectPatternNames(decl.id, names); + for (const decl of node.declarations) + collectPatternNames(decl.id, names); } // FunctionDeclaration name is a binding in the enclosing scope — record it. if (node.type === "FunctionDeclaration") { @@ -1557,7 +1659,10 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { if (node.id?.name) names.add(node.id.name); return; // don't recurse into the class body (separate scope) } - if (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") { + if ( + node.type === "FunctionExpression" || + node.type === "ArrowFunctionExpression" + ) { return; // different scope — stop } for (const key of Object.keys(node)) { @@ -1574,7 +1679,10 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { visitNode(ast, new Set()); if (!changed) return null; - return { code: s.toString(), map: s.generateMap({ hires: "boundary" }) }; + return { + code: s.toString(), + map: s.generateMap({ hires: "boundary" }), + }; }, }, { @@ -1583,7 +1691,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { async config(config, env) { root = config.root ?? process.cwd(); - const userResolve = config.resolve as UserResolveConfigWithTsconfigPaths | undefined; + const userResolve = config.resolve as + | UserResolveConfigWithTsconfigPaths + | undefined; const shouldEnableNativeTsconfigPaths = viteMajorVersion >= 8 && userResolve?.tsconfigPaths === undefined; @@ -1644,7 +1754,10 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { hasAppDir = !options.disableAppRouter && fs.existsSync(appDir); // Load next.config.js if present (always from project root, not src/) - const phase = env?.command === "build" ? PHASE_PRODUCTION_BUILD : PHASE_DEVELOPMENT_SERVER; + const phase = + env?.command === "build" + ? PHASE_PRODUCTION_BUILD + : PHASE_DEVELOPMENT_SERVER; const rawConfig = await loadNextConfig(root, phase); nextConfig = await resolveNextConfig(rawConfig, root); fileMatcher = createValidFileMatcher(nextConfig.pageExtensions); @@ -1667,7 +1780,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { defines[`process.env.${key}`] = JSON.stringify(value); } // Expose basePath to client-side code - defines["process.env.__NEXT_ROUTER_BASEPATH"] = JSON.stringify(nextConfig.basePath); + defines["process.env.__NEXT_ROUTER_BASEPATH"] = JSON.stringify( + nextConfig.basePath, + ); // Expose image remote patterns for validation in next/image shim defines["process.env.__VINEXT_IMAGE_REMOTE_PATTERNS"] = JSON.stringify( JSON.stringify(nextConfig.images?.remotePatterns ?? []), @@ -1682,26 +1797,35 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { const deviceSizes = nextConfig.images?.deviceSizes ?? [ 640, 750, 828, 1080, 1200, 1920, 2048, 3840, ]; - const imageSizes = nextConfig.images?.imageSizes ?? [16, 32, 48, 64, 96, 128, 256, 384]; + const imageSizes = nextConfig.images?.imageSizes ?? [ + 16, 32, 48, 64, 96, 128, 256, 384, + ]; defines["process.env.__VINEXT_IMAGE_DEVICE_SIZES"] = JSON.stringify( JSON.stringify(deviceSizes), ); - defines["process.env.__VINEXT_IMAGE_SIZES"] = JSON.stringify(JSON.stringify(imageSizes)); + defines["process.env.__VINEXT_IMAGE_SIZES"] = JSON.stringify( + JSON.stringify(imageSizes), + ); } // Expose dangerouslyAllowSVG flag for the image shim's auto-skip logic. // When false (default), .svg sources bypass the optimization endpoint. - defines["process.env.__VINEXT_IMAGE_DANGEROUSLY_ALLOW_SVG"] = JSON.stringify( - String(nextConfig.images?.dangerouslyAllowSVG ?? false), - ); + defines["process.env.__VINEXT_IMAGE_DANGEROUSLY_ALLOW_SVG"] = + JSON.stringify( + String(nextConfig.images?.dangerouslyAllowSVG ?? false), + ); // Draft mode secret — generated once at build time so the // __prerender_bypass cookie is consistent across all server // instances (e.g. multiple Cloudflare Workers isolates). - defines["process.env.__VINEXT_DRAFT_SECRET"] = JSON.stringify(crypto.randomUUID()); + defines["process.env.__VINEXT_DRAFT_SECRET"] = JSON.stringify( + crypto.randomUUID(), + ); // Build ID — resolved from next.config generateBuildId() or random UUID. // Exposed so server entries and the next/server shim can inject it. // Also used to namespace ISR cache keys so old cached entries from a // previous deploy are never served by the new one. - defines["process.env.__VINEXT_BUILD_ID"] = JSON.stringify(nextConfig.buildId); + defines["process.env.__VINEXT_BUILD_ID"] = JSON.stringify( + nextConfig.buildId, + ); // Build the shim alias map. Exact `.js` variants are included for the // public Next entrypoints that are file-backed in `next/package.json`. @@ -1750,52 +1874,72 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { "internal", "router-context", ), - "next/dist/shared/lib/utils": path.join(shimsDir, "internal", "utils"), - "next/dist/server/api-utils": path.join(shimsDir, "internal", "api-utils"), - "next/dist/server/web/spec-extension/cookies": path.join( + "next/dist/shared/lib/utils": path.join( shimsDir, "internal", - "cookies", + "utils", ), - "next/dist/compiled/@edge-runtime/cookies": path.join(shimsDir, "internal", "cookies"), - "next/dist/server/app-render/work-unit-async-storage.external": path.join( + "next/dist/server/api-utils": path.join( shimsDir, "internal", - "work-unit-async-storage", + "api-utils", ), - "next/dist/client/components/work-unit-async-storage.external": path.join( + "next/dist/server/web/spec-extension/cookies": path.join( shimsDir, "internal", - "work-unit-async-storage", + "cookies", ), - "next/dist/client/components/request-async-storage.external": path.join( + "next/dist/compiled/@edge-runtime/cookies": path.join( shimsDir, "internal", - "work-unit-async-storage", + "cookies", ), + "next/dist/server/app-render/work-unit-async-storage.external": + path.join(shimsDir, "internal", "work-unit-async-storage"), + "next/dist/client/components/work-unit-async-storage.external": + path.join(shimsDir, "internal", "work-unit-async-storage"), + "next/dist/client/components/request-async-storage.external": + path.join(shimsDir, "internal", "work-unit-async-storage"), "next/dist/client/components/request-async-storage": path.join( shimsDir, "internal", "work-unit-async-storage", ), // Re-export public modules for internal path imports - "next/dist/client/components/navigation": path.join(shimsDir, "navigation"), - "next/dist/server/config-shared": path.join(shimsDir, "internal", "utils"), + "next/dist/client/components/navigation": path.join( + shimsDir, + "navigation", + ), + "next/dist/server/config-shared": path.join( + shimsDir, + "internal", + "utils", + ), // server-only / client-only marker packages "server-only": path.join(shimsDir, "server-only"), "client-only": path.join(shimsDir, "client-only"), "vinext/error-boundary": path.join(shimsDir, "error-boundary"), - "vinext/layout-segment-context": path.join(shimsDir, "layout-segment-context"), + "vinext/layout-segment-context": path.join( + shimsDir, + "layout-segment-context", + ), "vinext/metadata": path.join(shimsDir, "metadata"), "vinext/fetch-cache": path.join(shimsDir, "fetch-cache"), "vinext/cache-runtime": path.join(shimsDir, "cache-runtime"), "vinext/navigation-state": path.join(shimsDir, "navigation-state"), - "vinext/unified-request-context": path.join(shimsDir, "unified-request-context"), + "vinext/unified-request-context": path.join( + shimsDir, + "unified-request-context", + ), "vinext/router-state": path.join(shimsDir, "router-state"), "vinext/head-state": path.join(shimsDir, "head-state"), "vinext/i18n-state": path.join(shimsDir, "i18n-state"), "vinext/i18n-context": path.join(shimsDir, "i18n-context"), - "vinext/instrumentation": path.resolve(__dirname, "server", "instrumentation"), + "vinext/instrumentation": path.resolve( + __dirname, + "server", + "instrumentation", + ), "vinext/html": path.resolve(__dirname, "server", "html"), }).flatMap(([k, v]) => k.startsWith("next/") @@ -1822,7 +1966,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { p && typeof p === "object" && typeof p.name === "string" && - (p.name === "vite-plugin-cloudflare" || p.name.startsWith("vite-plugin-cloudflare:")), + (p.name === "vite-plugin-cloudflare" || + p.name.startsWith("vite-plugin-cloudflare:")), ); hasNitroPlugin = pluginsFlat.some( (p: any) => @@ -1855,7 +2000,11 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { ); if ( !hasMdxPlugin && - hasMdxFiles(root, hasAppDir ? appDir : null, hasPagesDir ? pagesDir : null) + hasMdxFiles( + root, + hasAppDir ? appDir : null, + hasPagesDir ? pagesDir : null, + ) ) { try { const mdxRollup = await import("@mdx-js/rollup"); @@ -1866,7 +2015,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { mdxOpts.remarkPlugins = nextConfig.mdx.remarkPlugins; if (nextConfig.mdx.rehypePlugins) mdxOpts.rehypePlugins = nextConfig.mdx.rehypePlugins; - if (nextConfig.mdx.recmaPlugins) mdxOpts.recmaPlugins = nextConfig.mdx.recmaPlugins; + if (nextConfig.mdx.recmaPlugins) + mdxOpts.recmaPlugins = nextConfig.mdx.recmaPlugins; } mdxDelegate = mdxFactory(mdxOpts); if (nextConfig.mdx) { @@ -1874,7 +2024,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { "[vinext] Auto-injected @mdx-js/rollup with remark/rehype plugins from next.config", ); } else { - console.log("[vinext] Auto-injected @mdx-js/rollup for MDX support"); + console.log( + "[vinext] Auto-injected @mdx-js/rollup for MDX support", + ); } } catch { // @mdx-js/rollup not installed — warn but don't fail @@ -1908,9 +2060,11 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { ? { build: { rolldownOptions: { - ...getClientBuildOptions(viteMajorVersion).rolldownOptions, + ...getClientBuildOptions(viteMajorVersion) + .rolldownOptions, onwarn: createDirectiveOnwarn( - config.build?.rolldownOptions?.onwarn ?? config.build?.rollupOptions?.onwarn, + config.build?.rolldownOptions?.onwarn ?? + config.build?.rollupOptions?.onwarn, ), }, } as any, @@ -1920,7 +2074,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { rollupOptions: { ...getClientBuildOptions(viteMajorVersion).rollupOptions, onwarn: createDirectiveOnwarn( - config.build?.rolldownOptions?.onwarn ?? config.build?.rollupOptions?.onwarn, + config.build?.rolldownOptions?.onwarn ?? + config.build?.rollupOptions?.onwarn, ), }, }, @@ -1935,7 +2090,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { server: { cors: { preflightContinue: true, - origin: /^https?:\/\/(?:(?:[^:]+\.)?localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/, + origin: + /^https?:\/\/(?:(?:[^:]+\.)?localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/, }, }, // Configure SSR transform behaviour for Node targets. @@ -1964,7 +2120,12 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // brings its own React copy, multiple React instances can load, // causing cryptic "Invalid hook call" errors. This is a no-op // when only one copy exists. - dedupe: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"], + dedupe: [ + "react", + "react-dom", + "react/jsx-runtime", + "react/jsx-dev-runtime", + ], ...(shouldEnableNativeTsconfigPaths ? { tsconfigPaths: true } : {}), }, // NOTE: top-level optimizeDeps is now set below (after capturing @@ -1998,8 +2159,11 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // only from its `node` condition, not from the universal `default` one). // Without externalizing them, Vite's optimizer picks the wrong export // condition and the build fails with MISSING_EXPORT errors. - const nextServerExternal: string[] = nextConfig?.serverExternalPackages ?? []; - const userSsrExternal: string[] | true = Array.isArray(config.ssr?.external) + const nextServerExternal: string[] = + nextConfig?.serverExternalPackages ?? []; + const userSsrExternal: string[] | true = Array.isArray( + config.ssr?.external, + ) ? [...config.ssr.external, ...nextServerExternal] : config.ssr?.external === true ? true @@ -2048,18 +2212,27 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { external: userSsrExternal === true ? true - : ["satori", "@resvg/resvg-js", "yoga-wasm-web", ...userSsrExternal], + : [ + "satori", + "@resvg/resvg-js", + "yoga-wasm-web", + ...userSsrExternal, + ], // Force all node_modules through Vite's transform pipeline // so non-JS imports (CSS, images) don't hit Node's native // ESM loader. Matches Next.js behavior of bundling everything. // Packages in `external` above take precedence per Vite rules. // When user sets `ssr.external: true`, skip noExternal since // everything is already externalized. - ...(userSsrExternal === true ? {} : { noExternal: true as const }), + ...(userSsrExternal === true + ? {} + : { noExternal: true as const }), }, }), optimizeDeps: { - exclude: [...new Set([...incomingExclude, "vinext", "@vercel/og"])], + exclude: [ + ...new Set([...incomingExclude, "vinext", "@vercel/og"]), + ], entries: appEntries, }, build: { @@ -2074,17 +2247,22 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { ? {} : { resolve: { - external: userSsrExternal === true ? true : [...userSsrExternal], + external: + userSsrExternal === true ? true : [...userSsrExternal], // Force all node_modules through Vite's transform pipeline // so non-JS imports (CSS, images) don't hit Node's native // ESM loader. Matches Next.js behavior of bundling everything. // When user sets `ssr.external: true`, skip noExternal since // everything is already externalized. - ...(userSsrExternal === true ? {} : { noExternal: true as const }), + ...(userSsrExternal === true + ? {} + : { noExternal: true as const }), }, }), optimizeDeps: { - exclude: [...new Set([...incomingExclude, "vinext", "@vercel/og"])], + exclude: [ + ...new Set([...incomingExclude, "vinext", "@vercel/og"]), + ], entries: appEntries, }, build: { @@ -2112,7 +2290,12 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // but the browser optimizer resolves to `core.js` which lacks it, // causing MISSING_EXPORT build failures). exclude: [ - ...new Set([...incomingExclude, "vinext", "@vercel/og", ...nextServerExternal]), + ...new Set([ + ...incomingExclude, + "vinext", + "@vercel/og", + ...nextServerExternal, + ]), ], // Crawl app/ source files up front so client-only deps imported // by user components are discovered during startup instead of @@ -2176,13 +2359,18 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Assumes @vitejs/plugin-react top-level plugin names continue to use // the vite:react* prefix across supported versions. const reactRootPlugins = config.plugins.filter( - (p: any) => p && typeof p.name === "string" && p.name.startsWith("vite:react"), + (p: any) => + p && + typeof p.name === "string" && + p.name.startsWith("vite:react"), ); const counts = new Map(); for (const plugin of reactRootPlugins) { counts.set(plugin.name, (counts.get(plugin.name) ?? 0) + 1); } - const hasDuplicateReactPlugin = [...counts.values()].some((count) => count > 1); + const hasDuplicateReactPlugin = [...counts.values()].some( + (count) => count > 1, + ); if (hasDuplicateReactPlugin) { throw new Error( "[vinext] Duplicate @vitejs/plugin-react detected.\n" + @@ -2202,7 +2390,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { if (rscPluginPromise) { // Count top-level RSC plugins (name === "rsc") — each call to // the rsc() factory produces exactly one plugin with this name. - const rscRootPlugins = config.plugins.filter((p: any) => p && p.name === "rsc"); + const rscRootPlugins = config.plugins.filter( + (p: any) => p && p.name === "rsc", + ); if (rscRootPlugins.length > 1) { throw new Error( "[vinext] Duplicate @vitejs/plugin-rsc detected.\n" + @@ -2265,7 +2455,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // App Router virtual modules if (cleanId === VIRTUAL_RSC_ENTRY) return RESOLVED_RSC_ENTRY; if (cleanId === VIRTUAL_APP_SSR_ENTRY) return RESOLVED_APP_SSR_ENTRY; - if (cleanId === VIRTUAL_APP_BROWSER_ENTRY) return RESOLVED_APP_BROWSER_ENTRY; + if (cleanId === VIRTUAL_APP_BROWSER_ENTRY) + return RESOLVED_APP_BROWSER_ENTRY; if ( cleanId.endsWith("/" + VIRTUAL_RSC_ENTRY) || cleanId.endsWith("\\" + VIRTUAL_RSC_ENTRY) @@ -2297,10 +2488,18 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { } // App Router virtual modules if (id === RESOLVED_RSC_ENTRY && hasAppDir) { - const routes = await appRouter(appDir, nextConfig?.pageExtensions, fileMatcher); + const routes = await appRouter( + appDir, + nextConfig?.pageExtensions, + fileMatcher, + ); const metaRoutes = scanMetadataFiles(appDir); // Check for global-error.tsx at app root - const globalErrorPath = findFileWithExts(appDir, "global-error", fileMatcher); + const globalErrorPath = findFileWithExts( + appDir, + "global-error", + fileMatcher, + ); return generateRscEntry( appDir, routes, @@ -2333,7 +2532,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Stub node:async_hooks in client builds — see src/plugins/async-hooks-stub.ts asyncHooksStubPlugin, // Dedup client references from RSC proxy modules — see src/plugins/client-reference-dedup.ts - ...(options.experimental?.clientReferenceDedup ? [clientReferenceDedupPlugin()] : []), + ...(options.experimental?.clientReferenceDedup + ? [clientReferenceDedupPlugin()] + : []), // Proxy plugin for @mdx-js/rollup. The real MDX plugin is created lazily // during vinext:config's config() (when MDX files are detected), but // plugins returned from config() hooks run too late in the pipeline — @@ -2369,7 +2570,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { enforce: "pre", resolveId(id) { - if (id === "virtual:vinext-react-canary") return "\0virtual:vinext-react-canary"; + if (id === "virtual:vinext-react-canary") + return "\0virtual:vinext-react-canary"; }, load(id) { @@ -2392,7 +2594,10 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Quick check: does this file reference canary APIs and import from "react"? if ( - !(code.includes("ViewTransition") || code.includes("addTransitionType")) || + !( + code.includes("ViewTransition") || + code.includes("addTransitionType") + ) || !/from\s+['"]react['"]/.test(code) ) { return null; @@ -2406,7 +2611,10 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Rewrite all `from "react"` / `from 'react'` to use the canary shim. // This is safe because the virtual module re-exports everything from // react, so non-canary imports continue to work. - const result = code.replace(/from\s*['"]react['"]/g, 'from "virtual:vinext-react-canary"'); + const result = code.replace( + /from\s*['"]react['"]/g, + 'from "virtual:vinext-react-canary"', + ); if (result !== code) { return { code: result, map: null }; } @@ -2422,9 +2630,16 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // which may not be tracked in Vite's module graph. Explicitly // sending full-reload ensures changes are always reflected in // the browser. - hotUpdate(options: { file: string; server: ViteDevServer; modules: any[] }) { + hotUpdate(options: { + file: string; + server: ViteDevServer; + modules: any[]; + }) { if (!hasPagesDir || hasAppDir) return; - if (options.file.startsWith(pagesDir) && fileMatcher.extensionRegex.test(options.file)) { + if ( + options.file.startsWith(pagesDir) && + fileMatcher.extensionRegex.test(options.file) + ) { options.server.environments.client.hot.send({ type: "full-reload" }); return []; } @@ -2450,12 +2665,15 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // The runner is created lazily on first use so that all environments are // fully registered before we inspect them. We prefer "ssr", then any // non-"rsc" environment, then whatever is available. - let pagesRunner: import("vite/module-runner").ModuleRunner | null = null; + let pagesRunner: import("vite/module-runner").ModuleRunner | null = + null; function getPagesRunner() { if (!pagesRunner) { const env = server.environments["ssr"] ?? - Object.values(server.environments).find((e) => e !== server.environments["rsc"]) ?? + Object.values(server.environments).find( + (e) => e !== server.environments["rsc"], + ) ?? Object.values(server.environments)[0]; pagesRunner = createDirectRunner(env); } @@ -2482,19 +2700,35 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { } server.watcher.on("add", (filePath: string) => { - if (hasPagesDir && filePath.startsWith(pagesDir) && pageExtensions.test(filePath)) { + if ( + hasPagesDir && + filePath.startsWith(pagesDir) && + pageExtensions.test(filePath) + ) { invalidateRouteCache(pagesDir); } - if (hasAppDir && filePath.startsWith(appDir) && pageExtensions.test(filePath)) { + if ( + hasAppDir && + filePath.startsWith(appDir) && + pageExtensions.test(filePath) + ) { invalidateAppRouteCache(); invalidateRscEntryModule(); } }); server.watcher.on("unlink", (filePath: string) => { - if (hasPagesDir && filePath.startsWith(pagesDir) && pageExtensions.test(filePath)) { + if ( + hasPagesDir && + filePath.startsWith(pagesDir) && + pageExtensions.test(filePath) + ) { invalidateRouteCache(pagesDir); } - if (hasAppDir && filePath.startsWith(appDir) && pageExtensions.test(filePath)) { + if ( + hasAppDir && + filePath.startsWith(appDir) && + pageExtensions.test(filePath) + ) { invalidateAppRouteCache(); invalidateRscEntryModule(); } @@ -2510,14 +2744,22 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { { origin: req.headers.origin as string | undefined, host: req.headers.host, - "x-forwarded-host": req.headers["x-forwarded-host"] as string | undefined, - "sec-fetch-site": req.headers["sec-fetch-site"] as string | undefined, - "sec-fetch-mode": req.headers["sec-fetch-mode"] as string | undefined, + "x-forwarded-host": req.headers["x-forwarded-host"] as + | string + | undefined, + "sec-fetch-site": req.headers["sec-fetch-site"] as + | string + | undefined, + "sec-fetch-mode": req.headers["sec-fetch-mode"] as + | string + | undefined, }, nextConfig?.allowedDevOrigins, ); if (blockReason) { - console.warn(`[vinext] Blocked dev request: ${blockReason} (${req.url})`); + console.warn( + `[vinext] Blocked dev request: ${blockReason} (${req.url})`, + ); res.writeHead(403, { "Content-Type": "text/plain" }); res.end("Forbidden"); return; @@ -2549,9 +2791,11 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // calls environment.fetchModule() directly and never touches the hot // channel, making it safe with all Vite plugin combinations. if (instrumentationPath && !hasAppDir) { - runInstrumentation(getPagesRunner(), instrumentationPath).catch((err) => { - console.error("[vinext] Instrumentation error:", err); - }); + runInstrumentation(getPagesRunner(), instrumentationPath).catch( + (err) => { + console.error("[vinext] Instrumentation error:", err); + }, + ); } // App Router request logging in dev server // @@ -2572,7 +2816,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { url.startsWith("/@") || url.startsWith("/__vite") || url.startsWith("/node_modules") || - (url.includes(".") && !pathname.endsWith(".html") && !pathname.endsWith(".rsc")) + (url.includes(".") && + !pathname.endsWith(".html") && + !pathname.endsWith(".rsc")) ) { return next(); } @@ -2610,7 +2856,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { inHandlerCompileMs !== -1 ) { _compileMs = - Math.max(0, Math.round(handlerStart - _reqStart)) + inHandlerCompileMs; + Math.max(0, Math.round(handlerStart - _reqStart)) + + inHandlerCompileMs; } if (!Number.isNaN(renderMs) && renderMs !== -1) { _renderMs = renderMs; @@ -2638,7 +2885,11 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { } // Pull timing out of the headers object when present. - if (headers && typeof headers === "object" && !Array.isArray(headers)) { + if ( + headers && + typeof headers === "object" && + !Array.isArray(headers) + ) { const timingKey = Object.keys(headers).find( (k) => k.toLowerCase() === "x-vinext-timing", ); @@ -2714,14 +2965,22 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { { origin: req.headers.origin as string | undefined, host: req.headers.host, - "x-forwarded-host": req.headers["x-forwarded-host"] as string | undefined, - "sec-fetch-site": req.headers["sec-fetch-site"] as string | undefined, - "sec-fetch-mode": req.headers["sec-fetch-mode"] as string | undefined, + "x-forwarded-host": req.headers["x-forwarded-host"] as + | string + | undefined, + "sec-fetch-site": req.headers["sec-fetch-site"] as + | string + | undefined, + "sec-fetch-mode": req.headers["sec-fetch-mode"] as + | string + | undefined, }, nextConfig?.allowedDevOrigins, ); if (blockReason) { - console.warn(`[vinext] Blocked dev request: ${blockReason} (${url})`); + console.warn( + `[vinext] Blocked dev request: ${blockReason} (${url})`, + ); res.writeHead(403, { "Content-Type": "text/plain" }); res.end("Forbidden"); return; @@ -2748,17 +3007,28 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { imgUrl.startsWith("/node_modules") ) { res.writeHead(400); - res.end(!rawImgUrl ? "Missing url parameter" : "Only relative URLs allowed"); + res.end( + !rawImgUrl + ? "Missing url parameter" + : "Only relative URLs allowed", + ); return; } // Validate the constructed URL's origin hasn't changed (defense in depth). - const resolvedImg = new URL(imgUrl, `http://${req.headers.host || "localhost"}`); - if (resolvedImg.origin !== `http://${req.headers.host || "localhost"}`) { + const resolvedImg = new URL( + imgUrl, + `http://${req.headers.host || "localhost"}`, + ); + if ( + resolvedImg.origin !== + `http://${req.headers.host || "localhost"}` + ) { res.writeHead(400); res.end("Only relative URLs allowed"); return; } - const encodedLocation = resolvedImg.pathname + resolvedImg.search; + const encodedLocation = + resolvedImg.pathname + resolvedImg.search; res.writeHead(302, { Location: encodedLocation }); res.end(); return; @@ -2795,7 +3065,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // decodeURIComponent prevents /%61dmin bypassing /admin matchers. // normalizePath collapses // and resolves . / .. segments. try { - pathname = normalizePath(normalizePathnameForRouteMatchStrict(pathname)); + pathname = normalizePath( + normalizePathnameForRouteMatchStrict(pathname), + ); } catch { // Malformed percent-encoding (e.g. /%E0%A4%A) — return 400 instead of crashing. res.writeHead(400); @@ -2831,14 +3103,18 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { const hasTrailing = pathname.endsWith("/"); if (nextConfig.trailingSlash && !hasTrailing) { // trailingSlash: true — redirect /about → /about/ - const qs = url.includes("?") ? url.slice(url.indexOf("?")) : ""; + const qs = url.includes("?") + ? url.slice(url.indexOf("?")) + : ""; const dest = bp + pathname + "/" + qs; res.writeHead(308, { Location: dest }); res.end(); return; } else if (!nextConfig.trailingSlash && hasTrailing) { // trailingSlash: false (default) — redirect /about/ → /about - const qs = url.includes("?") ? url.slice(url.indexOf("?")) : ""; + const qs = url.includes("?") + ? url.slice(url.indexOf("?")) + : ""; const dest = bp + pathname.replace(/\/+$/, "") + qs; res.writeHead(308, { Location: dest }); res.end(); @@ -2861,15 +3137,21 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { Object.fromEntries( Object.entries(req.headers) .filter(([, v]) => v !== undefined) - .map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : String(v)]), + .map(([k, v]) => [ + k, + Array.isArray(v) ? v.join(", ") : String(v), + ]), ), ); const requestOrigin = `http://${req.headers.host || "localhost"}`; const preMiddlewareReqUrl = new URL(url, requestOrigin); - const preMiddlewareReqCtx: RequestContext = requestContextFromRequest( - new Request(preMiddlewareReqUrl, { headers: nodeRequestHeaders }), - ); + const preMiddlewareReqCtx: RequestContext = + requestContextFromRequest( + new Request(preMiddlewareReqUrl, { + headers: nodeRequestHeaders, + }), + ); // Config redirects run before middleware, but still match against // the original normalized pathname and request headers/cookies. @@ -2884,7 +3166,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { if (redirected) return; } - const applyRequestHeadersToNodeRequest = (nextRequestHeaders: Headers) => { + const applyRequestHeadersToNodeRequest = ( + nextRequestHeaders: Headers, + ) => { for (const key of Object.keys(req.headers)) { delete req.headers[key]; } @@ -2909,13 +3193,18 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Only trust X-Forwarded-Proto when behind a trusted proxy const devTrustProxy = process.env.VINEXT_TRUST_PROXY === "1" || - (process.env.VINEXT_TRUSTED_HOSTS ?? "").split(",").some((h) => h.trim()); + (process.env.VINEXT_TRUSTED_HOSTS ?? "") + .split(",") + .some((h) => h.trim()); const rawProto = devTrustProxy ? String(req.headers["x-forwarded-proto"] || "") .split(",")[0] .trim() : ""; - const mwProto = rawProto === "https" || rawProto === "http" ? rawProto : "http"; + const mwProto = + rawProto === "https" || rawProto === "http" + ? rawProto + : "http"; const origin = `${mwProto}://${req.headers.host || "localhost"}`; const middlewareRequest = new Request(new URL(url, origin), { method: req.method, @@ -2946,7 +3235,10 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { } } } - res.writeHead(result.redirectStatus ?? 307, redirectHeaders); + res.writeHead( + result.redirectStatus ?? 307, + redirectHeaders, + ); res.end(); return; } @@ -2955,7 +3247,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { for (const [key, value] of result.response.headers) { res.appendHeader(key, value); } - const body = Buffer.from(await result.response.arrayBuffer()); + const body = Buffer.from( + await result.response.arrayBuffer(), + ); res.end(body); return; } @@ -2975,10 +3269,11 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { } } - middlewareRequestHeaders = buildRequestHeadersFromMiddlewareResponse( - currentRequestHeaders, - result.responseHeaders, - ); + middlewareRequestHeaders = + buildRequestHeadersFromMiddlewareResponse( + currentRequestHeaders, + result.responseHeaders, + ); if (middlewareRequestHeaders && !hasAppDir) { applyRequestHeadersToNodeRequest(middlewareRequestHeaders); @@ -3029,7 +3324,10 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { for (const [key, value] of result.responseHeaders) { // Exclude control headers that runMiddleware already // consumed — matches the RSC entry's inline filtering. - if (key !== "x-middleware-next" && key !== "x-middleware-rewrite") { + if ( + key !== "x-middleware-next" && + key !== "x-middleware-rewrite" + ) { mwCtxEntries.push([key, value]); } } @@ -3047,7 +3345,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Convert Node.js IncomingMessage headers to a Web Request for // requestContextFromRequest(), which uses the standard Web API. const reqUrl = new URL(url, requestOrigin); - const reqCtxHeaders = middlewareRequestHeaders ?? nodeRequestHeaders; + const reqCtxHeaders = + middlewareRequestHeaders ?? nodeRequestHeaders; const reqCtx: RequestContext = requestContextFromRequest( new Request(reqUrl, { headers: reqCtxHeaders }), ); @@ -3057,14 +3356,23 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // pre-middleware request state; middleware response headers win // later because they are already on the outgoing response. if (nextConfig?.headers.length) { - applyHeaders(pathname, res, nextConfig.headers, preMiddlewareReqCtx); + applyHeaders( + pathname, + res, + nextConfig.headers, + preMiddlewareReqCtx, + ); } // Apply rewrites from next.config.js (beforeFiles) let resolvedUrl = url; if (nextConfig?.rewrites.beforeFiles.length) { resolvedUrl = - applyRewrites(pathname, nextConfig.rewrites.beforeFiles, reqCtx) ?? url; + applyRewrites( + pathname, + nextConfig.rewrites.beforeFiles, + reqCtx, + ) ?? url; } // External rewrite from beforeFiles — proxy to external URL @@ -3076,7 +3384,10 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Handle API routes first (pages/api/*) const resolvedPathname = resolvedUrl.split("?")[0]; - if (resolvedPathname.startsWith("/api/") || resolvedPathname === "/api") { + if ( + resolvedPathname.startsWith("/api/") || + resolvedPathname === "/api" + ) { const apiRoutes = await apiRouter( pagesDir, nextConfig?.pageExtensions, @@ -3107,7 +3418,11 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { return; } - const routes = await pagesRouter(pagesDir, nextConfig?.pageExtensions, fileMatcher); + const routes = await pagesRouter( + pagesDir, + nextConfig?.pageExtensions, + fileMatcher, + ); // Apply afterFiles rewrites — these run after initial route matching // If beforeFiles already rewrote the URL, afterFiles still run on the @@ -3166,7 +3481,10 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { await proxyExternalRewriteNode(req, res, fallbackRewrite); return; } - const fallbackMatch = matchRoute(fallbackRewrite.split("?")[0], routes); + const fallbackMatch = matchRoute( + fallbackRewrite.split("?")[0], + routes, + ); if (!fallbackMatch && hasAppDir) { return next(); } @@ -3212,7 +3530,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { if (!id.startsWith(pagesDir)) return null; // Skip API routes, _app, _document, _error const relativePath = id.slice(pagesDir.length); - if (relativePath.startsWith("/api/") || relativePath === "/api") return null; + if (relativePath.startsWith("/api/") || relativePath === "/api") + return null; if (/\/_(?:app|document|error)\b/.test(relativePath)) return null; const result = stripServerExports(code); @@ -3277,7 +3596,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { include: /\.(tsx?|jsx?|mjs)$/, exclude: /node_modules/, }, - code: new RegExp(`import\\s+\\w+\\s+from\\s+['"][^'"]+\\.(${IMAGE_EXTS})['"]`), + code: new RegExp( + `import\\s+\\w+\\s+from\\s+['"][^'"]+\\.(${IMAGE_EXTS})['"]`, + ), }, async handler(code, id) { // Defensive guard — duplicates filter logic @@ -3373,10 +3694,12 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Match font constructor calls: Inter({ weight: ..., subsets: ... }) // We look for PascalCase or Name_Name identifiers followed by ({...}) // This regex captures the font name and the options object literal - const fontCallRe = /\b([A-Z][A-Za-z]*(?:_[A-Z][A-Za-z]*)*)\s*\(\s*(\{[^}]*\})\s*\)/g; + const fontCallRe = + /\b([A-Z][A-Za-z]*(?:_[A-Z][A-Za-z]*)*)\s*\(\s*(\{[^}]*\})\s*\)/g; // Also need to verify these names came from next/font/google import - const importRe = /import\s*\{([^}]+)\}\s*from\s*['"]next\/font\/google['"]/; + const importRe = + /import\s*\{([^}]+)\}\s*from\s*['"]next\/font\/google['"]/; const importMatch = code.match(importRe); if (!importMatch) return null; @@ -3399,7 +3722,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { if (!importedNames.has(fontName)) continue; // Convert PascalCase/Underscore to font family - const family = fontName.replace(/_/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2"); + const family = fontName + .replace(/_/g, " ") + .replace(/([a-z])([A-Z])/g, "$1 $2"); // Parse options safely via AST — no eval/new Function // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -3467,7 +3792,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { const closingBrace = optionsStr.lastIndexOf("}"); const optionsWithCSS = optionsStr.slice(0, closingBrace) + - (optionsStr.slice(0, closingBrace).trim().endsWith("{") ? "" : ", ") + + (optionsStr.slice(0, closingBrace).trim().endsWith("{") + ? "" + : ", ") + `_selfHostedCSS: ${escapedCSS}` + optionsStr.slice(closingBrace); @@ -3483,7 +3810,11 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { }; }, }, - } as Plugin & { _isBuild: boolean; _fontCache: Map; _cacheDir: string }, + } as Plugin & { + _isBuild: boolean; + _fontCache: Map; + _cacheDir: string; + }, // Local font path resolution: // When a source file calls localFont({ src: "./font.woff2" }) or // localFont({ src: [{ path: "./font.woff2" }] }), the relative paths @@ -3524,7 +3855,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Match font file paths in `path: "..."` or `src: "..."` properties. // Captures: (1) property+colon prefix, (2) quote char, (3) the path. - const fontPathRe = /((?:path|src)\s*:\s*)(['"])([^'"]+\.(?:woff2?|ttf|otf|eot))\2/g; + const fontPathRe = + /((?:path|src)\s*:\s*)(['"])([^'"]+\.(?:woff2?|ttf|otf|eot))\2/g; let match; while ((match = fontPathRe.exec(code)) !== null) { @@ -3604,7 +3936,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // fn.body is a BlockStatement node ({type:"BlockStatement", body:Statement[]}), not // a raw array. Unwrap it. Arrow functions with expression bodies have a non-array // .body — the BlockStatement check handles that case (body.body would be undefined). - const stmts = fn?.body?.type === "BlockStatement" ? fn.body.body : null; + const stmts = + fn?.body?.type === "BlockStatement" ? fn.body.body : null; if (Array.isArray(stmts)) { for (const stmt of stmts) { if ( @@ -3633,9 +3966,18 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { } // Walk into variable declarations, export declarations, etc. for (const key of Object.keys(node)) { - if (key === "type" || key === "start" || key === "end" || key === "loc") continue; + if ( + key === "type" || + key === "start" || + key === "end" || + key === "loc" + ) + continue; const child = node[key]; - if (Array.isArray(child) && child.some((c) => c && typeof c === "object")) { + if ( + Array.isArray(child) && + child.some((c) => c && typeof c === "object") + ) { if (astHasInlineCache(child)) return true; } else if (child && typeof child === "object" && child.type) { if (astHasInlineCache([child])) return true; @@ -3644,7 +3986,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { } return false; } - const hasInlineCache = !cacheDirective && astHasInlineCache(ast.body as any[]); + const hasInlineCache = + !cacheDirective && astHasInlineCache(ast.body as any[]); if (!cacheDirective && !hasInlineCache) return null; @@ -3656,9 +3999,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { " @vitejs/plugin-rsc", ); } - const { transformWrapExport, transformHoistInlineDirective } = await import( - pathToFileURL(resolvedRscTransformsPath).href - ); + const { transformWrapExport, transformHoistInlineDirective } = + await import(pathToFileURL(resolvedRscTransformsPath).href); if (cacheDirective) { // File-level "use cache" — wrap function exports with @@ -3669,14 +4011,18 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { const variant = directiveValue === "use cache" ? "" - : directiveValue.replace("use cache:", "").replace("use cache: ", "").trim(); + : directiveValue + .replace("use cache:", "") + .replace("use cache: ", "") + .trim(); // Only skip default export wrapping for layouts and templates — // they receive {children} from the framework which requires // temporary reference handling that registerCachedFunction doesn't // support yet. Pages, not-found, loading, error, and default are // leaf components with no {children} prop and can be cached directly. - const isLayoutOrTemplate = /\/(layout|template)\.(tsx?|jsx?|mjs)$/.test(id); + const isLayoutOrTemplate = + /\/(layout|template)\.(tsx?|jsx?|mjs)$/.test(id); const runtimeModuleUrl = pathToFileURL( resolveShimModulePath(shimsDir, "cache-runtime"), @@ -3740,7 +4086,10 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { const variant = directiveMatch === "use cache" ? "" - : directiveMatch.replace("use cache:", "").replace("use cache: ", "").trim(); + : directiveMatch + .replace("use cache:", "") + .replace("use cache: ", "") + .trim(); return `(await import(${JSON.stringify(runtimeModuleUrl2)})).registerCachedFunction(${value}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)})`; }, rejectNonAsyncFunction: false, @@ -3921,7 +4270,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { const ogAssets = ["resvg.wasm"]; // Only copy if the bundle actually references these files - const referencedAssets = ogAssets.filter((asset) => content.includes(asset)); + const referencedAssets = ogAssets.filter((asset) => + content.includes(asset), + ); if (referencedAssets.length === 0) return; // Find @vercel/og in node_modules @@ -3967,7 +4318,10 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { contentSecurityPolicy: nextConfig?.images?.contentSecurityPolicy, }; - fs.writeFileSync(path.join(outDir, "image-config.json"), JSON.stringify(imageConfig)); + fs.writeFileSync( + path.join(outDir, "image-config.json"), + JSON.stringify(imageConfig), + ); }, }, }, @@ -4002,7 +4356,10 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { if (!outDir) return; const manifest = { prerenderSecret }; - fs.writeFileSync(path.join(outDir, "vinext-server.json"), JSON.stringify(manifest)); + fs.writeFileSync( + path.join(outDir, "vinext-server.json"), + JSON.stringify(manifest), + ); }, }, }; @@ -4026,10 +4383,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { if (!fs.existsSync(ssrManifestPath)) return; try { - const ssrManifest = JSON.parse(fs.readFileSync(ssrManifestPath, "utf-8")) as Record< - string, - string[] - >; + const ssrManifest = JSON.parse( + fs.readFileSync(ssrManifestPath, "utf-8"), + ) as Record; const buildRoot = this.environment?.config.root ?? process.cwd(); const buildBase = this.environment?.config.base ?? "/"; const augmentedManifest = augmentSsrManifestFromBundle( @@ -4038,7 +4394,10 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { buildRoot, buildBase, ); - fs.writeFileSync(ssrManifestPath, JSON.stringify(augmentedManifest, null, 2)); + fs.writeFileSync( + ssrManifestPath, + JSON.stringify(augmentedManifest, null, 2), + ); } catch (err) { // Leave Vite's manifest untouched if parsing fails. console.warn("[vinext] Failed to augment SSR manifest:", err); @@ -4083,17 +4442,32 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // App Router gets its client entry via the RSC plugin instead. let lazyChunksData: string[] | null = null; let clientEntryFile: string | null = null; - const buildManifestPath = path.join(clientDir, ".vite", "manifest.json"); + const buildManifestPath = path.join( + clientDir, + ".vite", + "manifest.json", + ); if (fs.existsSync(buildManifestPath)) { try { - const buildManifest = JSON.parse(fs.readFileSync(buildManifestPath, "utf-8")); - for (const [, value] of Object.entries(buildManifest) as [string, any][]) { + const buildManifest = JSON.parse( + fs.readFileSync(buildManifestPath, "utf-8"), + ); + for (const [, value] of Object.entries(buildManifest) as [ + string, + any, + ][]) { if (value && value.isEntry && value.file) { - clientEntryFile = manifestFileWithBase(value.file, clientBase); + clientEntryFile = manifestFileWithBase( + value.file, + clientBase, + ); break; } } - const lazy = manifestFilesWithBase(computeLazyChunks(buildManifest), clientBase); + const lazy = manifestFilesWithBase( + computeLazyChunks(buildManifest), + clientBase, + ); if (lazy.length > 0) lazyChunksData = lazy; } catch { /* ignore parse errors */ @@ -4102,10 +4476,16 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Read SSR manifest for per-page CSS/JS injection let ssrManifestData: Record | null = null; - const ssrManifestPath = path.join(clientDir, ".vite", "ssr-manifest.json"); + const ssrManifestPath = path.join( + clientDir, + ".vite", + "ssr-manifest.json", + ); if (fs.existsSync(ssrManifestPath)) { try { - ssrManifestData = JSON.parse(fs.readFileSync(ssrManifestPath, "utf-8")); + ssrManifestData = JSON.parse( + fs.readFileSync(ssrManifestPath, "utf-8"), + ); } catch { /* ignore parse errors */ } @@ -4117,7 +4497,10 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // __VINEXT_LAZY_CHUNKS__ and __VINEXT_SSR_MANIFEST__ into the // worker entry at dist/server/index.js. const workerEntry = path.resolve(distDir, "server", "index.js"); - if (fs.existsSync(workerEntry) && (lazyChunksData || ssrManifestData)) { + if ( + fs.existsSync(workerEntry) && + (lazyChunksData || ssrManifestData) + ) { let code = fs.readFileSync(workerEntry, "utf-8"); const globals: string[] = []; if (ssrManifestData) { @@ -4162,10 +4545,15 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { const files = fs.readdirSync(assetsDir); const entry = files.find( (f: string) => - (f.includes("vinext-client-entry") || f.includes("vinext-app-browser-entry")) && + (f.includes("vinext-client-entry") || + f.includes("vinext-app-browser-entry")) && f.endsWith(".js"), ); - if (entry) clientEntryFile = manifestFileWithBase("assets/" + entry, clientBase); + if (entry) + clientEntryFile = manifestFileWithBase( + "assets/" + entry, + clientBase, + ); } } @@ -4232,7 +4620,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { name: "vinext:og-font-patch", enforce: "pre" as const, transform(code: string, id: string) { - if (!id.includes("@vercel/og") || !id.includes("index.edge.js")) return null; + if (!id.includes("@vercel/og") || !id.includes("index.edge.js")) + return null; let result = code; // ── Extract yoga WASM and import via ?module ────────────────────────────────── @@ -4240,7 +4629,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // then later calls WebAssembly.instantiate(bytes, imports), which workerd rejects. // Emscripten supports a custom h2.instantiateWasm(imports, callback) escape hatch // that we inject to use a pre-compiled WebAssembly.Module loaded via ?module. - const YOGA_DATA_URL_RE = /H = "data:application\/octet-stream;base64,([A-Za-z0-9+/]+=*)";/; + const YOGA_DATA_URL_RE = + /H = "data:application\/octet-stream;base64,([A-Za-z0-9+/]+=*)";/; const yogaMatch = YOGA_DATA_URL_RE.exec(result); if (yogaMatch) { const yogaBase64 = yogaMatch[1]; @@ -4260,7 +4650,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { ` return {}; } })`; result = result.replace(YOGA_CALL, YOGA_CALL_PATCHED); // Prepend the yoga wasm ?module import so @cloudflare/vite-plugin handles it - result = `import yoga_wasm_module from "./yoga.wasm?module";\n` + result; + result = + `import yoga_wasm_module from "./yoga.wasm?module";\n` + result; } if (result === code) return null; @@ -4309,7 +4700,11 @@ export { matchConfigPattern } from "./config/config-matchers.js"; * Modeled after Next.js's SWC `next-ssg-transform`. */ function stripServerExports(code: string): string | null { - const SERVER_EXPORTS = new Set(["getServerSideProps", "getStaticProps", "getStaticPaths"]); + const SERVER_EXPORTS = new Set([ + "getServerSideProps", + "getStaticProps", + "getStaticPaths", + ]); if (![...SERVER_EXPORTS].some((name) => code.includes(name))) return null; let ast: ReturnType; @@ -4330,7 +4725,10 @@ function stripServerExports(code: string): string | null { // Case 2: export const/let/var name = ... if (node.declaration) { const decl = node.declaration; - if (decl.type === "FunctionDeclaration" && SERVER_EXPORTS.has(decl.id?.name)) { + if ( + decl.type === "FunctionDeclaration" && + SERVER_EXPORTS.has(decl.id?.name) + ) { s.overwrite( node.start, node.end, @@ -4339,8 +4737,15 @@ function stripServerExports(code: string): string | null { changed = true; } else if (decl.type === "VariableDeclaration") { for (const declarator of decl.declarations) { - if (declarator.id?.type === "Identifier" && SERVER_EXPORTS.has(declarator.id.name)) { - s.overwrite(node.start, node.end, `export const ${declarator.id.name} = undefined;`); + if ( + declarator.id?.type === "Identifier" && + SERVER_EXPORTS.has(declarator.id.name) + ) { + s.overwrite( + node.start, + node.end, + `export const ${declarator.id.name} = undefined;`, + ); changed = true; } } @@ -4402,7 +4807,9 @@ function applyRedirects( if (result) { // Sanitize to prevent open redirect via protocol-relative URLs const dest = sanitizeDestination( - basePath && !isExternalUrl(result.destination) && !hasBasePath(result.destination, basePath) + basePath && + !isExternalUrl(result.destination) && + !hasBasePath(result.destination, basePath) ? basePath + result.destination : result.destination, ); @@ -4451,7 +4858,9 @@ async function proxyExternalRewriteNode( proxyResponse.headers.forEach((value, key) => { const existing = nodeHeaders[key]; if (existing !== undefined) { - nodeHeaders[key] = Array.isArray(existing) ? [...existing, value] : [existing, value]; + nodeHeaders[key] = Array.isArray(existing) + ? [...existing, value] + : [existing, value]; } else { nodeHeaders[key] = value; } @@ -4560,7 +4969,11 @@ const _mdxScanCache = new Map(); /** * Check if the project has .mdx files in app/ or pages/ directories. */ -function hasMdxFiles(root: string, appDir: string | null, pagesDir: string | null): boolean { +function hasMdxFiles( + root: string, + appDir: string | null, + pagesDir: string | null, +): boolean { const cacheKey = `${root}\0${appDir ?? ""}\0${pagesDir ?? ""}`; if (_mdxScanCache.has(cacheKey)) return _mdxScanCache.get(cacheKey)!; const dirs = [appDir, pagesDir].filter(Boolean) as string[]; From 45cd35519638cae51b23b2806dcc5d403bfc26d3 Mon Sep 17 00:00:00 2001 From: Md Yunus Date: Sun, 22 Mar 2026 22:43:19 +0530 Subject: [PATCH 06/11] fix: resolve import conflict and formatting --- packages/vinext/src/index.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index deda5bc98..80e2f9faf 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -1035,6 +1035,27 @@ function createDirectiveOnwarn( }; } +/** + * Helper to suppress "Module level directives cause errors when bundled" + * warnings for "use client" / "use server" directives. + */ +function createDirectiveOnwarn(userOnwarn?: (warning: any, handler: (w: any) => void) => void) { + return (warning: any, defaultHandler: (warning: any) => void) => { + if ( + warning.code === "MODULE_LEVEL_DIRECTIVE" && + (warning.message?.includes('"use client"') || + warning.message?.includes('"use server"')) + ) { + return; + } + if (userOnwarn) { + userOnwarn(warning, defaultHandler); + } else { + defaultHandler(warning); + } + }; +} + export default function vinext(options: VinextOptions = {}): PluginOption[] { const viteMajorVersion = getViteMajorVersion(); let root: string; From 719778e28247c7c1951195f737faad5c7672d427 Mon Sep 17 00:00:00 2001 From: Md Yunus Date: Sun, 22 Mar 2026 22:46:36 +0530 Subject: [PATCH 07/11] fix: resolve duplicate import conflict --- packages/vinext/src/index.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 80e2f9faf..e8ef2f1f6 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -59,11 +59,8 @@ import { import { hasBasePath } from "./utils/base-path.js"; import { asyncHooksStubPlugin } from "./plugins/async-hooks-stub.js"; import { clientReferenceDedupPlugin } from "./plugins/client-reference-dedup.js"; -import { - hasWranglerConfig, - formatMissingCloudflarePluginError, -} from "./deploy.js"; -import tsconfigPaths from "vite-tsconfig-paths"; +import { createOptimizeImportsPlugin } from "./plugins/optimize-imports.js"; +import { hasWranglerConfig, formatMissingCloudflarePluginError } from "./deploy.js"; import type { Options as VitePluginReactOptions } from "@vitejs/plugin-react"; import MagicString from "magic-string"; import path from "node:path"; From bec552d09617819b670ed120e6355ed05291d7da Mon Sep 17 00:00:00 2001 From: Md Yunus Date: Sun, 22 Mar 2026 22:53:24 +0530 Subject: [PATCH 08/11] fix: remove duplicate createDirectiveOnwarn declaration --- packages/vinext/src/index.ts | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index e8ef2f1f6..14c2a1ebf 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -1009,29 +1009,6 @@ export interface VinextOptions { }; } -/** - * Helper to suppress "Module level directives cause errors when bundled" - * warnings for "use client" / "use server" directives. - */ -function createDirectiveOnwarn( - userOnwarn?: (warning: any, handler: (w: any) => void) => void, -) { - return (warning: any, defaultHandler: (warning: any) => void) => { - if ( - warning.code === "MODULE_LEVEL_DIRECTIVE" && - (warning.message?.includes('"use client"') || - warning.message?.includes('"use server"')) - ) { - return; - } - if (userOnwarn) { - userOnwarn(warning, defaultHandler); - } else { - defaultHandler(warning); - } - }; -} - /** * Helper to suppress "Module level directives cause errors when bundled" * warnings for "use client" / "use server" directives. From 89c24ea26a91b6e610b94e6575f92c2efe7b9375 Mon Sep 17 00:00:00 2001 From: Md Yunus Date: Mon, 23 Mar 2026 20:13:36 +0530 Subject: [PATCH 09/11] fix: update tests for Vite 8 version-gated config - Add getViteMajorVersion import to build-optimization.test.ts - Update treeshake config tests to handle both Vite 7 and Vite 8 paths - Update experimentalMinChunkSize test to check for undefined on Vite 8+ - Add missing tsconfigPaths import to index.ts (was accidentally removed) Signed-off-by: Md Yunus Co-authored-by: Qwen-Coder --- packages/vinext/src/index.ts | 1 + tests/build-optimization.test.ts | 70 +++++++++++++++++++++++--------- 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 14c2a1ebf..4b872186a 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -69,6 +69,7 @@ import { createRequire } from "node:module"; import fs from "node:fs"; import { randomBytes } from "node:crypto"; import commonjs from "vite-plugin-commonjs"; +import tsconfigPaths from "vite-tsconfig-paths"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); type VitePluginReactModule = typeof import("@vitejs/plugin-react"); diff --git a/tests/build-optimization.test.ts b/tests/build-optimization.test.ts index 14b60edf7..a7096c1ab 100644 --- a/tests/build-optimization.test.ts +++ b/tests/build-optimization.test.ts @@ -19,6 +19,7 @@ import { _asyncHooksStubPlugin, getClientOutputConfig, getClientTreeshakeConfig, + getViteMajorVersion, } from "../packages/vinext/src/index.js"; // The vinext config hook mutates process.env.NODE_ENV as a side effect (matching @@ -424,11 +425,20 @@ describe("treeshake config integration", () => { }; const result = await (mainPlugin as any).config(mockConfig, { command: "build" }); - // treeshake should be set on rollupOptions for non-SSR builds - expect(result.build.rollupOptions.treeshake).toEqual({ - preset: "recommended", - moduleSideEffects: "no-external", - }); + // treeshake should be set for non-SSR builds (version-gated for Vite 8+) + const viteVersion = getViteMajorVersion(); + if (viteVersion >= 8) { + // Vite 8+ uses rolldownOptions + expect(result.build.rolldownOptions?.treeshake).toEqual({ + moduleSideEffects: "no-external", + }); + } else { + // Vite 7 uses rollupOptions + expect(result.build.rollupOptions.treeshake).toEqual({ + preset: "recommended", + moduleSideEffects: "no-external", + }); + } } finally { await fsp.rm(tmpDir, { recursive: true, force: true }).catch(() => {}); } @@ -513,18 +523,31 @@ describe("treeshake config integration", () => { }; const result = await (mainPlugin as any).config(mockConfig, { command: "build" }); - // Global rollupOptions should NOT have treeshake (would leak into RSC/SSR) - expect(result.build.rollupOptions?.treeshake).toBeUndefined(); - - // Client environment should have treeshake - expect(result.environments.client.build.rollupOptions.treeshake).toEqual({ - preset: "recommended", - moduleSideEffects: "no-external", - }); + // Global build should NOT have treeshake (would leak into RSC/SSR) + const viteVersion = getViteMajorVersion(); + if (viteVersion >= 8) { + expect(result.build.rolldownOptions?.treeshake).toBeUndefined(); + // Client environment should have treeshake (Vite 8+) + expect(result.environments.client.build.rolldownOptions?.treeshake).toEqual({ + moduleSideEffects: "no-external", + }); + } else { + expect(result.build.rollupOptions?.treeshake).toBeUndefined(); + // Client environment should have treeshake (Vite 7) + expect(result.environments.client.build.rollupOptions.treeshake).toEqual({ + preset: "recommended", + moduleSideEffects: "no-external", + }); + } // RSC and SSR environments should NOT have treeshake - expect(result.environments.rsc.build?.rollupOptions?.treeshake).toBeUndefined(); - expect(result.environments.ssr.build?.rollupOptions?.treeshake).toBeUndefined(); + if (viteVersion >= 8) { + expect(result.environments.rsc.build?.rolldownOptions?.treeshake).toBeUndefined(); + expect(result.environments.ssr.build?.rolldownOptions?.treeshake).toBeUndefined(); + } else { + expect(result.environments.rsc.build?.rollupOptions?.treeshake).toBeUndefined(); + expect(result.environments.ssr.build?.rollupOptions?.treeshake).toBeUndefined(); + } } finally { await fsp.rm(tmpDir, { recursive: true, force: true }).catch(() => {}); } @@ -563,10 +586,19 @@ describe("treeshake config integration", () => { const result = await (mainPlugin as any).config(mockConfig, { command: "build" }); // For standalone client builds (non-SSR, non-multi-env), - // output config should include experimentalMinChunkSize - const output = result.build.rollupOptions.output; - expect(output).toBeDefined(); - expect(output.experimentalMinChunkSize).toBe(10_000); + // output config should include experimentalMinChunkSize (Vite 7 only) + const viteVersion = getViteMajorVersion(); + if (viteVersion >= 8) { + // Vite 8+ doesn't support experimentalMinChunkSize + const output = result.build.rolldownOptions?.output; + expect(output).toBeDefined(); + expect((output as any).experimentalMinChunkSize).toBeUndefined(); + } else { + // Vite 7 includes experimentalMinChunkSize + const output = result.build.rollupOptions.output; + expect(output).toBeDefined(); + expect(output.experimentalMinChunkSize).toBe(10_000); + } } finally { await fsp.rm(tmpDir, { recursive: true, force: true }).catch(() => {}); } From f0e3905e0a886301c89c7b7de81af4a8e2a4ce3d Mon Sep 17 00:00:00 2001 From: Md Yunus Date: Mon, 23 Mar 2026 20:27:43 +0530 Subject: [PATCH 10/11] fix: consolidate getClientBuildOptions functions and fix type cast Issue 3: Consolidated getClientBuildOptions and getClientBuildOptionsWithInput into a single function with optional input parameter. Issue 4: Replaced broad 'as any' cast with narrower computed property key cast for rolldownOptions. - packages/vinext/src/index.ts: Consolidate functions, fix type cast - packages/vinext/src/cli.ts: Update import to use getClientBuildOptions Signed-off-by: Md Yunus Co-authored-by: Qwen-Coder --- packages/vinext/src/cli.ts | 4 +- packages/vinext/src/index.ts | 72 +++++++----------------------------- 2 files changed, 15 insertions(+), 61 deletions(-) diff --git a/packages/vinext/src/cli.ts b/packages/vinext/src/cli.ts index 9d6283baa..95c8b2bfc 100644 --- a/packages/vinext/src/cli.ts +++ b/packages/vinext/src/cli.ts @@ -13,7 +13,7 @@ * needed for most Next.js apps. */ -import vinext, { getClientBuildOptionsWithInput, getViteMajorVersion } from "./index.js"; +import vinext, { getClientBuildOptions, getViteMajorVersion } from "./index.js"; import { printBuildReport } from "./build/report.js"; import { runPrerender } from "./build/run-prerender.js"; import path from "node:path"; @@ -466,7 +466,7 @@ async function buildApp() { outDir: "dist/client", manifest: true, ssrManifest: true, - ...getClientBuildOptionsWithInput(viteMajorVersion, { + ...getClientBuildOptions(viteMajorVersion, { index: "virtual:vinext-client-entry", }), }, diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 4b872186a..350925a0e 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -648,50 +648,15 @@ function getClientTreeshakeConfig(viteVersion: number): { /** * Get build options config for client builds, version-gated for Vite 8/Rolldown. * Vite 7 uses build.rollupOptions, Vite 8+ uses build.rolldownOptions. + * @param viteVersion - Vite major version + * @param input - Optional input config for custom entry points */ -function getClientBuildOptions(viteVersion: number): { - rollupOptions?: { - input?: Record; - output: { - manualChunks: typeof clientManualChunks; - experimentalMinChunkSize?: number; - }; - treeshake: { preset?: "recommended"; moduleSideEffects: "no-external" }; - }; - rolldownOptions?: { - input?: Record; - output: { manualChunks: typeof clientManualChunks }; - treeshake: { moduleSideEffects: "no-external" }; - }; -} { - if (viteVersion >= 8) { - // Vite 8+ uses Rolldown - config goes under rolldownOptions - return { - rolldownOptions: { - output: getClientOutputConfig(viteVersion), - treeshake: getClientTreeshakeConfig(viteVersion), - }, - }; - } - // Vite 7 uses Rollup - config goes under rollupOptions - return { - rollupOptions: { - output: getClientOutputConfig(viteVersion), - treeshake: getClientTreeshakeConfig(viteVersion), - }, - }; -} - -/** - * Get build options config for client builds with custom input, version-gated for Vite 8/Rolldown. - * Vite 7 uses build.rollupOptions, Vite 8+ uses build.rolldownOptions. - */ -function getClientBuildOptionsWithInput( +function getClientBuildOptions( viteVersion: number, - input: Record, + input?: Record, ): { rollupOptions?: { - input: Record; + input?: Record; output: { manualChunks: typeof clientManualChunks; experimentalMinChunkSize?: number; @@ -699,25 +664,15 @@ function getClientBuildOptionsWithInput( treeshake: { preset?: "recommended"; moduleSideEffects: "no-external" }; }; rolldownOptions?: { - input: Record; + input?: Record; output: { manualChunks: typeof clientManualChunks }; treeshake: { moduleSideEffects: "no-external" }; }; } { - if (viteVersion >= 8) { - // Vite 8+ uses Rolldown - config goes under rolldownOptions - return { - rolldownOptions: { - input, - output: getClientOutputConfig(viteVersion), - treeshake: getClientTreeshakeConfig(viteVersion), - }, - }; - } - // Vite 7 uses Rollup - config goes under rollupOptions + const optionsKey = viteVersion >= 8 ? "rolldownOptions" : "rollupOptions"; return { - rollupOptions: { - input, + [optionsKey]: { + ...(input ? { input } : {}), output: getClientOutputConfig(viteVersion), treeshake: getClientTreeshakeConfig(viteVersion), }, @@ -2055,7 +2010,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { : viteMajorVersion >= 8 ? { build: { - rolldownOptions: { + ["rolldownOptions" as keyof import("vite").BuildOptions]: { ...getClientBuildOptions(viteMajorVersion) .rolldownOptions, onwarn: createDirectiveOnwarn( @@ -2063,7 +2018,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { config.build?.rollupOptions?.onwarn, ), }, - } as any, + }, } : { build: { @@ -2319,7 +2274,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // on every page — defeating code-splitting for React.lazy() and // next/dynamic boundaries. ...(hasCloudflarePlugin ? { manifest: true } : {}), - ...getClientBuildOptionsWithInput(viteMajorVersion, { + ...getClientBuildOptions(viteMajorVersion, { index: VIRTUAL_APP_BROWSER_ENTRY, }), }, @@ -2336,7 +2291,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { build: { manifest: true, ssrManifest: true, - ...getClientBuildOptionsWithInput(viteMajorVersion, { + ...getClientBuildOptions(viteMajorVersion, { index: VIRTUAL_CLIENT_ENTRY, }), }, @@ -5020,7 +4975,6 @@ export { clientTreeshakeConfig, computeLazyChunks, getClientBuildOptions, - getClientBuildOptionsWithInput, getClientOutputConfig, getClientTreeshakeConfig, }; From 701a42d5eb999e0a22b7023ca6e434bd7fcd3d6b Mon Sep 17 00:00:00 2001 From: Md Yunus Date: Mon, 23 Mar 2026 20:29:11 +0530 Subject: [PATCH 11/11] test: add Vite 8 multi-env build config test (Issue 5) Adds test coverage for Vite 8 multi-env builds to verify config is placed under rolldownOptions instead of rollupOptions. - tests/build-optimization.test.ts: Add multi-env client build config test Signed-off-by: Md Yunus Co-authored-by: Qwen-Coder --- tests/build-optimization.test.ts | 66 ++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/build-optimization.test.ts b/tests/build-optimization.test.ts index a7096c1ab..fb1d2928d 100644 --- a/tests/build-optimization.test.ts +++ b/tests/build-optimization.test.ts @@ -1700,3 +1700,69 @@ export const getStaticPaths = () => [ expect(result).not.toContain("a;b"); }); }); + +// Vite 8 multi-env tests +describe("multi-env client build config (Vite 8)", () => { + it("places output and treeshake under rolldownOptions, not rollupOptions", 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-vite8-")); + const rootNodeModules = path.resolve(import.meta.dirname, "../node_modules"); + await fsp.symlink(rootNodeModules, path.join(tmpDir, "node_modules"), "junction"); + + // Create an app/ directory to trigger App Router multi-env mode + 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" }); + + const viteVersion = getViteMajorVersion(); + if (viteVersion >= 8) { + // Vite 8 shape - config under rolldownOptions + const clientBuild = result.environments.client.build; + expect(clientBuild.rolldownOptions?.output?.manualChunks).toBeDefined(); + expect(clientBuild.rolldownOptions?.treeshake).toEqual({ + moduleSideEffects: "no-external", + }); + + // These must NOT be present on Vite 8 + expect(clientBuild.rollupOptions?.treeshake).toBeUndefined(); + expect(clientBuild.rolldownOptions?.output?.experimentalMinChunkSize).toBeUndefined(); + } else { + // Vite 7 shape - config under rollupOptions + const clientBuild = result.environments.client.build; + expect(clientBuild.rollupOptions?.output?.manualChunks).toBeDefined(); + expect(clientBuild.rollupOptions?.treeshake).toEqual({ + preset: "recommended", + moduleSideEffects: "no-external", + }); + } + } finally { + await fsp.rm(tmpDir, { recursive: true, force: true }).catch(() => {}); + } + }); +});