mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
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 };
|
||||
· ───
|
||||
╰────
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user