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 });
+ });
});