eslint-plugin-nextfriday 4.0.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 +22 -0
- package/README.md +17 -15
- 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/docs/rules/NO_GHOST_WRAPPER.md +75 -0
- package/docs/rules/NO_REDUNDANT_FRAGMENT.md +56 -0
- package/lib/index.cjs +1037 -442
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +252 -0
- package/lib/index.d.ts +252 -0
- package/lib/index.js +1037 -442
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
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
|
+
|
|
15
|
+
## 4.1.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- [#124](https://github.com/next-friday/eslint-plugin-nextfriday/pull/124) [`be12809`](https://github.com/next-friday/eslint-plugin-nextfriday/commit/be128098e2af102a153c3d01546a2921518f4b96) Thanks [@joetakara](https://github.com/joetakara)! - add two JSX rules targeting unnecessary wrapper noise (the "Divitis" anti-pattern):
|
|
20
|
+
- `no-ghost-wrapper` flags bare `<div>` / `<span>` elements that carry no meaningful attributes. Self-closing variants are checked the same way. The `key` prop alone does not silence the rule, since `key` carries no structural intent. Any other attribute — `className`, `style`, `id`, `ref`, `role`, `aria-*`, `data-*`, `tabIndex`, event handlers, or spread attributes — is considered meaningful and lets the element pass.
|
|
21
|
+
- `no-redundant-fragment` flags Fragments (`<>...</>`, `<Fragment>`, `<React.Fragment>`) that wrap zero or one child. JSX text consisting only of whitespace is not counted. Long-form Fragments carrying a `key` attribute are exempt, since `key` is the canonical reason long-form Fragment exists (the shorthand `<>...</>` cannot accept attributes).
|
|
22
|
+
|
|
23
|
+
Both rules are report-only — no autofix is provided so authors retain control over which structural alternative (Fragment, semantic element, unwrapping the children) best fits the surrounding code. Both are added to the JSX rule tier and ship in the `react`, `react/recommended`, `nextjs`, and `nextjs/recommended` presets at `warn` and `error` severity respectively.
|
|
24
|
+
|
|
3
25
|
## 4.0.0
|
|
4
26
|
|
|
5
27
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -458,20 +458,26 @@ 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 | ❌ |
|
|
467
470
|
| [jsx-simple-props](docs/rules/JSX_SIMPLE_PROPS.md) | Enforce simple prop values (strings, variables, callbacks, ReactNode) | ❌ |
|
|
468
471
|
| [jsx-sort-props](docs/rules/JSX_SORT_PROPS.md) | Enforce JSX props are sorted by value type | ✅ |
|
|
469
472
|
| [jsx-spread-props-last](docs/rules/JSX_SPREAD_PROPS_LAST.md) | Enforce JSX spread attributes appear after all other props | ❌ |
|
|
473
|
+
| [no-ghost-wrapper](docs/rules/NO_GHOST_WRAPPER.md) | Disallow bare `<div>`/`<span>` with no meaningful attributes | ❌ |
|
|
474
|
+
| [no-redundant-fragment](docs/rules/NO_REDUNDANT_FRAGMENT.md) | Disallow Fragments wrapping zero or one child | ❌ |
|
|
470
475
|
| [prefer-jsx-template-literals](docs/rules/PREFER_JSX_TEMPLATE_LITERALS.md) | Enforce template literals instead of mixing text and JSX expressions | ✅ |
|
|
471
476
|
| [prefer-props-with-children](docs/rules/PREFER_PROPS_WITH_CHILDREN.md) | Prefer PropsWithChildren over manually declaring children: ReactNode | ❌ |
|
|
472
477
|
| [react-props-destructure](docs/rules/REACT_PROPS_DESTRUCTURE.md) | Enforce destructuring props inside React component body | ❌ |
|
|
473
478
|
| [enforce-props-suffix](docs/rules/ENFORCE_PROPS_SUFFIX.md) | Enforce 'Props' suffix for interfaces and types in \*.tsx files | ❌ |
|
|
474
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 | ❌ |
|
|
475
481
|
|
|
476
482
|
## Configurations
|
|
477
483
|
|
|
@@ -481,10 +487,10 @@ In practice: turn the high tier on as `"error"` first, leave the medium tier as
|
|
|
481
487
|
| -------------------- | -------- | ---------- | --------- | ----------- |
|
|
482
488
|
| `base` | warn | 40 | 0 | 40 |
|
|
483
489
|
| `base/recommended` | error | 40 | 0 | 40 |
|
|
484
|
-
| `react` | warn | 40 |
|
|
485
|
-
| `react/recommended` | error | 40 |
|
|
486
|
-
| `nextjs` | warn | 40 |
|
|
487
|
-
| `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 |
|
|
488
494
|
|
|
489
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.
|
|
490
496
|
|
|
@@ -533,22 +539,28 @@ Included in `base`, `base/recommended`, and all other presets:
|
|
|
533
539
|
- `nextfriday/sort-type-alphabetically`
|
|
534
540
|
- `nextfriday/sort-type-required-first`
|
|
535
541
|
|
|
536
|
-
### JSX Rules (
|
|
542
|
+
### JSX Rules (23 rules)
|
|
537
543
|
|
|
538
544
|
Additionally included in `react`, `react/recommended`, `nextjs`, `nextjs/recommended`:
|
|
539
545
|
|
|
540
546
|
- `nextfriday/enforce-props-suffix`
|
|
541
547
|
- `nextfriday/enforce-readonly-component-props`
|
|
548
|
+
- `nextfriday/enforce-render-naming`
|
|
542
549
|
- `nextfriday/jsx-newline-between-elements`
|
|
550
|
+
- `nextfriday/jsx-no-data-array`
|
|
551
|
+
- `nextfriday/jsx-no-data-object`
|
|
543
552
|
- `nextfriday/jsx-no-inline-object-prop`
|
|
544
553
|
- `nextfriday/jsx-no-newline-single-line-elements`
|
|
545
554
|
- `nextfriday/jsx-no-non-component-function`
|
|
555
|
+
- `nextfriday/jsx-no-sub-interface`
|
|
546
556
|
- `nextfriday/jsx-no-ternary-null`
|
|
547
557
|
- `nextfriday/jsx-no-variable-in-callback`
|
|
548
558
|
- `nextfriday/jsx-require-suspense`
|
|
549
559
|
- `nextfriday/jsx-simple-props`
|
|
550
560
|
- `nextfriday/jsx-sort-props`
|
|
551
561
|
- `nextfriday/jsx-spread-props-last`
|
|
562
|
+
- `nextfriday/no-ghost-wrapper`
|
|
563
|
+
- `nextfriday/no-redundant-fragment`
|
|
552
564
|
- `nextfriday/prefer-interface-for-component-props`
|
|
553
565
|
- `nextfriday/prefer-interface-over-inline-types`
|
|
554
566
|
- `nextfriday/prefer-jsx-template-literals`
|
|
@@ -571,16 +583,6 @@ Additionally included in `react`, `react/recommended`, `nextjs`, `nextjs/recomme
|
|
|
571
583
|
- **Clean code practices**: Prevents emoji usage, enforces parameter destructuring, and more
|
|
572
584
|
- **Formatting rules**: Enforces consistent blank lines around multi-line blocks and return statements
|
|
573
585
|
|
|
574
|
-
## Agent Skill
|
|
575
|
-
|
|
576
|
-
This plugin ships with an [Agent Skill](https://github.com/anthropics/skills) that teaches AI coding assistants (Claude Code, Cursor, etc.) all 57 rules so they generate compliant code from the start.
|
|
577
|
-
|
|
578
|
-
```bash
|
|
579
|
-
npx skills add next-friday/eslint-plugin-nextfriday --skill eslint-plugin-nextfriday
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
Once installed, AI assistants will automatically follow the naming, code style, type, JSX, import, and formatting patterns enforced by this plugin — reducing lint errors to zero.
|
|
583
|
-
|
|
584
586
|
## Need Help?
|
|
585
587
|
|
|
586
588
|
If you encounter any issues or have questions:
|
|
@@ -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)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# no-ghost-wrapper
|
|
2
|
+
|
|
3
|
+
Disallow bare `<div>` and `<span>` elements that have no meaningful attributes.
|
|
4
|
+
|
|
5
|
+
## Rule Details
|
|
6
|
+
|
|
7
|
+
This rule flags `<div>` and `<span>` elements that carry zero meaningful attributes — the "ghost wrapper" anti-pattern (also called "Divitis"). These elements add depth to the DOM tree without contributing semantics, styling, behavior, accessibility hooks, or test hooks.
|
|
8
|
+
|
|
9
|
+
The `key` prop alone does not count as meaningful: `key` is a React-only signal for list reconciliation and carries no structural intent.
|
|
10
|
+
|
|
11
|
+
### Why?
|
|
12
|
+
|
|
13
|
+
- **Semantic clarity**: A wrapper without attributes signals that the author has not decided whether the element is structural, presentational, or semantic.
|
|
14
|
+
- **DOM bloat**: Empty wrappers extend the DOM tree, increasing layout work and accessibility-tree noise.
|
|
15
|
+
- **Cognitive load**: Reviewers must guess whether a bare wrapper is intentional or a leftover from refactoring.
|
|
16
|
+
- **Better alternatives exist**: Fragments (`<>...</>`), semantic elements (`<section>`, `<article>`, `<header>`, `<nav>`), or simply unwrapping the children.
|
|
17
|
+
|
|
18
|
+
## Examples
|
|
19
|
+
|
|
20
|
+
### Incorrect
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
<div>x</div>
|
|
24
|
+
<span>text</span>
|
|
25
|
+
<div></div>
|
|
26
|
+
<div> </div>
|
|
27
|
+
<div />
|
|
28
|
+
<span />
|
|
29
|
+
<div key={item.id}>x</div>
|
|
30
|
+
<div>{children}</div>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Correct
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
<div className="container">x</div>
|
|
37
|
+
<div data-testid="root">x</div>
|
|
38
|
+
<div role="button">x</div>
|
|
39
|
+
<div aria-label="close">x</div>
|
|
40
|
+
<div ref={ref}>x</div>
|
|
41
|
+
<div onClick={handleClick}>x</div>
|
|
42
|
+
<div id="anchor">x</div>
|
|
43
|
+
<div style={{ color: "red" }}>x</div>
|
|
44
|
+
<div {...props}>x</div>
|
|
45
|
+
<div tabIndex={0}>x</div>
|
|
46
|
+
<section>x</section>
|
|
47
|
+
<article>x</article>
|
|
48
|
+
<>x</>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## What This Rule Checks
|
|
52
|
+
|
|
53
|
+
The rule fires on a `<div>` or `<span>` opening element when, after filtering out a lone `key` attribute, it has zero remaining attributes (including spread attributes). Self-closing elements (`<div />`, `<span />`) are checked the same way.
|
|
54
|
+
|
|
55
|
+
Any of the following attributes count as meaningful and silence the rule:
|
|
56
|
+
|
|
57
|
+
- `className`
|
|
58
|
+
- `style`
|
|
59
|
+
- `id`
|
|
60
|
+
- `ref`
|
|
61
|
+
- `role`
|
|
62
|
+
- `aria-*`
|
|
63
|
+
- `data-*`
|
|
64
|
+
- `tabIndex`
|
|
65
|
+
- Event handlers (`onClick`, `onChange`, etc.)
|
|
66
|
+
- Spread attributes (`{...props}`)
|
|
67
|
+
- Any other JSX attribute except `key`
|
|
68
|
+
|
|
69
|
+
## When Not To Use It
|
|
70
|
+
|
|
71
|
+
If your codebase intentionally uses bare `<div>` and `<span>` as structural placeholders, or if you rely on parent CSS selectors that target a fixed wrapper depth.
|
|
72
|
+
|
|
73
|
+
## Related Rules
|
|
74
|
+
|
|
75
|
+
- [no-redundant-fragment](NO_REDUNDANT_FRAGMENT.md)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# no-redundant-fragment
|
|
2
|
+
|
|
3
|
+
Disallow Fragments that wrap zero or one child.
|
|
4
|
+
|
|
5
|
+
## Rule Details
|
|
6
|
+
|
|
7
|
+
This rule flags Fragments (`<>...</>`, `<Fragment>...</Fragment>`, `<React.Fragment>...</React.Fragment>`) that wrap zero or one child. A Fragment is only justified when grouping multiple sibling nodes, or when a `key` prop is required during list iteration.
|
|
8
|
+
|
|
9
|
+
A Fragment with a single child is structurally equivalent to that child alone. An empty Fragment renders nothing and adds noise to the source.
|
|
10
|
+
|
|
11
|
+
### Why?
|
|
12
|
+
|
|
13
|
+
- **Source clarity**: A Fragment around a single child is dead syntax — it tells the reader nothing.
|
|
14
|
+
- **Less noise in the AST**: Devtools, search, and codemods do not have to step over an extra layer.
|
|
15
|
+
- **Forces a decision**: Either the children are siblings (Fragment is the right tool) or they are not (drop it).
|
|
16
|
+
|
|
17
|
+
## Examples
|
|
18
|
+
|
|
19
|
+
### Incorrect
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
<></>
|
|
23
|
+
<>{x}</>
|
|
24
|
+
<><A /></>
|
|
25
|
+
<>text</>
|
|
26
|
+
<Fragment>x</Fragment>
|
|
27
|
+
<React.Fragment>{x}</React.Fragment>
|
|
28
|
+
<React.Fragment></React.Fragment>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Correct
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
<>{a}{b}</>
|
|
35
|
+
<><A /><B /></>
|
|
36
|
+
<Fragment>{a}{b}</Fragment>
|
|
37
|
+
<React.Fragment>{a}{b}</React.Fragment>
|
|
38
|
+
|
|
39
|
+
// key is the legitimate reason to use long-form Fragment:
|
|
40
|
+
<React.Fragment key={item.id}>{item.label}{item.value}</React.Fragment>
|
|
41
|
+
<Fragment key={k}>{x}</Fragment>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## What This Rule Checks
|
|
45
|
+
|
|
46
|
+
A Fragment is flagged when its meaningful children count is `0` or `1`. JSX text nodes that contain only whitespace are not counted as children.
|
|
47
|
+
|
|
48
|
+
The rule does not fire on a long-form Fragment (`<Fragment>` or `<React.Fragment>`) that carries a `key` attribute — `key` is the canonical reason long-form Fragment exists, since the shorthand `<>...</>` cannot accept attributes.
|
|
49
|
+
|
|
50
|
+
## When Not To Use It
|
|
51
|
+
|
|
52
|
+
If your codebase uses single-child Fragments to keep diffs stable around conditionally-rendered siblings, or if you rely on a build-time transform that depends on the Fragment wrapper.
|
|
53
|
+
|
|
54
|
+
## Related Rules
|
|
55
|
+
|
|
56
|
+
- [no-ghost-wrapper](NO_GHOST_WRAPPER.md)
|