vitejs / vitejs/vite-plugin-react
react({ compiler: true }) ignores tsconfig jsxImportSource
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 1.2k
- Forks
- 269
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 19
Description
Describe the bug
Enabling the native React Compiler with react({ compiler: true }) changes the JSX runtime selected from each file's nearest tsconfig.json. With no explicit plugin-level jsxImportSource, a file configured to use @emotion/react instead imports react/jsx-runtime.
Expected: enabling the compiler should preserve the documented per-file tsconfig inference.
Two identical TSX files produce these imports:
| Plugin options | styled/component.tsx (jsxImportSource: "@emotion/react") |
plain/component.tsx (default React) |
|---|---|---|
{ compiler: false } |
@emotion/react/jsx-runtime |
react/jsx-runtime |
{ compiler: true } |
react/jsx-runtime — incorrect |
react/jsx-runtime |
{ compiler: true, jsxImportSource: "@emotion/react" } |
@emotion/react/jsx-runtime |
@emotion/react/jsx-runtime |
The last row is expected global-override behavior, not another bug: setting one global source cannot preserve both per-file configurations.
Reproduction
Self-contained files below. No pragmas, Babel, CSS plugins, or workspace tooling are involved. Runtime imports are externalized so the test can inspect the emitted specifiers without installing or executing React/Emotion.
Create the files in an empty directory, then run:
npm install --ignore-scripts
npm run repro
The script checks the compiler-disabled baseline, prints all three configurations, and exits with code 1 for the compiler-enabled regression. The final global-override demonstration does not affect the exit code.
Complete reproduction files
package.json
{
"name": "vite-jsx-import-source-repro",
"private": true,
"type": "module",
"scripts": {
"repro": "node repro.mjs"
},
"devDependencies": {
"@vitejs/plugin-react": "6.1.1",
"oxc-transform-react": "0.145.0",
"vite": "8.2.2"
}
}
styled/tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@emotion/react"
}
}
plain/tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx"
}
}
Both styled/component.tsx and plain/component.tsx:
export function Component() {
return <div>Hello</div>;
}
repro.mjs
import assert from "node:assert/strict";
import { fileURLToPath } from "node:url";
import react from "@vitejs/plugin-react";
import { build } from "vite";
const root = fileURLToPath(new URL(".", import.meta.url));
const expected = {
styled: "@emotion/react/jsx-runtime",
plain: "react/jsx-runtime",
};
let failures = 0;
for (const options of [
{ compiler: false },
{ compiler: true },
{ compiler: true, jsxImportSource: "@emotion/react" },
]) {
const buildOutput = await build({
root,
configFile: false,
logLevel: "silent",
plugins: [react(options)],
build: {
write: false,
minify: false,
lib: {
entry: {
styled: `${root}styled/component.tsx`,
plain: `${root}plain/component.tsx`,
},
formats: ["es"],
},
rolldownOptions: {
external: /^(react|@emotion\/react)(\/|$)/,
output: { preserveModules: true },
},
},
});
console.log(JSON.stringify(options));
const chunks = [buildOutput].flat().flatMap((output) => output.output);
for (const [entry, runtime] of Object.entries(expected)) {
const chunk = chunks.find(
(output) =>
output.type === "chunk" &&
output.facadeModuleId === `${root}${entry}/component.tsx`,
);
assert.ok(chunk, `Missing entry: ${entry}`);
const imports = chunk.imports.filter((specifier) =>
specifier.endsWith("/jsx-runtime"),
);
const matches = imports.length === 1 && imports[0] === runtime;
console.log(
` ${entry}: ${imports.join(", ")} (${matches ? "PASS" : "FAIL"})`,
);
if (!options.compiler) assert.deepEqual(imports, [runtime]);
if (!matches && !options.jsxImportSource) failures++;
}
}
process.exitCode = failures ? 1 : 0;
System Info
macOS 26.4.1 arm64
Node 22.14.0
npm 10.9.2
vite 8.2.2
@vitejs/plugin-react 6.1.1
oxc-transform-react 0.145.0
Also reproduced after a clean npm ci --ignore-scripts using the generated lockfile.
Related context
- #726 restored tsconfig inference in the regular JSX transform path. It predates the native compiler integration from #1419 and does not cover this compiler-enabled path.
- #1224 concerns file-level JSX pragma comments being moved below imports. This reproduction contains no pragmas, so moving comments cannot address this case.
- In the native compiler transform,
jsx.importSourceis supplied fromreactOptions.jsxImportSource, and JSX is already lowered before Vite's regular transform can apply the file's tsconfig.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the self-contained reproduction in repro.mjs and the styled/tsconfig.json and plain/tsconfig.json fixtures; run npm ci --ignore-scripts and npm run repro. Trace the native compiler transform where jsx.importSource is supplied from reactOptions.jsxImportSource. Done means compiler-enabled output preserves @emotion/react/jsx-runtime for styled/component.tsx and react/jsx-runtime for plain/component.tsx, while the explicit global override still behaves as shown.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript, vite
- Domain
- frontend, tooling
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100