Skip to content

react({ compiler: true }) ignores tsconfig jsxImportSource #1448

Description

@undefined1e4

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions