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
Describe the bug
Enabling the native React Compiler with
react({ compiler: true })changes the JSX runtime selected from each file's nearesttsconfig.json. With no explicit plugin-leveljsxImportSource, a file configured to use@emotion/reactinstead importsreact/jsx-runtime.Expected: enabling the compiler should preserve the documented per-file tsconfig inference.
Two identical TSX files produce these imports:
styled/component.tsx(jsxImportSource: "@emotion/react")plain/component.tsx(default React){ compiler: false }@emotion/react/jsx-runtimereact/jsx-runtime{ compiler: true }react/jsx-runtime— incorrectreact/jsx-runtime{ compiler: true, jsxImportSource: "@emotion/react" }@emotion/react/jsx-runtime@emotion/react/jsx-runtimeThe 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:
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.tsxandplain/component.tsx:repro.mjsSystem Info
Also reproduced after a clean
npm ci --ignore-scriptsusing the generated lockfile.Related context
jsx.importSourceis supplied fromreactOptions.jsxImportSource, and JSX is already lowered before Vite's regular transform can apply the file's tsconfig.