eslint-plugin-nextfriday 4.1.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +13 -5
- package/docs/rules/ENFORCE_RENDER_NAMING.md +96 -0
- package/docs/rules/JSX_NO_DATA_ARRAY.md +63 -0
- package/docs/rules/JSX_NO_DATA_OBJECT.md +71 -0
- package/docs/rules/JSX_NO_SUB_INTERFACE.md +86 -0
- package/lib/index.cjs +923 -454
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +168 -0
- package/lib/index.d.ts +168 -0
- package/lib/index.js +923 -454
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# eslint-plugin-nextfriday
|
|
2
2
|
|
|
3
|
+
## 4.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#126](https://github.com/next-friday/eslint-plugin-nextfriday/pull/126) [`ec5eec8`](https://github.com/next-friday/eslint-plugin-nextfriday/commit/ec5eec80689a5ec6470807077e94b7065d1cb13c) Thanks [@joetakara](https://github.com/joetakara)! - add four JSX rules that target a single anti-pattern: component files acting as content / type / naming dumping grounds. The `.tsx` and `.jsx` file is meant to describe one component's render — data, sub-types, and helper render-fragments belong elsewhere or under a clearer name.
|
|
8
|
+
- `jsx-no-data-array` flags top-level `const` declarations whose initializer is an array literal containing object literals — including `as const` and `satisfies` variants and exported declarations. Arrays of primitives are unaffected. The shape is a strong signal of fixture / seed / content data, which belongs in a sibling `*.data.ts` module.
|
|
9
|
+
- `jsx-no-data-object` flags top-level `const` declarations whose initializer is an object literal that contains a nested object or a nested array of objects — including `as const` and `satisfies` variants and exported declarations. Flat maps of primitives (`{ home: "/", about: "/about" }`) are not flagged. Nested configuration / content belongs in a data module.
|
|
10
|
+
- `jsx-no-sub-interface` flags any top-level `interface` or `type` declaration in a `.tsx` / `.jsx` file that is not the main props type for a component declared in the same file. The "main" type is determined by the parameter type of a top-level PascalCase function or arrow component, with common wrappers (`Readonly<T>`, `Required<T>`, `Partial<T>`, `PropsWithChildren<T>`, `NoInfer<T>`) unwrapped. Sub-interfaces (e.g. `StoreCardAddressProps` referenced as a field in `StoreCardProps`) and helper unions belong in their own module — typically a sibling `*.types.ts`. Files that declare no component at all are not checked, so type-only `.tsx` modules and re-export-only files are unaffected.
|
|
11
|
+
- `enforce-render-naming` flags variables declared inside a top-level PascalCase component whose initializer holds or returns JSX, but whose name does not start with `render` followed by a camelCase boundary. Detected JSX-producing initializers include `JSXElement` / `JSXFragment` literals, conditional and logical expressions whose branch is JSX, arrays of JSX, arrow / function expressions whose body returns JSX, `.map` / `.flatMap` / `.filter` calls whose callback returns JSX, and `as` / `satisfies` wrappers around any of the above. Both value form (`const renderHeader = <div />`) and function form (`const renderHeader = () => <div />`) satisfy the rule — the convention checks intent via the prefix, not the shape.
|
|
12
|
+
|
|
13
|
+
All four rules ship in the `react`, `react/recommended`, `nextjs`, and `nextjs/recommended` presets at `warn` and `error` severity respectively. Total rule count is now 63 (40 base + 23 JSX). None of the new rules are auto-fixable: each surfaces a structural decision (where to put the data, where to put the type, what to name the fragment) that the author should make explicitly.
|
|
14
|
+
|
|
3
15
|
## 4.1.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -458,9 +458,12 @@ In practice: turn the high tier on as `"error"` first, leave the medium tier as
|
|
|
458
458
|
| Rule | Description | Fixable |
|
|
459
459
|
| ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ------- |
|
|
460
460
|
| [jsx-newline-between-elements](docs/rules/JSX_NEWLINE_BETWEEN_ELEMENTS.md) | Require empty lines between sibling multi-line JSX children | ✅ |
|
|
461
|
+
| [jsx-no-data-array](docs/rules/JSX_NO_DATA_ARRAY.md) | Disallow top-level array of object literals in `.tsx`/`.jsx` | ❌ |
|
|
462
|
+
| [jsx-no-data-object](docs/rules/JSX_NO_DATA_OBJECT.md) | Disallow top-level nested object literals in `.tsx`/`.jsx` | ❌ |
|
|
461
463
|
| [jsx-no-inline-object-prop](docs/rules/JSX_NO_INLINE_OBJECT_PROP.md) | Disallow inline object literals in JSX props | ❌ |
|
|
462
464
|
| [jsx-no-newline-single-line-elements](docs/rules/JSX_NO_NEWLINE_SINGLE_LINE_ELEMENTS.md) | Disallow empty lines between single-line sibling JSX elements | ✅ |
|
|
463
465
|
| [jsx-no-non-component-function](docs/rules/JSX_NO_NON_COMPONENT_FUNCTION.md) | Disallow non-component functions at top level in .tsx/.jsx files | ❌ |
|
|
466
|
+
| [jsx-no-sub-interface](docs/rules/JSX_NO_SUB_INTERFACE.md) | Disallow sub-interfaces and helper types in component files | ❌ |
|
|
464
467
|
| [jsx-no-ternary-null](docs/rules/JSX_NO_TERNARY_NULL.md) | Enforce logical AND over ternary with null/undefined in JSX | ✅ |
|
|
465
468
|
| [jsx-no-variable-in-callback](docs/rules/JSX_NO_VARIABLE_IN_CALLBACK.md) | Disallow variable declarations inside callback functions in JSX | ❌ |
|
|
466
469
|
| [jsx-require-suspense](docs/rules/JSX_REQUIRE_SUSPENSE.md) | Require lazy-loaded components to be wrapped in Suspense | ❌ |
|
|
@@ -474,6 +477,7 @@ In practice: turn the high tier on as `"error"` first, leave the medium tier as
|
|
|
474
477
|
| [react-props-destructure](docs/rules/REACT_PROPS_DESTRUCTURE.md) | Enforce destructuring props inside React component body | ❌ |
|
|
475
478
|
| [enforce-props-suffix](docs/rules/ENFORCE_PROPS_SUFFIX.md) | Enforce 'Props' suffix for interfaces and types in \*.tsx files | ❌ |
|
|
476
479
|
| [enforce-readonly-component-props](docs/rules/ENFORCE_READONLY_COMPONENT_PROPS.md) | Enforce Readonly wrapper for React component props | ✅ |
|
|
480
|
+
| [enforce-render-naming](docs/rules/ENFORCE_RENDER_NAMING.md) | Enforce 'render' prefix for variables holding JSX inside components | ❌ |
|
|
477
481
|
|
|
478
482
|
## Configurations
|
|
479
483
|
|
|
@@ -483,10 +487,10 @@ In practice: turn the high tier on as `"error"` first, leave the medium tier as
|
|
|
483
487
|
| -------------------- | -------- | ---------- | --------- | ----------- |
|
|
484
488
|
| `base` | warn | 40 | 0 | 40 |
|
|
485
489
|
| `base/recommended` | error | 40 | 0 | 40 |
|
|
486
|
-
| `react` | warn | 40 |
|
|
487
|
-
| `react/recommended` | error | 40 |
|
|
488
|
-
| `nextjs` | warn | 40 |
|
|
489
|
-
| `nextjs/recommended` | error | 40 |
|
|
490
|
+
| `react` | warn | 40 | 23 | 63 |
|
|
491
|
+
| `react/recommended` | error | 40 | 23 | 63 |
|
|
492
|
+
| `nextjs` | warn | 40 | 23 | 63 |
|
|
493
|
+
| `nextjs/recommended` | error | 40 | 23 | 63 |
|
|
490
494
|
|
|
491
495
|
The `nextjs` and `nextjs/recommended` presets currently share the same rule set as `react` and `react/recommended`; they are kept as named aliases for ergonomics.
|
|
492
496
|
|
|
@@ -535,16 +539,20 @@ Included in `base`, `base/recommended`, and all other presets:
|
|
|
535
539
|
- `nextfriday/sort-type-alphabetically`
|
|
536
540
|
- `nextfriday/sort-type-required-first`
|
|
537
541
|
|
|
538
|
-
### JSX Rules (
|
|
542
|
+
### JSX Rules (23 rules)
|
|
539
543
|
|
|
540
544
|
Additionally included in `react`, `react/recommended`, `nextjs`, `nextjs/recommended`:
|
|
541
545
|
|
|
542
546
|
- `nextfriday/enforce-props-suffix`
|
|
543
547
|
- `nextfriday/enforce-readonly-component-props`
|
|
548
|
+
- `nextfriday/enforce-render-naming`
|
|
544
549
|
- `nextfriday/jsx-newline-between-elements`
|
|
550
|
+
- `nextfriday/jsx-no-data-array`
|
|
551
|
+
- `nextfriday/jsx-no-data-object`
|
|
545
552
|
- `nextfriday/jsx-no-inline-object-prop`
|
|
546
553
|
- `nextfriday/jsx-no-newline-single-line-elements`
|
|
547
554
|
- `nextfriday/jsx-no-non-component-function`
|
|
555
|
+
- `nextfriday/jsx-no-sub-interface`
|
|
548
556
|
- `nextfriday/jsx-no-ternary-null`
|
|
549
557
|
- `nextfriday/jsx-no-variable-in-callback`
|
|
550
558
|
- `nextfriday/jsx-require-suspense`
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# enforce-render-naming
|
|
2
|
+
|
|
3
|
+
Enforce `render` prefix for variables that hold or return JSX inside React components.
|
|
4
|
+
|
|
5
|
+
## Rule Details
|
|
6
|
+
|
|
7
|
+
This rule flags variables declared inside a React component (a top-level PascalCase function or arrow function) whose initializer holds or returns JSX, but whose name does not start with `render`. The `render` prefix makes intent explicit: a reader scanning the component body can tell at a glance which locals are render fragments and which are plain data.
|
|
8
|
+
|
|
9
|
+
The rule applies inside any function or block nested within the component — not only the component's top-level body.
|
|
10
|
+
|
|
11
|
+
The prefix must be `render` followed by a camelCase boundary (uppercase letter, or end-of-name). Names like `renderer` (lowercase letter after the prefix) do not count.
|
|
12
|
+
|
|
13
|
+
### Why?
|
|
14
|
+
|
|
15
|
+
- **Self-documenting code**: `renderPhoneEntries` reads as "this is a render fragment for phone entries". `phoneEntries` reads as "this is data — a list of phones". The distinction matters when the same component holds both.
|
|
16
|
+
- **Refactor signal**: When you want to extract render fragments into separate components, grepping for `render*` finds candidates immediately.
|
|
17
|
+
- **Consistency**: Aligns with `enforce-hook-naming` (`use*` for hooks) and `enforce-service-naming` (`fetch*` for services) — name-by-purpose conventions throughout the plugin.
|
|
18
|
+
|
|
19
|
+
## Examples
|
|
20
|
+
|
|
21
|
+
### Incorrect
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
const Component = (props) => {
|
|
25
|
+
const header = <div />;
|
|
26
|
+
const cardElements = props.items.map((item) => <Card {...item} />);
|
|
27
|
+
const phoneEntries = props.phones.map((phone) => {
|
|
28
|
+
return <span>{phone}</span>;
|
|
29
|
+
});
|
|
30
|
+
const fallback = props.condition ? <A /> : <B />;
|
|
31
|
+
const banner = props.isVisible && <Banner />;
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
{header}
|
|
35
|
+
{cardElements}
|
|
36
|
+
{phoneEntries}
|
|
37
|
+
{fallback}
|
|
38
|
+
{banner}
|
|
39
|
+
</>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Correct
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
const Component = (props) => {
|
|
48
|
+
const renderHeader = <div />;
|
|
49
|
+
const renderCardElements = props.items.map((item) => <Card {...item} />);
|
|
50
|
+
const renderPhoneEntries = props.phones.map((phone) => {
|
|
51
|
+
return <span>{phone}</span>;
|
|
52
|
+
});
|
|
53
|
+
const renderFallback = props.condition ? <A /> : <B />;
|
|
54
|
+
const renderBanner = props.isVisible && <Banner />;
|
|
55
|
+
return (
|
|
56
|
+
<>
|
|
57
|
+
{renderHeader}
|
|
58
|
+
{renderCardElements}
|
|
59
|
+
{renderPhoneEntries}
|
|
60
|
+
{renderFallback}
|
|
61
|
+
{renderBanner}
|
|
62
|
+
</>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Both value form and function form are accepted as long as the prefix is correct:
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
const renderHeader = <div />; // value form — eval once at component render
|
|
71
|
+
const renderHeader = () => <div />; // function form — eval each call
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## What This Rule Checks
|
|
75
|
+
|
|
76
|
+
The rule fires when a `const` or `let` declaration is created **inside** a top-level PascalCase function (a React component) and the initializer is one of:
|
|
77
|
+
|
|
78
|
+
- A `JSXElement` or `JSXFragment` directly
|
|
79
|
+
- A `ConditionalExpression` whose consequent or alternate produces JSX
|
|
80
|
+
- A `LogicalExpression` whose right-hand side produces JSX
|
|
81
|
+
- An `ArrayExpression` containing JSX elements
|
|
82
|
+
- An `ArrowFunctionExpression` or `FunctionExpression` whose body returns JSX
|
|
83
|
+
- A `CallExpression` to `.map`, `.flatMap`, or `.filter` whose callback returns JSX
|
|
84
|
+
- A `TSAsExpression` or `TSSatisfiesExpression` wrapping any of the above
|
|
85
|
+
|
|
86
|
+
The rule applies only to `.tsx` and `.jsx` files. Variables declared outside any PascalCase function (top-level module constants, helpers in plain functions) are not checked.
|
|
87
|
+
|
|
88
|
+
## When Not To Use It
|
|
89
|
+
|
|
90
|
+
If your team prefers other naming conventions for extracted JSX (e.g., `*Element`, `*Section`, `*Fragment`), or if you do not extract JSX into intermediate variables in the first place.
|
|
91
|
+
|
|
92
|
+
## Related Rules
|
|
93
|
+
|
|
94
|
+
- [enforce-hook-naming](ENFORCE_HOOK_NAMING.md)
|
|
95
|
+
- [enforce-service-naming](ENFORCE_SERVICE_NAMING.md)
|
|
96
|
+
- [boolean-naming-prefix](BOOLEAN_NAMING_PREFIX.md)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# jsx-no-data-array
|
|
2
|
+
|
|
3
|
+
Disallow top-level arrays of object literals in `.tsx`/`.jsx` files (extract to a data file).
|
|
4
|
+
|
|
5
|
+
## Rule Details
|
|
6
|
+
|
|
7
|
+
This rule flags top-level `const` declarations in `.tsx` and `.jsx` files whose initializer is an array literal containing one or more object literals. This shape is a strong signal of fixture / seed / content data, which belongs in a sibling data module — not in a component file.
|
|
8
|
+
|
|
9
|
+
The rule fires on both unexported declarations and `export const` declarations, and on `as const` / `satisfies` variants.
|
|
10
|
+
|
|
11
|
+
### Why?
|
|
12
|
+
|
|
13
|
+
- **Separation of concerns**: Component files should describe rendering. Content / fixture data belongs in `*.data.ts`, `*.fixtures.ts`, a CMS, or a content layer.
|
|
14
|
+
- **Diff hygiene**: Editing one address line should not produce noise inside the component diff.
|
|
15
|
+
- **AI assistant pressure**: LLMs default to inlining mock data when they cannot find a data layer; a lint rule is the cheapest way to redirect them.
|
|
16
|
+
|
|
17
|
+
## Examples
|
|
18
|
+
|
|
19
|
+
### Incorrect
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
const stores = [{ name: "Koh Samui", isOpen: true }];
|
|
23
|
+
const items: Item[] = [{ id: 1 }, { id: 2 }];
|
|
24
|
+
const config = [{ key: "a" }] as const;
|
|
25
|
+
const data = [{ id: 1 }] satisfies readonly { id: number }[];
|
|
26
|
+
export const navItems = [{ label: "Home", href: "/" }];
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Correct
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
const TIMEOUT_MS = 1000;
|
|
33
|
+
const labels = ["Home", "About", "Contact"];
|
|
34
|
+
const numbers = [1, 2, 3];
|
|
35
|
+
const ROUTES = { home: "/", about: "/about" };
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Move arrays of object literals to a sibling data file:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
// stores.data.ts
|
|
42
|
+
export const stores = [{ name: "Koh Samui", isOpen: true }];
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
// HeaderStore.tsx
|
|
47
|
+
import { stores } from "./stores.data";
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## What This Rule Checks
|
|
51
|
+
|
|
52
|
+
The rule fires when a top-level `const` declaration's initializer (after unwrapping `as`/`satisfies`) is an `ArrayExpression` and at least one of its elements is an `ObjectExpression` (also after unwrapping). Arrays of primitives, identifiers, or member expressions are not flagged.
|
|
53
|
+
|
|
54
|
+
The rule applies only to `.tsx` and `.jsx` files. Plain `.ts` and `.js` files are not checked — extract data files into those extensions.
|
|
55
|
+
|
|
56
|
+
## When Not To Use It
|
|
57
|
+
|
|
58
|
+
If your project deliberately co-locates small static data with components, or if you treat `.tsx` files as both component and data layer.
|
|
59
|
+
|
|
60
|
+
## Related Rules
|
|
61
|
+
|
|
62
|
+
- [jsx-no-data-object](JSX_NO_DATA_OBJECT.md)
|
|
63
|
+
- [jsx-no-non-component-function](JSX_NO_NON_COMPONENT_FUNCTION.md)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# jsx-no-data-object
|
|
2
|
+
|
|
3
|
+
Disallow top-level nested object literals in `.tsx`/`.jsx` files (extract to a data file).
|
|
4
|
+
|
|
5
|
+
## Rule Details
|
|
6
|
+
|
|
7
|
+
This rule flags top-level `const` declarations in `.tsx` and `.jsx` files whose initializer is an object literal that contains at least one nested object or nested array. The "nesting" is the smell — flat maps of primitives are fine, but objects-of-objects or objects-of-object-arrays look like configuration / content data and belong in a data module.
|
|
8
|
+
|
|
9
|
+
The rule fires on both unexported declarations and `export const` declarations, and on `as const` / `satisfies` variants.
|
|
10
|
+
|
|
11
|
+
### Why?
|
|
12
|
+
|
|
13
|
+
- **Separation of concerns**: Component files should describe rendering. Nested configuration / content belongs in a sibling data module.
|
|
14
|
+
- **Diff hygiene**: Tweaking a nested setting should not bloat the component diff.
|
|
15
|
+
- **AI assistant pressure**: LLMs default to inlining mock config when they cannot find a data layer; a lint rule is the cheapest way to redirect them.
|
|
16
|
+
|
|
17
|
+
## Examples
|
|
18
|
+
|
|
19
|
+
### Incorrect
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
const config = { home: { url: "/" } };
|
|
23
|
+
const config = { items: [{ id: 1 }] };
|
|
24
|
+
const matrix = {
|
|
25
|
+
values: [
|
|
26
|
+
[1, 2],
|
|
27
|
+
[3, 4],
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
const config = { a: { b: 1 } } as const;
|
|
31
|
+
export const config = { a: { b: 1 } };
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Correct
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
const TIMEOUT_MS = 1000;
|
|
38
|
+
const ROUTES = { home: "/", about: "/about", contact: "/contact" };
|
|
39
|
+
const labels = ["Home", "About"];
|
|
40
|
+
const config = { a: 1, b: "x", c: true };
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Move nested objects to a sibling data file:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// settings.data.ts
|
|
47
|
+
export const settings = { home: { url: "/" } };
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// HomePage.tsx
|
|
52
|
+
import { settings } from "./settings.data";
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## What This Rule Checks
|
|
56
|
+
|
|
57
|
+
The rule fires when a top-level `const` declaration's initializer (after unwrapping `as`/`satisfies`) is an `ObjectExpression` whose properties contain at least one of:
|
|
58
|
+
|
|
59
|
+
- A property whose value is another `ObjectExpression`
|
|
60
|
+
- A property whose value is an `ArrayExpression` containing an `ObjectExpression` or another `ArrayExpression`
|
|
61
|
+
|
|
62
|
+
Flat maps of primitives are not flagged. The rule applies only to `.tsx` and `.jsx` files.
|
|
63
|
+
|
|
64
|
+
## When Not To Use It
|
|
65
|
+
|
|
66
|
+
If your project deliberately co-locates small nested config with components, or if you treat `.tsx` files as both component and data layer.
|
|
67
|
+
|
|
68
|
+
## Related Rules
|
|
69
|
+
|
|
70
|
+
- [jsx-no-data-array](JSX_NO_DATA_ARRAY.md)
|
|
71
|
+
- [jsx-no-non-component-function](JSX_NO_NON_COMPONENT_FUNCTION.md)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# jsx-no-sub-interface
|
|
2
|
+
|
|
3
|
+
Disallow sub-interfaces and helper types in component files; keep only the main component props.
|
|
4
|
+
|
|
5
|
+
## Rule Details
|
|
6
|
+
|
|
7
|
+
This rule flags any top-level `interface` or `type` declaration in a `.tsx` / `.jsx` file that is not the main props type for a component declared in the same file. Sub-interfaces (e.g., `StoreCardAddressProps` referenced as a field in `StoreCardProps`) and helper unions (e.g., `type StoreCardKind = "a" | "b"`) should live in their own modules — typically a sibling `*.types.ts` file or alongside their own component.
|
|
8
|
+
|
|
9
|
+
The "main" props type is determined by the parameter type of a top-level PascalCase function or arrow component. Common wrappers like `Readonly<T>`, `Required<T>`, `Partial<T>`, `PropsWithChildren<T>`, and `NoInfer<T>` are unwrapped to find the underlying type name.
|
|
10
|
+
|
|
11
|
+
If the file declares no component at all, the rule does not fire — pure `.tsx` / `.jsx` files used as type or re-export modules are skipped.
|
|
12
|
+
|
|
13
|
+
### Why?
|
|
14
|
+
|
|
15
|
+
- **Single responsibility per file**: A component file should describe one component. Sub-types pollute it with shapes that are referenced but not the component's own concern.
|
|
16
|
+
- **Reusability**: Sub-types extracted to their own module can be imported by sibling components and tests; trapped inside a component file, they cannot.
|
|
17
|
+
- **Diff hygiene**: Editing a sub-interface should not produce noise in the component diff.
|
|
18
|
+
|
|
19
|
+
## Examples
|
|
20
|
+
|
|
21
|
+
### Incorrect
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
interface StoreCardProps {
|
|
25
|
+
address: StoreCardAddressProps;
|
|
26
|
+
image: StoreCardImageProps;
|
|
27
|
+
name: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface StoreCardAddressProps {
|
|
31
|
+
label: string;
|
|
32
|
+
mapUrl: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface StoreCardImageProps {
|
|
36
|
+
alt: string;
|
|
37
|
+
src: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type StoreCardKind = "phone" | "whatsapp";
|
|
41
|
+
|
|
42
|
+
const StoreCard = (props: Readonly<StoreCardProps>) => <div />;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Correct
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import type { StoreCardAddressProps } from "./store-card-address.types";
|
|
49
|
+
import type { StoreCardImageProps } from "./store-card-image.types";
|
|
50
|
+
|
|
51
|
+
interface StoreCardProps {
|
|
52
|
+
address: StoreCardAddressProps;
|
|
53
|
+
image: StoreCardImageProps;
|
|
54
|
+
name: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const StoreCard = (props: Readonly<StoreCardProps>) => <div />;
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// store-card-address.types.ts
|
|
62
|
+
export interface StoreCardAddressProps {
|
|
63
|
+
label: string;
|
|
64
|
+
mapUrl: string;
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## What This Rule Checks
|
|
69
|
+
|
|
70
|
+
The rule walks the program body and collects:
|
|
71
|
+
|
|
72
|
+
- **Components** — top-level PascalCase `function` declarations and PascalCase `const X = (...) => ...` or `const X = function(...) {}` initializers, including ones wrapped in `export` / `export default`.
|
|
73
|
+
- **Main types** — for each component, the type referenced by its first parameter's annotation, after unwrapping `Readonly<T>`, `Required<T>`, `Partial<T>`, `PropsWithChildren<T>`, and `NoInfer<T>`.
|
|
74
|
+
- **Top-level type declarations** — `interface` and `type` declarations at the top level (including ones wrapped in `export`).
|
|
75
|
+
|
|
76
|
+
Any top-level type declaration whose name is not in the set of main types is flagged. The rule does not fire when the file has no component.
|
|
77
|
+
|
|
78
|
+
## When Not To Use It
|
|
79
|
+
|
|
80
|
+
If your project deliberately co-locates sub-types and helper types with their consuming component, or if you keep the component and its supporting types in a single file by convention.
|
|
81
|
+
|
|
82
|
+
## Related Rules
|
|
83
|
+
|
|
84
|
+
- [enforce-props-suffix](ENFORCE_PROPS_SUFFIX.md)
|
|
85
|
+
- [prefer-interface-for-component-props](PREFER_INTERFACE_FOR_COMPONENT_PROPS.md)
|
|
86
|
+
- [prefer-interface-over-inline-types](PREFER_INTERFACE_OVER_INLINE_TYPES.md)
|