From e81b9a6707ad45fc366b767b463290007298e019 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:07:07 +0800 Subject: [PATCH 1/2] feat: support vite8 resolve.tsconfigPaths --- packages/vinext/package.json | 2 +- packages/vinext/src/index.ts | 7 ++- tests/tsconfig-paths-vite8.test.ts | 78 ++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 tests/tsconfig-paths-vite8.test.ts diff --git a/packages/vinext/package.json b/packages/vinext/package.json index 7137fde87..562e036ba 100644 --- a/packages/vinext/package.json +++ b/packages/vinext/package.json @@ -78,7 +78,7 @@ "react": ">=19.2.0", "react-dom": ">=19.2.0", "react-server-dom-webpack": "^19.2.4", - "vite": "^7.0.0" + "vite": "^7.0.0 || ^8.0.0-beta.0" }, "peerDependenciesMeta": { "@mdx-js/rollup": { diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 35aee41ff..3c9b2f1e1 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -704,6 +704,7 @@ export interface VinextOptions { } export default function vinext(options: VinextOptions = {}): PluginOption[] { + const viteMajorVersion = getViteMajorVersion(); let root: string; let pagesDir: string; let appDir: string; @@ -824,7 +825,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { const plugins: PluginOption[] = [ // Resolve tsconfig paths/baseUrl aliases so real-world Next.js repos // that use @/*, #/*, or baseUrl imports work out of the box. - tsconfigPaths(), + // Vite 8+ supports this natively via resolve.tsconfigPaths. + ...(viteMajorVersion >= 8 ? [] : [tsconfigPaths()]), // React Fast Refresh + JSX transform for client components. reactPlugin, // Transform CJS require()/module.exports to ESM before other plugins @@ -1238,6 +1240,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // 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"], + ...(viteMajorVersion >= 8 ? { tsconfigPaths: true } : {}), }, // Exclude vinext from dependency optimization so esbuild doesn't // scan dist files containing virtual module imports (virtual:vinext-*) @@ -1250,7 +1253,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { }, // Enable JSX in .tsx/.jsx files // Vite 7 uses `esbuild` for transforms, Vite 8+ uses `oxc` - ...(getViteMajorVersion() >= 8 + ...(viteMajorVersion >= 8 ? { oxc: { jsx: { runtime: "automatic" } } } : { esbuild: { jsx: "automatic" } }), // Define env vars for client bundle diff --git a/tests/tsconfig-paths-vite8.test.ts b/tests/tsconfig-paths-vite8.test.ts new file mode 100644 index 000000000..1932ffec1 --- /dev/null +++ b/tests/tsconfig-paths-vite8.test.ts @@ -0,0 +1,78 @@ +import { afterEach, describe, expect, it } from "vitest"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import type { Plugin, PluginOption } from "vite"; +import vinext from "../packages/vinext/src/index.js"; + +const originalCwd = process.cwd(); + +function setupProject(viteVersion: string): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-vite-major-")); + fs.mkdirSync(path.join(root, "pages"), { recursive: true }); + fs.mkdirSync(path.join(root, "node_modules", "vite"), { recursive: true }); + fs.writeFileSync( + path.join(root, "package.json"), + JSON.stringify({ name: "test-project", version: "1.0.0" }, null, 2), + ); + fs.writeFileSync( + path.join(root, "node_modules", "vite", "package.json"), + JSON.stringify({ name: "vite", version: viteVersion }, null, 2), + ); + fs.writeFileSync( + path.join(root, "pages", "index.tsx"), + "export default function Page() { return
hello
; }\n", + ); + return root; +} + +function isPlugin(plugin: PluginOption): plugin is Plugin { + return !!plugin && !Array.isArray(plugin) && typeof plugin === "object" && "name" in plugin; +} + +function findNamedPlugin(plugins: ReturnType, name: string) { + return plugins.find((plugin): plugin is Plugin => isPlugin(plugin) && plugin.name === name); +} + +afterEach(() => { + process.chdir(originalCwd); +}); + +describe("Vite tsconfig paths support", () => { + it("keeps vite-tsconfig-paths on Vite 7", async () => { + const root = setupProject("7.3.1"); + process.chdir(root); + + const plugins = vinext({ appDir: root }); + + expect(findNamedPlugin(plugins, "vite-tsconfig-paths")).toBeDefined(); + + fs.rmSync(root, { recursive: true, force: true }); + }); + + it("uses resolve.tsconfigPaths on Vite 8 instead of vite-tsconfig-paths", async () => { + const root = setupProject("8.0.0-beta.18"); + process.chdir(root); + + const plugins = vinext({ appDir: root }); + + expect(findNamedPlugin(plugins, "vite-tsconfig-paths")).toBeUndefined(); + + const configPlugin = findNamedPlugin(plugins, "vinext:config") as { + config?: ( + config: { root: string }, + env: { command: "serve"; mode: string }, + ) => Promise<{ + resolve?: Record; + }>; + }; + const resolvedConfig = await configPlugin.config?.( + { root }, + { command: "serve", mode: "development" }, + ); + + expect(resolvedConfig?.resolve?.tsconfigPaths).toBe(true); + + fs.rmSync(root, { recursive: true, force: true }); + }); +}); From c4a8d96dab7a2f6e1c271a3cf5056b7c3857218e Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:23:08 +0800 Subject: [PATCH 2/2] Update --- packages/vinext/src/index.ts | 9 ++++++++- tests/tsconfig-paths-vite8.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 3c9b2f1e1..0e4a05aca 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -250,6 +250,10 @@ function getViteMajorVersion(): number { } } +type UserResolveConfigWithTsconfigPaths = NonNullable & { + tsconfigPaths?: boolean; +}; + /** * PostCSS config file names to search for, in priority order. * Matches the same search order as postcss-load-config / lilconfig. @@ -838,6 +842,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 shouldEnableNativeTsconfigPaths = + viteMajorVersion >= 8 && userResolve?.tsconfigPaths === undefined; // Load .env files into process.env before anything else. // Next.js loads .env files before evaluating next.config.js, so @@ -1240,7 +1247,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // 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"], - ...(viteMajorVersion >= 8 ? { tsconfigPaths: true } : {}), + ...(shouldEnableNativeTsconfigPaths ? { tsconfigPaths: true } : {}), }, // Exclude vinext from dependency optimization so esbuild doesn't // scan dist files containing virtual module imports (virtual:vinext-*) diff --git a/tests/tsconfig-paths-vite8.test.ts b/tests/tsconfig-paths-vite8.test.ts index 1932ffec1..473d5d61e 100644 --- a/tests/tsconfig-paths-vite8.test.ts +++ b/tests/tsconfig-paths-vite8.test.ts @@ -75,4 +75,27 @@ describe("Vite tsconfig paths support", () => { fs.rmSync(root, { recursive: true, force: true }); }); + + it("does not override user-defined resolve.tsconfigPaths on Vite 8", async () => { + const root = setupProject("8.0.0-beta.18"); + process.chdir(root); + + const plugins = vinext({ appDir: root }); + const configPlugin = findNamedPlugin(plugins, "vinext:config") as { + config?: ( + config: { root: string; resolve?: Record }, + env: { command: "serve"; mode: string }, + ) => Promise<{ + resolve?: Record; + }>; + }; + const resolvedConfig = await configPlugin.config?.( + { root, resolve: { tsconfigPaths: false } }, + { command: "serve", mode: "development" }, + ); + + expect(resolvedConfig?.resolve?.tsconfigPaths).toBeUndefined(); + + fs.rmSync(root, { recursive: true, force: true }); + }); });