Skip to content

feat(napi/transform, oxc): add React Compiler behind a Cargo feature - #24542

Closed
Boshen wants to merge 3 commits into
mainfrom
react-compiler-napi
Closed

Boshen wants to merge 3 commits into
mainfrom
react-compiler-napi

Conversation

@Boshen

@Boshen Boshen commented Jul 15, 2026 •

Copy link
Copy Markdown
Member

Summary

Exposes the React Compiler as a Cargo feature on the oxc crate and the oxc-transform napi binding, surfaced to JS as plugins.reactCompiler.

oxc crate. The react_compiler feature already existed, but nothing re-exported oxc_react_compiler — so enabling it produced a TransformOptions::react_compiler: Option<PluginOptions> field whose type callers could not name, leaving them to add their own version-matched oxc_react_compiler dependency. This adds oxc::react_compiler, which is also what lets the binding build PluginOptions through oxc rather than depending on the crate directly, as it did before #23590.

napi binding. Restores the option removed in #23590, now grouped with the other third-party plugin configs as plugins.reactCompiler. That PR rejected feature-gating because it saw two options, neither workable:

  • keep the option types unconditional → a feature-off build silently ignores the option;
  • gate the type surface too → build-test regenerates a .d.ts that no longer matches the checked-in one, breaking git diff --exit-code.

There's a third: gate below the type surface. The JS-facing option types are plain strings and bools with no oxc types, so they stay unconditional and only the conversion into PluginOptions is gated. A --no-default-features build then regenerates a byte-identical index.d.ts, so git diff --exit-code holds in every feature config, and a lean build passing the option gets a hard error rather than a silent no-op.

The feature is on by default so the published binary supports what the types advertise. It's a default rather than --features react_compiler in the build scripts because build already passes --features allocator, and the napi CLI forwards repeated flags as --features react_compiler allocator, which cargo misreads.

Notes

  • The nesting under plugins is a JS-API grouping only. oxc::transformer::PluginsOptions has no such field — the transformer runs the compiler as its own pass before the main traversal, off TransformOptions::react_compiler — so TryFrom<TransformOptions> lifts it out of plugins before converting the rest. TransformOptions's evaluation-order doc still lists plugins.reactCompiler first for that reason.
  • The compiler's options API drifted since the binding last exposed it: default_plugin_options() is gone (now PluginOptions::default()), compilationMode / panicThreshold / outputMode are enums rather than pass-through strings, and filename / isDev / enableReanimated were dropped in refactor(react_compiler): remove unused js options #24065 as unused no-ops, so those three are no longer surfaced. Unknown enum values are now rejected; target previously fell back to the React 19 runtime silently on a typo and now errors.
  • This re-adds an option that shipped in 0.135.0/0.136.0 and was removed in 0.137.0, so it likely wants a changelog note.

🤖 Generated with Claude Code

@github-actions github-actions Bot added the A-transformer Area - Transformer / Transpiler label Jul 15, 2026
@codspeed

codspeed Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 67 untouched benchmarks
⏩ 9 skipped benchmarks1


Comparing react-compiler-napi (62d0731) with main (ba65790)

Open in CodSpeed

Footnotes

  1. 9 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@Boshen
Boshen force-pushed the react-compiler-napi branch from 253919d to 94c9b83 Compare July 15, 2026 14:04
Boshen added 3 commits July 16, 2026 19:46
Exposes the React Compiler as a Cargo feature on the `oxc` crate and the
`oxc-transform` napi binding.

`oxc` already had a `react_compiler` feature, but nothing re-exported
`oxc_react_compiler`, so enabling it produced a `TransformOptions::react_compiler`
field whose `PluginOptions` type callers could not name. Add `oxc::react_compiler`,
which also lets the binding build `PluginOptions` through `oxc` instead of depending
on the crate directly.

Restore the napi `reactCompiler` option, removed in #23590. That change rejected
feature-gating because both options it saw were unworkable: unconditional types mean
a feature-off build silently ignores the option, and gated types mean `build-test`
regenerates a `.d.ts` that no longer matches the checked-in one. Gating below the
type surface avoids both -- the JS-facing option types are plain strings and bools,
so they stay unconditional and only the conversion into `PluginOptions` is gated. A
`--no-default-features` build regenerates a byte-identical `index.d.ts`, and passing
`reactCompiler` to such a build is a hard error rather than a silent no-op.

The feature is on by default so the published binary supports what the types
advertise. It is a default rather than `--features react_compiler` in the build
scripts because `build` already passes `--features allocator`, and the napi CLI
forwards repeated flags as `--features react_compiler allocator`, which cargo
misreads.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Group the option with the other third-party plugin configs as
`plugins.reactCompiler`, rather than a top-level `reactCompiler`.

The nesting is a JS-API grouping only. `oxc::transformer::PluginsOptions` has no
such field -- the transformer runs the compiler as its own pass before the main
traversal, off `TransformOptions::react_compiler` -- so `TryFrom<TransformOptions>`
lifts the option out of `plugins` before converting the rest. The evaluation-order
doc keeps listing it first for that reason.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
`environment` is the option upstream's Babel plugin leads with
(`environment: Partial<EnvironmentConfig>`), and it was the one part of
`PluginOptions` this binding did not mirror. Without it a JS caller cannot reach
the passes that are off by default -- `validateNoSetStateInEffects`,
`validateNoDerivedComputationsInEffects`, `enableJsxOutlining` and others -- so
the compiler carried roughly 85 KiB of code no caller could execute.

Field names mirror `EnvironmentConfig` one-for-one, so an `environment` block that
works in a Babel config works here unchanged. Every field is optional; unset fields
keep the compiler's default.

Not surfaced: the five composite entries (`customHooks`, `moduleTypeProvider`,
`enableEmitHookGuards`, `enableEmitInstrumentForget`,
`validateExhaustiveEffectDependencies`), and `throwUnknownExceptionTestonly`, which
exists to make the compiler panic in upstream's own tests.

Upstream defaults were checked against
`babel-plugin-react-compiler/src/HIR/Environment.ts`: the flags this port defaults
to `false` default to `false` there too. Pragmas remain test-only, matching upstream,
where the parser is `parseConfigPragmaForTests` in `Utils/TestUtils.ts`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@Boshen
Boshen force-pushed the react-compiler-napi branch from 15c00ae to 62d0731 Compare July 16, 2026 11:48
@Boshen Boshen closed this Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-transformer Area - Transformer / Transpiler

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant