fix(react_compiler): skip node_modules by default (#25859)

## Summary
- skip React Compiler transform and lint analysis for `node_modules` by default
- let explicit transform `sources` allowlists opt dependencies back in
- document the behavior and add regression coverage

## Validation
- focused Oxlint and transform-react checks
- Clippy with warnings denied
- `just ready` reached the all-features suite; three Oxlint ANSI-color snapshots differed in this environment

AI assistance: Codex was used to implement and verify this change.
This commit is contained in:
Boshen committed 2026-08-18 13:50:31 +00:00
1 parent 2f40afa45b
commit 345f9818c5
7 files changed
+80 -26

No files matched your search

@@ -87,3 +87,19 @@ function Component() {
Tester::new(Purity::NAME, Purity::PLUGIN, pass, fail).test_and_snapshot();
}
#[test]
fn skips_node_modules() {
use std::path::PathBuf;
use crate::tester::Tester;
let pass = vec![(
"function Component() { return <div>{Date.now()}</div>; }",
None,
None,
Some(PathBuf::from("node_modules/package/Component.tsx")),
)];
Tester::new(Purity::NAME, Purity::PLUGIN, pass, vec![]).test();
}
@@ -88,13 +88,16 @@ pub fn build_react_compiler_results(host: &ContextHost) -> ReactCompilerResults
ReactCompilerResults { diagnostics: result.diagnostics }
}
/// Shared `should_run` for the React Compiler family: skip files with
/// multiple `<script>` sections (vue/astro/svelte). Required for correctness,
/// not just speed — the cache on [`ContextHost`] is per file, so it would
/// replay the first script section's findings for every later section.
/// Shared `should_run` for the React Compiler family: match the upstream
/// `node_modules` source filter and skip files with multiple `<script>` sections
/// (vue/astro/svelte). The latter is required for correctness, not just speed —
/// the cache on [`ContextHost`] is per file, so it would replay the first script
/// section's findings for every later section.
pub fn should_run_react_compiler(ctx: &ContextHost) -> bool {
!ctx.file_extension()
.is_some_and(|ext| LINT_PARTIAL_LOADER_EXTENSIONS.iter().any(|e| e == &ext))
!ctx.file_path().to_string_lossy().contains("node_modules")
&& !ctx
.file_extension()
.is_some_and(|ext| LINT_PARTIAL_LOADER_EXTENSIONS.iter().any(|e| e == &ext))
}
/// Shared `run_once` body for the React Compiler family of rules: report the
+15 -15
View File
@@ -101,21 +101,21 @@ omitted.
### React Compiler options
| Option | Type | Default | Description |
| ------------------------ | ------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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"`. |
| `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`. |
| `noEmit` | `boolean` | `false` | Request lint mode without applying React Compiler output. Deprecated; use `outputMode: "lint"`. |
| `outputMode` | `string` | `"client"` | Select `"client"`, `"ssr"`, or `"lint"` React Compiler output. |
| `eslintSuppressionRules` | `string[]` | React Hooks rules | ESLint or Oxlint rule names whose matching suppression comments opt a function out. Defaults to `react-hooks/exhaustive-deps` and `react-hooks/rules-of-hooks`; `[]` disables this behavior. |
| `flowSuppressions` | `boolean` | `true` | Treat `$FlowFixMe...`, `$FlowExpectedError`, or `$FlowIssue` tags immediately followed by `[react-rule` as compiler opt-outs. |
| `ignoreUseNoForget` | `boolean` | `false` | Compile functions carrying `"use no memo"` or `"use no forget"`. |
| `customOptOutDirectives` | `string[]` | Unset | Add directive strings that opt a function or module out of compilation. |
| `sources` | `string[]` | All files | Only run React Compiler when the filename contains one of the provided strings. |
| `environment` | `ReactCompilerEnvironmentOptions` | Compiler defaults | Override compiler feature flags and validation settings. |
| Option | Type | Default | Description |
| ------------------------ | ------------------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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"`. |
| `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`. |
| `noEmit` | `boolean` | `false` | Request lint mode without applying React Compiler output. Deprecated; use `outputMode: "lint"`. |
| `outputMode` | `string` | `"client"` | Select `"client"`, `"ssr"`, or `"lint"` React Compiler output. |
| `eslintSuppressionRules` | `string[]` | React Hooks rules | ESLint or Oxlint rule names whose matching suppression comments opt a function out. Defaults to `react-hooks/exhaustive-deps` and `react-hooks/rules-of-hooks`; `[]` disables this behavior. |
| `flowSuppressions` | `boolean` | `true` | Treat `$FlowFixMe...`, `$FlowExpectedError`, or `$FlowIssue` tags immediately followed by `[react-rule` as compiler opt-outs. |
| `ignoreUseNoForget` | `boolean` | `false` | Compile functions carrying `"use no memo"` or `"use no forget"`. |
| `customOptOutDirectives` | `string[]` | Unset | Add directive strings that opt a function or module out of compilation. |
| `sources` | `string[]` | All except `node_modules` | Only run React Compiler when the filename contains one of the provided strings. Providing this option replaces the default filter. |
| `environment` | `ReactCompilerEnvironmentOptions` | Compiler defaults | Override compiler feature flags and validation settings. |
Despite its upstream-compatible name, `eslintSuppressionRules` recognizes both
ESLint and Oxlint comments: `eslint-disable`, `eslint-disable-next-line`,
+3
View File
@@ -237,6 +237,9 @@ export interface ReactCompilerOptions {
/**
* Only run the React Compiler when the filename contains one of these strings.
*
* By default, files whose filename contains `node_modules` are skipped.
* Providing this option replaces that default filter.
*
* Function-valued `sources` filters from the Babel plugin are intentionally
* unsupported across the native boundary.
*/
+14 -5
View File
@@ -104,6 +104,9 @@ pub struct ReactCompilerOptions {
/// Only run the React Compiler when the filename contains one of these strings.
///
/// By default, files whose filename contains `node_modules` are skipped.
/// Providing this option replaces that default filter.
///
/// Function-valued `sources` filters from the Babel plugin are intentionally
/// unsupported across the native boundary.
pub sources: Option<Vec<String>>,
@@ -274,7 +277,9 @@ impl TransformOptions {
filename: &str,
) -> Result<(Option<PluginOptions>, oxc::transformer::TransformOptions), OxcDiagnostic> {
let react_compiler = match self.react_compiler {
None | Some(Either::A(true)) => Some(PluginOptions::default()),
None | Some(Either::A(true)) => {
should_compile_react_source(filename, None).then(PluginOptions::default)
}
Some(Either::A(false)) => None,
Some(Either::B(options)) => options.resolve(filename)?,
};
@@ -300,10 +305,7 @@ impl TransformOptions {
impl ReactCompilerOptions {
fn resolve(self, filename: &str) -> Result<Option<PluginOptions>, OxcDiagnostic> {
let enabled = self
.sources
.as_ref()
.is_none_or(|sources| sources.iter().any(|source| filename.contains(source.as_str())));
let enabled = should_compile_react_source(filename, self.sources.as_deref());
enabled.then(|| self.into_plugin_options()).transpose()
}
@@ -372,6 +374,13 @@ impl ReactCompilerOptions {
}
}
fn should_compile_react_source(filename: &str, sources: Option<&[String]>) -> bool {
sources.map_or_else(
|| !filename.contains("node_modules"),
|sources| sources.iter().any(|source| filename.contains(source)),
)
}
impl From<JsxOptions> for oxc::transformer::JsxOptions {
fn from(options: JsxOptions) -> Self {
let defaults = Self::default();
@@ -168,6 +168,26 @@ export function Component({ value }: { value: string }) {
expect(result.code).not.toContain("<button");
});
it("skips node_modules by default", () => {
for (const options of [undefined, { reactCompiler: {} }]) {
const result = transformSync("node_modules/package/Component.tsx", fixture, options);
expect(result.errors).toEqual([]);
expect(result.code).not.toContain("react/compiler-runtime");
expect(result.code).not.toContain("_c(");
}
});
it("allows sources to include node_modules", () => {
const result = transformSync("node_modules/package/Component.tsx", fixture, {
reactCompiler: { sources: ["node_modules/package"] },
});
expect(result.errors).toEqual([]);
expect(result.code).toContain("react/compiler-runtime");
expect(result.code).toContain("_c(");
});
it("keeps imports used by compiled computed keys", () => {
const result = transformSync(
"Box.tsx",
@@ -237,6 +237,9 @@ export interface ReactCompilerOptions {
/**
* Only run the React Compiler when the filename contains one of these strings.
*
* By default, files whose filename contains `node_modules` are skipped.
* Providing this option replaces that default filter.
*
* Function-valued `sources` filters from the Babel plugin are intentionally
* unsupported across the native boundary.
*/