vitejs / vitejs/vite-plugin-react

react({ compiler: true }) ignores tsconfig jsxImportSource

Open
#1,448 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

plugin: react
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.importSource is supplied from reactOptions.jsxImportSource, and JSX is already lowered before Vite's regular transform can apply the file's tsconfig.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.