mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-14 22:29:17 +00:00
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:
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
|
||||
|
||||
@@ -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`,
|
||||
|
||||
Vendored
+3
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in new issue
Block a user