Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/vinext/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
14 changes: 12 additions & 2 deletions packages/vinext/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ function getViteMajorVersion(): number {
}
}

type UserResolveConfigWithTsconfigPaths = NonNullable<UserConfig["resolve"]> & {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Once Vite 8 ships a stable release with tsconfigPaths in its public UserConfig["resolve"] type, this custom type can be removed and you can use Vite's type directly. Worth a TODO comment so it doesn't get forgotten.

Non-blocking.

tsconfigPaths?: boolean;
};

/**
* PostCSS config file names to search for, in priority order.
* Matches the same search order as postcss-load-config / lilconfig.
Expand Down Expand Up @@ -704,6 +708,7 @@ export interface VinextOptions {
}

export default function vinext(options: VinextOptions = {}): PluginOption[] {
const viteMajorVersion = getViteMajorVersion();
let root: string;
let pagesDir: string;
let appDir: string;
Expand Down Expand Up @@ -824,7 +829,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
Expand All @@ -836,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
Expand Down Expand Up @@ -1238,6 +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"],
...(shouldEnableNativeTsconfigPaths ? { tsconfigPaths: true } : {}),
},
// Exclude vinext from dependency optimization so esbuild doesn't
// scan dist files containing virtual module imports (virtual:vinext-*)
Expand All @@ -1250,7 +1260,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
Expand Down
101 changes: 101 additions & 0 deletions tests/tsconfig-paths-vite8.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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 <div>hello</div>; }\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<typeof vinext>, 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 });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The temp directory cleanup (fs.rmSync) is at the end of each test body, but if an assertion fails mid-test, it won't run. Since afterEach already restores process.cwd(), consider also moving rmSync into afterEach (or using try/finally within each test) so temp dirs don't leak on failure.

Non-blocking — the leaked dirs are in os.tmpdir() so they'll be cleaned up by the OS eventually.

});

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<string, unknown>;
}>;
};
const resolvedConfig = await configPlugin.config?.(
{ root },
{ command: "serve", mode: "development" },
);

expect(resolvedConfig?.resolve?.tsconfigPaths).toBe(true);

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<string, unknown> },
env: { command: "serve"; mode: string },
) => Promise<{
resolve?: Record<string, unknown>;
}>;
};
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 });
});
});
Loading