mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-15 06:39:17 +00:00
feat(transform-react): add reportDiagnostics option to expose recoverable React Compiler diagnostics (#26624)
## What
Adds an opt-in `reactCompiler.reportDiagnostics` option to
`oxc-transform-react`. When `true`, the recoverable diagnostics the
React Compiler produced for a successful compile
(`CompileResult::Success { diagnostics }`) are appended to
`result.errors` alongside code, keeping the category severity the
compiler assigned (`Error` for `Suppression`, `Warning` for
`IncompatibleLibrary`, ...). `fatal` keeps its meaning and tells whether
`code` is usable.
The default is unchanged: recoverable diagnostics stay omitted, matching
Babel's default `logger: null` as decided in #26128.
## Why
Since #26128 (0.148.0) there is no way to learn *why* a function was
skipped: `errors` is fatal-only, and `panicThreshold` is not a
substitute because it aborts at the first diagnostic without producing
code. Consumers that need the reasons — a burndown inventory when
adopting the compiler, or checking which `eslint-disable` suppressions
actually took effect — lost their only channel. Details and a repro are
in #26318.
Two concrete downstream effects on 0.148+:
- `@vitejs/plugin-react` added `logDiagnostics`
(vitejs/vite-plugin-react#1431) one day after #26128, reading
recoverable diagnostics from `result.errors`; with the current binding
that option is a no-op. With this PR it only needs to forward
`reportDiagnostics: logDiagnostics`.
- `outputMode: 'lint'` / `noEmit` return `errors: []`, so lint mode has
no observable output at the Node boundary.
Babel's `logger` is the knob that turns these reports on; this option is
the non-callback equivalent. Upstream's own native bridge
(`babel-plugin-react-compiler-rust`) likewise returns every event on the
native result and lets the JS shim apply `logger: null`.
## Design notes
- **Opt-in rather than a new always-populated field**, so the default
result is byte-identical to today and nobody pays for rendering
codeframes they never read.
- **Reuses `errors`** instead of adding a second array: `errors` already
carries non-fatal parser and transformer warnings, and the 0.147 shape
(recoverable compiler diagnostics in `errors`, `fatal` to tell if output
is usable) is what existing consumers were written against.
- The severities are forwarded as the compiler assigned them (no `Error
→ Warning` rewrite as in 0.147); `fatal` is the fatality signal, as
documented on `oxc_react_compiler::compile`.
## Verification
- `napi/transform-react`: `pnpm run build-dev && pnpm test` — 33 tests
pass (3 new: opt-in vs default, multiple diagnostics with their own
severities, lint mode).
- `cargo fmt --check`, `cargo clippy -p oxc_transform_react_napi
--all-targets --all-features -- -D warnings`, `typos` on the touched
files.
- Repro from #26318 against this build (`memoized` = `_c(` count, shows
the suppression is still honored):
```
default reportDiagnostics: true
default errors: 0 errors: 1 (Suppression, label = the eslint comment)
no suppressions errors: 0 errors: 0, memoized: 2
lint errors: 0 errors: 1, memoized: 0
panic fatal, 1 unchanged
```
- Ran the real consumer from #26318 (a suppression gate that maps
`react-compiler(Suppression)` labels back to the component they cover)
against this build: with the option it recovers all 5 expected sites
across the codebase; against published 0.149.0 it finds none.
`index.d.ts` is regenerated by `napi build`;
`transform-react.wasi.d.cts` mirrors it by hand, as in #26128.
closes #26318
---
AI usage disclosure (per AGENTS.md): this PR was drafted with Claude
Fable 5.1. I reviewed the diff; the verification steps above were run on
my machine against this branch.
---------
Co-authored-by: JacktheRanger <219502738+JacktheRanger@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Cameron Clark <cameron.clark@hey.com>
This commit is contained in:
7 files changed
+194
-28
No files matched your search
@@ -59,9 +59,11 @@ The asynchronous `transform` runs on a worker-pool thread. It is useful when
|
||||
processing files concurrently, but can be slower for a single small file.
|
||||
|
||||
`TransformResult` contains `code`, an optional source `map`, `errors`, and a
|
||||
`fatal` flag. Recoverable React Compiler bail-outs are omitted in every output
|
||||
mode, matching Babel's default `logger: null`. Fatal diagnostics appear in
|
||||
`errors` without producing code.
|
||||
`fatal` flag. Fatal diagnostics appear in `errors` without producing code.
|
||||
Recoverable React Compiler diagnostics are omitted by default;
|
||||
set `reactCompiler.reportDiagnostics` to include them in
|
||||
`errors` while code is still produced, and use `fatal` to determine whether the
|
||||
output is usable.
|
||||
|
||||
## Options
|
||||
|
||||
@@ -105,6 +107,7 @@ omitted.
|
||||
| ------------------------ | ------------------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `compilationMode` | `string` | `"infer"` | Select which functions to compile: `"infer"`, `"syntax"`, `"annotation"`, or `"all"`. |
|
||||
| `panicThreshold` | `string` | `"none"` | Select which diagnostics abort the transform: `"none"`, `"critical_errors"`, or `"all_errors"`. |
|
||||
| `reportDiagnostics` | `boolean` | `false` | Include recoverable React Compiler diagnostics (skipped functions, rule suppressions, lint findings) in `result.errors` while still emitting code. Fatal diagnostics are always reported. |
|
||||
| `target` | `string` or `ReactCompilerMetaTarget` | `"19"` | Target React `"17"`, `"18"`, `"19"`, or a Meta-internal runtime. |
|
||||
| `gating` | `ReactCompilerGating` | Unset | Emit compiled and original functions behind an imported feature flag. |
|
||||
| `dynamicGating` | `ReactCompilerDynamicGating` | Unset | Resolve flags in `"use memo if(...)"` directives from an imported module. A directive takes precedence over `gating`. |
|
||||
@@ -125,7 +128,9 @@ ESLint and Oxlint comments: `eslint-disable`, `eslint-disable-next-line`,
|
||||
`outputMode: "ssr"` takes precedence over `noEmit`. Otherwise, `noEmit: true`
|
||||
forces lint output, including when `outputMode: "client"` is set. Lint output
|
||||
suppresses only React Compiler rewrites; the downstream Oxc transform still
|
||||
removes TypeScript syntax and applies the configured JSX transform.
|
||||
removes TypeScript syntax and applies the configured JSX transform. Lint
|
||||
findings are recoverable diagnostics, so pair lint output with
|
||||
`reportDiagnostics: true` to receive them in `errors`.
|
||||
|
||||
The object-valued target and gating options have these shapes:
|
||||
|
||||
|
||||
Vendored
+22
-3
@@ -205,6 +205,16 @@ export interface ReactCompilerOptions {
|
||||
* @default 'none'
|
||||
*/
|
||||
panicThreshold?: 'none' | 'critical_errors' | 'all_errors'
|
||||
/**
|
||||
* Include recoverable React Compiler diagnostics in `errors`.
|
||||
*
|
||||
* Enable this to receive recoverable diagnostics (bail-outs, rule suppressions,
|
||||
* lint findings) while code is still produced. Fatal diagnostics are reported
|
||||
* regardless of this option.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
reportDiagnostics?: boolean
|
||||
/**
|
||||
* React runtime target. React 17 and 18 use `react-compiler-runtime`;
|
||||
* React 19 uses `react/compiler-runtime`.
|
||||
@@ -217,13 +227,17 @@ export interface ReactCompilerOptions {
|
||||
/** Enable `"use memo if(...)"` directive-driven gating. */
|
||||
dynamicGating?: ReactCompilerDynamicGating
|
||||
/**
|
||||
* Analyze and report diagnostics without applying compiler output.
|
||||
* Analyze without applying compiler output. Pair with `reportDiagnostics`
|
||||
* to receive the findings.
|
||||
*
|
||||
* @deprecated Prefer `outputMode: "lint"`.
|
||||
* @default false
|
||||
*/
|
||||
noEmit?: boolean
|
||||
/** Select client, SSR, or lint output. */
|
||||
/**
|
||||
* Select client, SSR, or lint output. Lint findings are recoverable
|
||||
* diagnostics, so pair lint output with `reportDiagnostics`.
|
||||
*/
|
||||
outputMode?: 'client' | 'ssr' | 'lint'
|
||||
/**
|
||||
* ESLint rule names whose suppressions opt a function out of compilation.
|
||||
@@ -334,7 +348,12 @@ export interface TransformResult {
|
||||
code: string
|
||||
/** Source map, populated when `sourcemap` is `true`. */
|
||||
map?: SourceMap
|
||||
/** Parse, semantic, downstream transform, and fatal React Compiler diagnostics. */
|
||||
/**
|
||||
* Parse, semantic, downstream transform, and fatal React Compiler diagnostics.
|
||||
*
|
||||
* Recoverable React Compiler diagnostics are included when
|
||||
* `reactCompiler.reportDiagnostics` is `true`.
|
||||
*/
|
||||
errors: Array<OxcError>
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,9 @@ pub struct TransformResult {
|
||||
pub map: Option<SourceMap>,
|
||||
|
||||
/// Parse, semantic, downstream transform, and fatal React Compiler diagnostics.
|
||||
///
|
||||
/// Recoverable React Compiler diagnostics are included when
|
||||
/// `reactCompiler.reportDiagnostics` is `true`.
|
||||
pub errors: Vec<OxcError>,
|
||||
}
|
||||
|
||||
@@ -66,17 +69,18 @@ fn transform_impl(
|
||||
);
|
||||
let sourcemap = options.as_ref().and_then(|options| options.sourcemap).unwrap_or(false);
|
||||
|
||||
let (react_compiler_options, transform_options) =
|
||||
match options.unwrap_or_default().resolve(filename) {
|
||||
Ok(options) => options,
|
||||
Err(error) => {
|
||||
return TransformResult {
|
||||
fatal: true,
|
||||
errors: OxcError::from_diagnostics(filename, source_text, [error]),
|
||||
..TransformResult::default()
|
||||
};
|
||||
}
|
||||
};
|
||||
let options = options.unwrap_or_default();
|
||||
let report_diagnostics = options.report_react_compiler_diagnostics();
|
||||
let (react_compiler_options, transform_options) = match options.resolve(filename) {
|
||||
Ok(options) => options,
|
||||
Err(error) => {
|
||||
return TransformResult {
|
||||
fatal: true,
|
||||
errors: OxcError::from_diagnostics(filename, source_text, [error]),
|
||||
..TransformResult::default()
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let allocator = Allocator::default();
|
||||
let parser_return = Parser::new(&allocator, source_text, source_type).parse();
|
||||
@@ -101,8 +105,13 @@ fn transform_impl(
|
||||
let react_output = match react_compiler_options {
|
||||
None => None,
|
||||
Some(options) => match react_compiler_compile(&program, &semantic, &allocator, options) {
|
||||
// Match Babel's default `logger: null` by omitting recoverable diagnostics.
|
||||
CompileResult::Success { output, .. } => output,
|
||||
// Include recoverable diagnostics only when requested.
|
||||
CompileResult::Success { output, diagnostics: react_diagnostics } => {
|
||||
if report_diagnostics {
|
||||
diagnostics.extend(react_diagnostics);
|
||||
}
|
||||
output
|
||||
}
|
||||
CompileResult::Fatal { diagnostics: react_diagnostics } => {
|
||||
diagnostics.extend(react_diagnostics);
|
||||
return error_result(filename, source_text, diagnostics);
|
||||
|
||||
@@ -61,6 +61,15 @@ pub struct ReactCompilerOptions {
|
||||
#[napi(ts_type = "'none' | 'critical_errors' | 'all_errors'")]
|
||||
pub panic_threshold: Option<String>,
|
||||
|
||||
/// Include recoverable React Compiler diagnostics in `errors`.
|
||||
///
|
||||
/// Enable this to receive recoverable diagnostics (bail-outs, rule suppressions,
|
||||
/// lint findings) while code is still produced. Fatal diagnostics are reported
|
||||
/// regardless of this option.
|
||||
///
|
||||
/// @default false
|
||||
pub report_diagnostics: Option<bool>,
|
||||
|
||||
/// React runtime target. React 17 and 18 use `react-compiler-runtime`;
|
||||
/// React 19 uses `react/compiler-runtime`.
|
||||
///
|
||||
@@ -74,13 +83,15 @@ pub struct ReactCompilerOptions {
|
||||
/// Enable `"use memo if(...)"` directive-driven gating.
|
||||
pub dynamic_gating: Option<ReactCompilerDynamicGating>,
|
||||
|
||||
/// Analyze and report diagnostics without applying compiler output.
|
||||
/// Analyze without applying compiler output. Pair with `reportDiagnostics`
|
||||
/// to receive the findings.
|
||||
///
|
||||
/// @deprecated Prefer `outputMode: "lint"`.
|
||||
/// @default false
|
||||
pub no_emit: Option<bool>,
|
||||
|
||||
/// Select client, SSR, or lint output.
|
||||
/// Select client, SSR, or lint output. Lint findings are recoverable
|
||||
/// diagnostics, so pair lint output with `reportDiagnostics`.
|
||||
#[napi(ts_type = "'client' | 'ssr' | 'lint'")]
|
||||
pub output_mode: Option<String>,
|
||||
|
||||
@@ -272,6 +283,15 @@ pub struct ReactCompilerEnvironmentOptions {
|
||||
}
|
||||
|
||||
impl TransformOptions {
|
||||
/// Whether recoverable React Compiler diagnostics are forwarded to `errors`
|
||||
/// (`reactCompiler.reportDiagnostics`).
|
||||
pub(crate) fn report_react_compiler_diagnostics(&self) -> bool {
|
||||
matches!(
|
||||
&self.react_compiler,
|
||||
Some(Either::B(options)) if options.report_diagnostics == Some(true)
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn resolve(
|
||||
self,
|
||||
filename: &str,
|
||||
|
||||
@@ -328,6 +328,81 @@ export function Component({ value }: { value: string }) {
|
||||
expect(result.code).not.toContain("<span");
|
||||
});
|
||||
|
||||
it("reports recoverable diagnostics only when reportDiagnostics is enabled", () => {
|
||||
const source = `function Suppressed({ value }: { value: number }) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const doubled = value * 2;
|
||||
return <div>{doubled}</div>;
|
||||
}
|
||||
export function Component(props: { text: string }) {
|
||||
return <span>{props.text}</span>;
|
||||
}`;
|
||||
|
||||
const silent = transformSync("Components.tsx", source);
|
||||
expect(silent.fatal).toBe(false);
|
||||
expect(silent.errors).toEqual([]);
|
||||
|
||||
const reported = transformSync("Components.tsx", source, {
|
||||
reactCompiler: { reportDiagnostics: true },
|
||||
});
|
||||
expect(reported.fatal).toBe(false);
|
||||
expect(reported.code).toBe(silent.code);
|
||||
expect(reported.errors).toHaveLength(1);
|
||||
expect(reported.errors[0]).toMatchObject({
|
||||
severity: "Error",
|
||||
message: "React rule suppression prevents optimization",
|
||||
});
|
||||
expect(reported.errors[0].codeframe).toContain("react-compiler(Suppression)");
|
||||
const [label] = reported.errors[0].labels;
|
||||
expect(source.slice(label.start, label.end)).toBe(
|
||||
"// eslint-disable-next-line react-hooks/exhaustive-deps",
|
||||
);
|
||||
});
|
||||
|
||||
it("reports every recoverable diagnostic with its own severity", () => {
|
||||
const result = transformSync(
|
||||
"Components.tsx",
|
||||
`import { useReactTable } from "@tanstack/react-table";
|
||||
function Table() {
|
||||
const table = useReactTable({});
|
||||
return <div>{table}</div>;
|
||||
}
|
||||
function Suppressed({ value }: { value: number }) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
export function Component(props: { text: string }) {
|
||||
return <span>{props.text}</span>;
|
||||
}`,
|
||||
{ reactCompiler: { reportDiagnostics: true } },
|
||||
);
|
||||
|
||||
expect(result.fatal).toBe(false);
|
||||
expect(result.code).toContain("_c(");
|
||||
expect(result.errors.map((error) => [error.severity, error.message])).toEqual([
|
||||
["Warning", "Use of incompatible library"],
|
||||
["Error", "React rule suppression prevents optimization"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("reports lint findings with reportDiagnostics", () => {
|
||||
const result = transformSync(
|
||||
"Component.tsx",
|
||||
`function Component({ value }: { value: number }) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const doubled = value * 2;
|
||||
return <div>{doubled}</div>;
|
||||
}`,
|
||||
{ reactCompiler: { outputMode: "lint", reportDiagnostics: true } },
|
||||
);
|
||||
|
||||
expect(result.fatal).toBe(false);
|
||||
expect(result.code).not.toContain("_c(");
|
||||
expect(result.code).not.toContain(": number");
|
||||
expect(result.errors).toHaveLength(1);
|
||||
expect(result.errors[0].message).toBe("React rule suppression prevents optimization");
|
||||
});
|
||||
|
||||
it.each(["critical_errors", "all_errors"] as const)(
|
||||
"makes suppression bailouts fatal at panicThreshold %s",
|
||||
(panicThreshold) => {
|
||||
|
||||
@@ -200,6 +200,16 @@ export interface ReactCompilerOptions {
|
||||
* @default 'none'
|
||||
*/
|
||||
panicThreshold?: 'none' | 'critical_errors' | 'all_errors'
|
||||
/**
|
||||
* Include recoverable React Compiler diagnostics in `errors`.
|
||||
*
|
||||
* Enable this to receive recoverable diagnostics (bail-outs, rule suppressions,
|
||||
* lint findings) while code is still produced. Fatal diagnostics are reported
|
||||
* regardless of this option.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
reportDiagnostics?: boolean
|
||||
/**
|
||||
* React runtime target. React 17 and 18 use `react-compiler-runtime`;
|
||||
* React 19 uses `react/compiler-runtime`.
|
||||
@@ -212,13 +222,17 @@ export interface ReactCompilerOptions {
|
||||
/** Enable `"use memo if(...)"` directive-driven gating. */
|
||||
dynamicGating?: ReactCompilerDynamicGating
|
||||
/**
|
||||
* Analyze and report diagnostics without applying compiler output.
|
||||
* Analyze without applying compiler output. Pair with `reportDiagnostics`
|
||||
* to receive the findings.
|
||||
*
|
||||
* @deprecated Prefer `outputMode: "lint"`.
|
||||
* @default false
|
||||
*/
|
||||
noEmit?: boolean
|
||||
/** Select client, SSR, or lint output. */
|
||||
/**
|
||||
* Select client, SSR, or lint output. Lint findings are recoverable
|
||||
* diagnostics, so pair lint output with `reportDiagnostics`.
|
||||
*/
|
||||
outputMode?: 'client' | 'ssr' | 'lint'
|
||||
/**
|
||||
* ESLint rule names whose suppressions opt a function out of compilation.
|
||||
@@ -329,7 +343,12 @@ export interface TransformResult {
|
||||
code: string
|
||||
/** Source map, populated when `sourcemap` is `true`. */
|
||||
map?: SourceMap
|
||||
/** Parse, semantic, downstream transform, and fatal React Compiler diagnostics. */
|
||||
/**
|
||||
* Parse, semantic, downstream transform, and fatal React Compiler diagnostics.
|
||||
*
|
||||
* Recoverable React Compiler diagnostics are included when
|
||||
* `reactCompiler.reportDiagnostics` is `true`.
|
||||
*/
|
||||
errors: Array<OxcError>
|
||||
}
|
||||
|
||||
|
||||
@@ -200,6 +200,16 @@ export interface ReactCompilerOptions {
|
||||
* @default 'none'
|
||||
*/
|
||||
panicThreshold?: 'none' | 'critical_errors' | 'all_errors'
|
||||
/**
|
||||
* Include recoverable React Compiler diagnostics in `errors`.
|
||||
*
|
||||
* Enable this to receive recoverable diagnostics (bail-outs, rule suppressions,
|
||||
* lint findings) while code is still produced. Fatal diagnostics are reported
|
||||
* regardless of this option.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
reportDiagnostics?: boolean
|
||||
/**
|
||||
* React runtime target. React 17 and 18 use `react-compiler-runtime`;
|
||||
* React 19 uses `react/compiler-runtime`.
|
||||
@@ -212,13 +222,17 @@ export interface ReactCompilerOptions {
|
||||
/** Enable `"use memo if(...)"` directive-driven gating. */
|
||||
dynamicGating?: ReactCompilerDynamicGating
|
||||
/**
|
||||
* Analyze and report diagnostics without applying compiler output.
|
||||
* Analyze without applying compiler output. Pair with `reportDiagnostics`
|
||||
* to receive the findings.
|
||||
*
|
||||
* @deprecated Prefer `outputMode: "lint"`.
|
||||
* @default false
|
||||
*/
|
||||
noEmit?: boolean
|
||||
/** Select client, SSR, or lint output. */
|
||||
/**
|
||||
* Select client, SSR, or lint output. Lint findings are recoverable
|
||||
* diagnostics, so pair lint output with `reportDiagnostics`.
|
||||
*/
|
||||
outputMode?: 'client' | 'ssr' | 'lint'
|
||||
/**
|
||||
* ESLint rule names whose suppressions opt a function out of compilation.
|
||||
@@ -329,7 +343,12 @@ export interface TransformResult {
|
||||
code: string
|
||||
/** Source map, populated when `sourcemap` is `true`. */
|
||||
map?: SourceMap
|
||||
/** Parse, semantic, downstream transform, and fatal React Compiler diagnostics. */
|
||||
/**
|
||||
* Parse, semantic, downstream transform, and fatal React Compiler diagnostics.
|
||||
*
|
||||
* Recoverable React Compiler diagnostics are included when
|
||||
* `reactCompiler.reportDiagnostics` is `true`.
|
||||
*/
|
||||
errors: Array<OxcError>
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user