diff --git a/.changeset/fix-component-export-tanstack-router.md b/.changeset/fix-component-export-tanstack-router.md new file mode 100644 index 000000000000..e9ae0c58074e --- /dev/null +++ b/.changeset/fix-component-export-tanstack-router.md @@ -0,0 +1,13 @@ +--- +"@biomejs/biome": patch +--- + +Fixed [#8628](https://github.com/biomejs/biome/issues/8628): [`useComponentExportOnlyModules`](https://biomejs.dev/linter/rules/use-component-export-only-modules/) now allows components referenced as object property values in exported expressions. This fixes false positives for TanStack Router patterns. + +```jsx +export const Route = createFileRoute('/')({ + component: HomeComponent, +}) + +function HomeComponent() { ... } // no longer reported as "should be exported" +``` diff --git a/crates/biome_js_analyze/src/lint/style/use_component_export_only_modules.rs b/crates/biome_js_analyze/src/lint/style/use_component_export_only_modules.rs index 0093ce1dec4f..cff55e5a8ac1 100644 --- a/crates/biome_js_analyze/src/lint/style/use_component_export_only_modules.rs +++ b/crates/biome_js_analyze/src/lint/style/use_component_export_only_modules.rs @@ -4,10 +4,13 @@ use biome_analyze::{ }; use biome_console::markup; use biome_diagnostics::Severity; -use biome_js_syntax::{AnyJsModuleItem, AnyJsStatement, JsModule, export_ext::AnyJsExported}; -use biome_rowan::{AstNode, TextRange}; +use biome_js_syntax::{ + AnyJsModuleItem, AnyJsStatement, JsIdentifierExpression, JsModule, JsPropertyObjectMember, + JsShorthandPropertyObjectMember, export_ext::AnyJsExported, +}; +use biome_rowan::{AstNode, SyntaxNodeCast, TextRange}; use biome_rule_options::use_component_export_only_modules::UseComponentExportOnlyModulesOptions; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; declare_lint_rule! { /// Enforce declaring components only within modules that export React Components exclusively. @@ -215,6 +218,55 @@ impl Rule for UseComponentExportOnlyModules { } } + // Collect identifiers referenced as object property values in exported expressions. + // If a local component is referenced as an object property value, + // it should not be reported as unexported. + // This handles patterns like TanStack Router: + // export const Route = createFileRoute('/')({ component: HomeComponent }) + // function HomeComponent() { ... } + // + // We only exempt components referenced in object literals (like { component: X }) + // and not direct function call arguments (like hoge(X)), because the latter + // might be non-standard HOCs that could break Fast Refresh. + let referenced_ids: FxHashSet> = exported_non_component_ids + .iter() + .filter_map(|item| item.exported.as_ref()) + .flat_map(|exported| { + exported + .syntax() + .descendants() + .filter_map(JsIdentifierExpression::cast) + .filter(|id| { + // Only include identifiers that are object property values + id.syntax() + .parent() + .is_some_and(|parent| parent.cast::().is_some()) + }) + .filter_map(|id| id.name().ok()) + .filter_map(|name| name.value_token().ok()) + .map(|token| token.text_trimmed().into()) + }) + .collect(); + + // Also collect shorthand property references like { HomeComponent } + let shorthand_ids: FxHashSet> = exported_non_component_ids + .iter() + .filter_map(|item| item.exported.as_ref()) + .flat_map(|exported| { + exported + .syntax() + .descendants() + .filter_map(JsShorthandPropertyObjectMember::cast) + .filter_map(|prop| prop.name().ok()) + .filter_map(|name| name.value_token().ok()) + .map(|token| token.text_trimmed().into()) + }) + .collect(); + + // Remove components that are referenced as object property values + local_components + .retain(|name, _| !referenced_ids.contains(name) && !shorthand_ids.contains(name)); + if !exported_component_ids.is_empty() { return exported_non_component_ids .iter() diff --git a/crates/biome_js_analyze/tests/specs/style/useComponentExportOnlyModules/valid_component_referenced_in_export.jsx b/crates/biome_js_analyze/tests/specs/style/useComponentExportOnlyModules/valid_component_referenced_in_export.jsx new file mode 100644 index 000000000000..50547176866f --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/style/useComponentExportOnlyModules/valid_component_referenced_in_export.jsx @@ -0,0 +1,12 @@ +/* should not generate diagnostics */ + +// TanStack Router pattern - component referenced in exported object +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomeComponent, +}) + +function HomeComponent() { + return
Home
+} diff --git a/crates/biome_js_analyze/tests/specs/style/useComponentExportOnlyModules/valid_component_referenced_in_export.jsx.snap b/crates/biome_js_analyze/tests/specs/style/useComponentExportOnlyModules/valid_component_referenced_in_export.jsx.snap new file mode 100644 index 000000000000..384cf6d49ec5 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/style/useComponentExportOnlyModules/valid_component_referenced_in_export.jsx.snap @@ -0,0 +1,20 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid_component_referenced_in_export.jsx +--- +# Input +```jsx +/* should not generate diagnostics */ + +// TanStack Router pattern - component referenced in exported object +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: HomeComponent, +}) + +function HomeComponent() { + return
Home
+} + +``` diff --git a/crates/biome_js_analyze/tests/specs/style/useComponentExportOnlyModules/valid_component_shorthand_in_export.jsx b/crates/biome_js_analyze/tests/specs/style/useComponentExportOnlyModules/valid_component_shorthand_in_export.jsx new file mode 100644 index 000000000000..e9ee6824eb82 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/style/useComponentExportOnlyModules/valid_component_shorthand_in_export.jsx @@ -0,0 +1,10 @@ +/* should not generate diagnostics */ + +// Shorthand property pattern - component referenced via shorthand syntax +export const config = { + HomeComponent, +} + +function HomeComponent() { + return
Home
+} diff --git a/crates/biome_js_analyze/tests/specs/style/useComponentExportOnlyModules/valid_component_shorthand_in_export.jsx.snap b/crates/biome_js_analyze/tests/specs/style/useComponentExportOnlyModules/valid_component_shorthand_in_export.jsx.snap new file mode 100644 index 000000000000..31951b6ec3a1 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/style/useComponentExportOnlyModules/valid_component_shorthand_in_export.jsx.snap @@ -0,0 +1,18 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid_component_shorthand_in_export.jsx +--- +# Input +```jsx +/* should not generate diagnostics */ + +// Shorthand property pattern - component referenced via shorthand syntax +export const config = { + HomeComponent, +} + +function HomeComponent() { + return
Home
+} + +```