feat(linter/react/only-export-components): support allowCompoundComponents (#27117)

Adds support for `allowCompoundComponents` in
`react/only-export-components`.

When enabled, the rule accepts a non-empty object export when every
property is a React component. It also checks default exports and
exported local bindings. Nested objects, spreads, accessors, mixed
constants, and anonymous functions without a static component name
remain invalid. The option defaults to `false`.

The behavior follows
[eslint-plugin-react-refresh#117](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/pull/117).

Closes #27058

## AI usage disclosure

agent summarized the upstream's actions and conducted a review after i
completed the feature

Co-authored-by: Cameron <cameron.clark@hey.com>
This commit is contained in:
Kuroda KaynandCameron authored and GitHub committed 2026-09-28 09:53:04 +01:00
1 parent 0b94d8eb52
commit 9d80eeddbd
4 files changed
+566 -13

No files matched your search

@@ -5435,6 +5435,20 @@ export interface NoUnstableNestedComponentsConfig {
propNamePattern?: string;
}
export interface OnlyExportComponentsConfig {
/**
* Allow an exported object when every property is a React component.
* This matches Vite's compound component support. The object must be non-empty,
* contain no nested objects, spreads, or accessors, and anonymous functions
* must use a component name as a static property key.
*
* ```jsx
* // Allowed when allowCompoundComponents: true
* const Root = () => <div />;
* const Label = () => <span />;
* export const Tag = { Root, Label };
* ```
*/
allowCompoundComponents?: boolean;
/**
* Allow exporting primitive constants (string/number/boolean/template literal)
* alongside component exports without triggering a violation. Recommended when your
@@ -74,6 +74,18 @@ pub struct OnlyExportComponentsConfig {
/// export const Foo = () => null;
/// ```
allow_constant_export: bool,
/// Allow an exported object when every property is a React component.
/// This matches Vite's compound component support. The object must be non-empty,
/// contain no nested objects, spreads, or accessors, and anonymous functions
/// must use a component name as a static property key.
///
/// ```jsx
/// // Allowed when allowCompoundComponents: true
/// const Root = () => <div />;
/// const Label = () => <span />;
/// export const Tag = { Root, Label };
/// ```
allow_compound_components: bool,
/// If you export components wrapped in custom higher-order components, list their
/// identifiers here to avoid false positives.
#[serde(rename = "customHOCs")]
@@ -218,6 +230,12 @@ impl OnlyExportComponents {
fn can_be_react_function_component(&self, init: Option<&Expression>) -> bool {
if let Some(raw_init) = init {
if self.allow_compound_components
&& let Expression::ObjectExpression(object) = raw_init.get_inner_expression()
{
return self.is_compound_component(object);
}
let js_init = Self::skip_ts_expression(raw_init);
match js_init {
@@ -236,6 +254,78 @@ impl OnlyExportComponents {
}
}
fn is_compound_component(&self, object: &ObjectExpression) -> bool {
!object.properties.is_empty()
&& object.properties.iter().all(|property| {
let ObjectPropertyKind::ObjectProperty(property) = property else {
return false;
};
if property.kind != PropertyKind::Init {
return false;
}
// An anonymous function gets its name from a static property key.
let has_component_name = !property.computed
&& match &property.key {
PropertyKey::StaticIdentifier(ident) => {
is_react_component_name(&ident.name)
}
PropertyKey::StringLiteral(literal) => {
is_react_component_name(&literal.value)
}
_ => false,
};
self.is_compound_component_value(&property.value, has_component_name)
})
}
fn is_compound_component_value(&self, expr: &Expression, has_component_name: bool) -> bool {
match expr.get_inner_expression() {
Expression::Identifier(ident) => is_react_component_name(&ident.name),
Expression::StaticMemberExpression(member) => {
is_react_component_name(&member.property.name)
}
Expression::ArrowFunctionExpression(func) => {
func.params.items.len() + usize::from(func.params.rest.is_some()) <= 2
&& has_component_name
}
Expression::FunctionExpression(func) => {
func.body.is_some()
&& func.params.items.len() + usize::from(func.params.rest.is_some()) <= 2
&& func
.id
.as_ref()
.map_or(has_component_name, |id| is_react_component_name(&id.name))
}
Expression::ConditionalExpression(cond) => {
self.is_compound_component_value(&cond.consequent, has_component_name)
&& self.is_compound_component_value(&cond.alternate, has_component_name)
}
Expression::CallExpression(call) => {
if !self.is_callee_hoc(&call.callee) {
return false;
}
let validate_argument = match &call.callee {
Expression::Identifier(ident) => {
matches!(ident.name.as_str(), "memo" | "forwardRef")
}
Expression::StaticMemberExpression(member) => {
matches!(member.property.name.as_str(), "memo" | "forwardRef")
}
_ => false,
};
!validate_argument
|| call.arguments.first().and_then(Argument::as_expression).is_some_and(|arg| {
self.is_compound_component_value(arg, has_component_name)
})
}
Expression::TaggedTemplateExpression(tagged) => {
has_component_name && self.is_callee_hoc(&tagged.tag)
}
_ => false,
}
}
fn skip_ts_expression<'a>(exp: &'a Expression<'a>) -> &'a Expression<'a> {
match exp {
Expression::TSAsExpression(ts_expr) => &ts_expr.expression,
@@ -282,7 +372,7 @@ impl OnlyExportComponents {
ctx.diagnostic(export_all_components_diagnostic(export_all.span));
}
AstKind::ExportDefaultDeclaration(export_default) => {
let result = self.analyze_export_default(export_default);
let result = self.analyze_export_default(ctx, export_default);
if let Some(span) = result.anonymous_span {
ctx.diagnostic(anonymous_components_diagnostic(span));
}
@@ -295,13 +385,14 @@ impl OnlyExportComponents {
AstKind::ExportNamedDeclaration(export_named)
if export_named.export_kind.is_value() =>
{
let result = self.analyze_export_specifiers(&export_named.specifiers);
let result =
self.analyze_export_specifiers(&export_named.specifiers, Some(ctx));
analysis.merge(result);
}
AstKind::ExportFromDeclaration(export_from)
if export_from.export_kind.is_value() =>
{
let result = self.analyze_export_specifiers(&export_from.specifiers);
let result = self.analyze_export_specifiers(&export_from.specifiers, None);
analysis.merge(result);
}
_ => {}
@@ -370,9 +461,25 @@ impl OnlyExportComponents {
}
}
fn analyze_export_default(&self, export_default: &ExportDefaultDeclaration) -> ExportAnalysis {
fn analyze_export_default(
&self,
ctx: &LintContext,
export_default: &ExportDefaultDeclaration,
) -> ExportAnalysis {
let mut analysis = ExportAnalysis::default();
if self.allow_compound_components
&& let Some(expr) = export_default.declaration.as_expression()
&& let Expression::ObjectExpression(object) = expr.get_inner_expression()
{
analysis.add_export(if self.is_compound_component(object) {
ExportType::ReactComponent
} else {
ExportType::NonComponent(object.span)
});
return analysis;
}
match &export_default.declaration {
ExportDefaultDeclarationKind::TSInterfaceDeclaration(_) => return analysis,
ExportDefaultDeclarationKind::FunctionDeclaration(func) => {
@@ -403,16 +510,24 @@ impl OnlyExportComponents {
analysis.has_react_export = true;
}
ExportDefaultDeclarationKind::Identifier(ident) => {
let export_type =
self.classify_export(ident.name.as_str(), ident.span, false, None);
let export_type = self.classify_export_reference(
Some(ctx),
ident.name.as_str(),
ident.name.as_str(),
ident.span,
);
analysis.add_export(export_type);
}
ExportDefaultDeclarationKind::TSAsExpression(ts_as_expr) => {
return self
.analyze_export_default_expression(&ts_as_expr.expression, export_default);
return self.analyze_export_default_expression(
ctx,
&ts_as_expr.expression,
export_default,
);
}
ExportDefaultDeclarationKind::TSSatisfiesExpression(ts_satisfies_expr) => {
return self.analyze_export_default_expression(
ctx,
&ts_satisfies_expr.expression,
export_default,
);
@@ -427,6 +542,7 @@ impl OnlyExportComponents {
fn analyze_export_default_expression(
&self,
ctx: &LintContext,
expr: &Expression,
export_default: &ExportDefaultDeclaration,
) -> ExportAnalysis {
@@ -437,8 +553,12 @@ impl OnlyExportComponents {
analysis.has_react_export = true;
}
Expression::Identifier(ident) => {
let export_type =
self.classify_export(ident.name.as_str(), ident.span, false, None);
let export_type = self.classify_export_reference(
Some(ctx),
ident.name.as_str(),
ident.name.as_str(),
ident.span,
);
analysis.add_export(export_type);
}
_ => {
@@ -500,7 +620,11 @@ impl OnlyExportComponents {
analysis
}
fn analyze_export_specifiers(&self, specifiers: &[ExportSpecifier]) -> ExportAnalysis {
fn analyze_export_specifiers(
&self,
specifiers: &[ExportSpecifier],
ctx: Option<&LintContext>,
) -> ExportAnalysis {
let mut analysis = ExportAnalysis::default();
for export_spec in specifiers {
@@ -514,9 +638,9 @@ impl OnlyExportComponents {
let span = export_spec.local.span();
let export = if exported_name == Some("default") {
self.classify_export(local_name.as_str(), span, false, None)
self.classify_export_reference(ctx, local_name.as_str(), local_name.as_str(), span)
} else if let Some(name) = exported_name {
self.classify_export(name, span, false, None)
self.classify_export_reference(ctx, name, local_name.as_str(), span)
} else {
ExportType::NonComponent(span)
};
@@ -526,6 +650,30 @@ impl OnlyExportComponents {
analysis
}
fn classify_export_reference(
&self,
ctx: Option<&LintContext>,
name: &str,
local_name: &str,
span: Span,
) -> ExportType {
let init = ctx.and_then(|ctx| {
let scoping = ctx.scoping();
let symbol_id = scoping.get_binding(scoping.root_scope_id(), local_name.into())?;
let AstKind::VariableDeclarator(declarator) =
ctx.semantic().symbol_declaration(symbol_id).kind()
else {
return None;
};
if !matches!(declarator.id, BindingPattern::BindingIdentifier(_)) {
return None;
}
let init = declarator.init.as_ref()?.get_inner_expression();
matches!(init, Expression::ObjectExpression(_)).then_some(init)
});
self.classify_export(name, span, self.can_be_react_function_component(init), init)
}
fn classify_export(
&self,
name: &str,
@@ -575,6 +723,10 @@ impl OnlyExportComponents {
}
if let Some(init_expr) = init {
if matches!(init_expr.get_inner_expression(), Expression::ObjectExpression(_)) {
return ExportType::NonComponent(span);
}
if let Expression::CallExpression(call_expr) = Self::skip_ts_expression(init_expr) {
let is_create_context = match &call_expr.callee {
Expression::Identifier(ident) => ident.name == "createContext",
@@ -899,6 +1051,104 @@ export function Button(props: PropsWithChildren): ReactNode {
),
// Named HOC export with anonymous arrow function argument
("export const Foo = React.memo(() => <div/>); export const Bar = () => null;", None),
(
"const Root = () => <div />; export const Tag = { Root };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"const Root = () => <div />; const Label = () => <span />; const Tag = { Root, Label } as const; export { Tag };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"const Tag = { Root: () => <div /> }; export { Tag as Components };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"const Tag = { Root: () => <div /> }; export { Tag as default };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"const Tag = { Root: () => <div /> }; export default Tag as Components;",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"const Tag = {}; export { Tag } from './components';",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"const { Root } = { Root: () => <div /> }; export { Root }; export const App = () => <div />;",
None,
),
(
"const Root = () => <div />; export const Tag = { root: Root };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"const Root = () => <div />; const Label = () => <span />; export default { Root, Label };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Tag = { Root: () => <div />, Label: function () { return <span />; } };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Tag = { 'Root': () => <div />, Label() { return <span />; } };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Tag = { root: function Root() { return <div />; } };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"const Root = () => <div />; export const Tag = { [key]: Root };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Tag = { Root: Namespace.Root };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Tag = { Root: (() => <div />) as React.FC } as const satisfies Components;",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export default ({ Root: () => <div /> } as const) satisfies Components;",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Tag = { Root: React.memo(forwardRef(() => <div />)) };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"const Root = () => <div />; export const Tag = { root: memo(Root) };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Tag = { root: lazy(() => import('./Root')) };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Tag = { Root: observer(() => <div />), Label: styled.span`` };",
Some(
serde_json::json!([{ "allowCompoundComponents": true, "customHOCs": ["observer", "styled"] }]),
),
),
(
"export const Tag = { Root: enabled ? () => <div /> : () => null };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Tag = { Root: () => <div /> }; export const VERSION = 1;",
Some(
serde_json::json!([{ "allowCompoundComponents": true, "allowConstantExport": true }]),
),
),
(
"export const Foo = () => {}; export const Config = {};",
Some(
serde_json::json!([{ "allowCompoundComponents": true, "allowExportNames": ["Config"] }]),
),
),
];
let fail = vec![
@@ -972,6 +1222,109 @@ export function Button(props: PropsWithChildren): ReactNode {
"export const foo = React.lazy(() => import('./Foo')); export const Bar = () => null;",
None,
),
("const Root = () => <div />; export const Tag = { Root };", None),
(
"const Root = () => <div />; export const Tag = { Root };",
Some(serde_json::json!([{ "allowCompoundComponents": false }])),
),
("const Root = () => <div />; export default { Root };", None),
(
"export const Foo = () => {}; export const Tag = { Root: () => <div /> } as const satisfies Components;",
None,
),
(
"const Root = () => <div />; const Label = () => <span />; const Tag = { Root, Label } as const; export { Tag };",
None,
),
("const Root = () => <div />; const Tag = { Root }; export default Tag;", None),
(
"export const Foo = () => {}; const Tag = { Root: () => <div />, size: 1 }; export { Tag };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Foo = () => {}; const Tag = {}; export default Tag;",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Foo = () => {}; export const Tag = { size: 1 } as const satisfies Config;",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"const Root = () => <div />; export default { Root, size: 1 };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Foo = () => {}; export const Config = { limit: 1 };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Foo = () => {}; export const Empty = {};",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Foo = () => <div />; export const Tag = { Nested: { Root } };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Foo = () => <div />; export const Tag = { Nested: ({ Root } as const) };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"const Tag = { Root: () => <div /> }; export const helper = 1;",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"const Tag = { Root: () => <div /> };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Foo = () => {}; export const Tag = { get Root() { return <div />; } };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Foo = () => {}; export const Tag = { set Root(value) {} };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Foo = () => {}; export const Tag = { root: () => <div /> };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Foo = () => {}; export const Tag = { ['Root']: () => <div /> };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Foo = () => {}; export const Tag = { 1: () => <div /> };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Foo = () => {}; export const Tag = { ...Components };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Foo = () => {}; export const Tag = { Root: function helper() {} };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Foo = () => {}; export const Tag = { Root: (a, b, c) => <div /> };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Foo = () => {}; export const Tag = { Root: Namespace.helper };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Foo = () => {}; export const Tag = { Root: unknownHoc(() => <div />) };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Foo = () => {}; export const Tag = { root: memo(() => <div />) };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
(
"export const Foo = () => {}; export const Tag = { Root: enabled ? () => <div /> : null };",
Some(serde_json::json!([{ "allowCompoundComponents": true }])),
),
];
Tester::new(OnlyExportComponents::NAME, OnlyExportComponents::PLUGIN, pass, fail)
@@ -195,3 +195,183 @@ source: crates/oxc_linter/src/tester.rs
1 │ export const foo = React.lazy(() => import('./Foo')); export const Bar = () => null;
· ───
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Move your component(s) to a separate file.
╭─[only_export_components.tsx:1:7]
1 │ const Root = () => <div />; export const Tag = { Root };
· ────
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Move your component(s) to a separate file.
╭─[only_export_components.tsx:1:7]
1 │ const Root = () => <div />; export const Tag = { Root };
· ────
╰────
⚠ react(only-export-components): Fast refresh can't handle anonymous components. Add a name to your export.
╭─[only_export_components.tsx:1:29]
1 │ const Root = () => <div />; export default { Root };
· ────────────────────────
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Move your component(s) to a separate file.
╭─[only_export_components.tsx:1:7]
1 │ const Root = () => <div />; export default { Root };
· ────
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.
╭─[only_export_components.tsx:1:43]
1 │ export const Foo = () => {}; export const Tag = { Root: () => <div /> } as const satisfies Components;
· ───
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Move your component(s) to a separate file.
╭─[only_export_components.tsx:1:7]
1 │ const Root = () => <div />; const Label = () => <span />; const Tag = { Root, Label } as const; export { Tag };
· ────
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Move your component(s) to a separate file.
╭─[only_export_components.tsx:1:35]
1 │ const Root = () => <div />; const Label = () => <span />; const Tag = { Root, Label } as const; export { Tag };
· ─────
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Move your component(s) to a separate file.
╭─[only_export_components.tsx:1:7]
1 │ const Root = () => <div />; const Tag = { Root }; export default Tag;
· ────
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.
╭─[only_export_components.tsx:1:85]
1 │ export const Foo = () => {}; const Tag = { Root: () => <div />, size: 1 }; export { Tag };
· ───
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.
╭─[only_export_components.tsx:1:61]
1 │ export const Foo = () => {}; const Tag = {}; export default Tag;
· ───
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.
╭─[only_export_components.tsx:1:43]
1 │ export const Foo = () => {}; export const Tag = { size: 1 } as const satisfies Config;
· ───
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Move your component(s) to a separate file.
╭─[only_export_components.tsx:1:7]
1 │ const Root = () => <div />; export default { Root, size: 1 };
· ────
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.
╭─[only_export_components.tsx:1:43]
1 │ export const Foo = () => {}; export const Config = { limit: 1 };
· ──────
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.
╭─[only_export_components.tsx:1:43]
1 │ export const Foo = () => {}; export const Empty = {};
· ─────
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.
╭─[only_export_components.tsx:1:48]
1 │ export const Foo = () => <div />; export const Tag = { Nested: { Root } };
· ───
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.
╭─[only_export_components.tsx:1:48]
1 │ export const Foo = () => <div />; export const Tag = { Nested: ({ Root } as const) };
· ───
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Move your component(s) to a separate file.
╭─[only_export_components.tsx:1:7]
1 │ const Tag = { Root: () => <div /> }; export const helper = 1;
· ───
╰────
⚠ react(only-export-components): Fast refresh only works when a file has exports. Move your component(s) to a separate file.
╭─[only_export_components.tsx:1:7]
1 │ const Tag = { Root: () => <div /> };
· ───
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.
╭─[only_export_components.tsx:1:43]
1 │ export const Foo = () => {}; export const Tag = { get Root() { return <div />; } };
· ───
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.
╭─[only_export_components.tsx:1:43]
1 │ export const Foo = () => {}; export const Tag = { set Root(value) {} };
· ───
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.
╭─[only_export_components.tsx:1:43]
1 │ export const Foo = () => {}; export const Tag = { root: () => <div /> };
· ───
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.
╭─[only_export_components.tsx:1:43]
1 │ export const Foo = () => {}; export const Tag = { ['Root']: () => <div /> };
· ───
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.
╭─[only_export_components.tsx:1:43]
1 │ export const Foo = () => {}; export const Tag = { 1: () => <div /> };
· ───
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.
╭─[only_export_components.tsx:1:43]
1 │ export const Foo = () => {}; export const Tag = { ...Components };
· ───
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.
╭─[only_export_components.tsx:1:43]
1 │ export const Foo = () => {}; export const Tag = { Root: function helper() {} };
· ───
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.
╭─[only_export_components.tsx:1:43]
1 │ export const Foo = () => {}; export const Tag = { Root: (a, b, c) => <div /> };
· ───
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.
╭─[only_export_components.tsx:1:43]
1 │ export const Foo = () => {}; export const Tag = { Root: Namespace.helper };
· ───
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.
╭─[only_export_components.tsx:1:43]
1 │ export const Foo = () => {}; export const Tag = { Root: unknownHoc(() => <div />) };
· ───
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.
╭─[only_export_components.tsx:1:43]
1 │ export const Foo = () => {}; export const Tag = { root: memo(() => <div />) };
· ───
╰────
⚠ react(only-export-components): Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.
╭─[only_export_components.tsx:1:43]
1 │ export const Foo = () => {}; export const Tag = { Root: enabled ? () => <div /> : null };
· ───
╰────
+6
View File
@@ -16920,6 +16920,12 @@
"OnlyExportComponentsConfig": {
"type": "object",
"properties": {
"allowCompoundComponents": {
"description": "Allow an exported object when every property is a React component.\nThis matches Vite's compound component support. The object must be non-empty,\ncontain no nested objects, spreads, or accessors, and anonymous functions\nmust use a component name as a static property key.\n\n```jsx\n// Allowed when allowCompoundComponents: true\nconst Root = () => <div />;\nconst Label = () => <span />;\nexport const Tag = { Root, Label };\n```",
"default": false,
"type": "boolean",
"markdownDescription": "Allow an exported object when every property is a React component.\nThis matches Vite's compound component support. The object must be non-empty,\ncontain no nested objects, spreads, or accessors, and anonymous functions\nmust use a component name as a static property key.\n\n```jsx\n// Allowed when allowCompoundComponents: true\nconst Root = () => <div />;\nconst Label = () => <span />;\nexport const Tag = { Root, Label };\n```"
},
"allowConstantExport": {
"description": "Allow exporting primitive constants (string/number/boolean/template literal)\nalongside component exports without triggering a violation. Recommended when your\nbundler’s Fast Refresh integration supports this (enabled by the plugin’s `vite`\npreset).\n\n```jsx\n// Allowed when allowConstantExport: true\nexport const VERSION = \"3\";\nexport const Foo = () => null;\n```",
"default": false,