Skip to content

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

Open

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

  1. doctor8296 commented on Sep 11, 2026

    @doctor8296

    I have exactly the same problem!

  2. ArnaudBarre commented on Oct 4, 2026

    @ArnaudBarre
    Member

    @undefined1e4 @doctor8296 @undefined1e4 Does using the jsxImportSource option have downside for you apart from duplicating the config value?

    The current suggestion to have the react plugin track the tsconfig seems like an heavy addition

  3. swandir commented on Oct 4, 2026

    @swandir

    @undefined1e4 @doctor8296 @undefined1e4 Does using the jsxImportSource option have downside for you apart from duplicating the config value?

    The current suggestion to have the react plugin track the tsconfig seems like an heavy addition

    Files in the same app can belong to different tsconfigs with different jsxImportSource connected via project references. For example, in a monorepo setup only certain packages may use a custom JSX factory.

  4. self-assigned this
    on Oct 5, 2026
  5. added a commit that references this issue on Oct 6, 2026
    2cb9f75
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions