eslint-plugin-nextfriday 4.1.0 → 4.3.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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # eslint-plugin-nextfriday
2
2
 
3
+ ## 4.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#128](https://github.com/next-friday/eslint-plugin-nextfriday/pull/128) [`b165ec5`](https://github.com/next-friday/eslint-plugin-nextfriday/commit/b165ec51eb3ec870a546ab808821426186543356) Thanks [@joetakara](https://github.com/joetakara)! - add enforce-hook-filename, enforce-test-filename, no-helper-function-in-hook, no-helper-function-in-test rules
8
+
9
+ ## 4.2.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#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.
14
+ - `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.
15
+ - `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.
16
+ - `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.
17
+ - `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.
18
+
19
+ 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.
20
+
3
21
  ## 4.1.0
4
22
 
5
23
  ### Minor Changes
package/README.md CHANGED
@@ -413,6 +413,8 @@ In practice: turn the high tier on as `"error"` first, leave the medium tier as
413
413
  | [require-explicit-return-type](docs/rules/REQUIRE_EXPLICIT_RETURN_TYPE.md) | Require explicit return types on functions for better documentation | ❌ |
414
414
  | [no-complex-inline-return](docs/rules/NO_COMPLEX_INLINE_RETURN.md) | Disallow complex inline expressions in return - extract to const first | ❌ |
415
415
  | [no-logic-in-params](docs/rules/NO_LOGIC_IN_PARAMS.md) | Disallow logic/conditions in function parameters - extract to const | ❌ |
416
+ | [enforce-hook-filename](docs/rules/ENFORCE_HOOK_FILENAME.md) | Enforce that files exporting custom hooks are named \*.hook.ts | ❌ |
417
+ | [enforce-test-filename](docs/rules/ENFORCE_TEST_FILENAME.md) | Enforce that files containing test code are named \*.test.ts | ❌ |
416
418
  | [enforce-hook-naming](docs/rules/ENFORCE_HOOK_NAMING.md) | Enforce 'use' prefix for functions in \*.hook.ts files | ❌ |
417
419
  | [enforce-service-naming](docs/rules/ENFORCE_SERVICE_NAMING.md) | Enforce 'fetch' prefix for async functions in \*.service.ts files | ❌ |
418
420
  | [enforce-sorted-destructuring](docs/rules/ENFORCE_SORTED_DESTRUCTURING.md) | Enforce alphabetical sorting of destructured properties | ✅ |
@@ -427,6 +429,8 @@ In practice: turn the high tier on as `"error"` first, leave the medium tier as
427
429
  | [prefer-async-await](docs/rules/PREFER_ASYNC_AWAIT.md) | Enforce async/await over .then() promise chains | ❌ |
428
430
  | [no-nested-ternary](docs/rules/NO_NESTED_TERNARY.md) | Disallow nested ternary expressions | ❌ |
429
431
  | [prefer-guard-clause](docs/rules/PREFER_GUARD_CLAUSE.md) | Enforce guard clause pattern instead of nested if statements | ❌ |
432
+ | [no-helper-function-in-hook](docs/rules/NO_HELPER_FUNCTION_IN_HOOK.md) | Disallow non-hook helper function definitions in hook files | ❌ |
433
+ | [no-helper-function-in-test](docs/rules/NO_HELPER_FUNCTION_IN_TEST.md) | Disallow helper function definitions in test files | ❌ |
430
434
 
431
435
  ### Import Optimization Rules
432
436
 
@@ -458,9 +462,12 @@ In practice: turn the high tier on as `"error"` first, leave the medium tier as
458
462
  | Rule | Description | Fixable |
459
463
  | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ------- |
460
464
  | [jsx-newline-between-elements](docs/rules/JSX_NEWLINE_BETWEEN_ELEMENTS.md) | Require empty lines between sibling multi-line JSX children | ✅ |
465
+ | [jsx-no-data-array](docs/rules/JSX_NO_DATA_ARRAY.md) | Disallow top-level array of object literals in `.tsx`/`.jsx` | ❌ |
466
+ | [jsx-no-data-object](docs/rules/JSX_NO_DATA_OBJECT.md) | Disallow top-level nested object literals in `.tsx`/`.jsx` | ❌ |
461
467
  | [jsx-no-inline-object-prop](docs/rules/JSX_NO_INLINE_OBJECT_PROP.md) | Disallow inline object literals in JSX props | ❌ |
462
468
  | [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
469
  | [jsx-no-non-component-function](docs/rules/JSX_NO_NON_COMPONENT_FUNCTION.md) | Disallow non-component functions at top level in .tsx/.jsx files | ❌ |
470
+ | [jsx-no-sub-interface](docs/rules/JSX_NO_SUB_INTERFACE.md) | Disallow sub-interfaces and helper types in component files | ❌ |
464
471
  | [jsx-no-ternary-null](docs/rules/JSX_NO_TERNARY_NULL.md) | Enforce logical AND over ternary with null/undefined in JSX | ✅ |
465
472
  | [jsx-no-variable-in-callback](docs/rules/JSX_NO_VARIABLE_IN_CALLBACK.md) | Disallow variable declarations inside callback functions in JSX | ❌ |
466
473
  | [jsx-require-suspense](docs/rules/JSX_REQUIRE_SUSPENSE.md) | Require lazy-loaded components to be wrapped in Suspense | ❌ |
@@ -474,6 +481,7 @@ In practice: turn the high tier on as `"error"` first, leave the medium tier as
474
481
  | [react-props-destructure](docs/rules/REACT_PROPS_DESTRUCTURE.md) | Enforce destructuring props inside React component body | ❌ |
475
482
  | [enforce-props-suffix](docs/rules/ENFORCE_PROPS_SUFFIX.md) | Enforce 'Props' suffix for interfaces and types in \*.tsx files | ❌ |
476
483
  | [enforce-readonly-component-props](docs/rules/ENFORCE_READONLY_COMPONENT_PROPS.md) | Enforce Readonly wrapper for React component props | ✅ |
484
+ | [enforce-render-naming](docs/rules/ENFORCE_RENDER_NAMING.md) | Enforce 'render' prefix for variables holding JSX inside components | ❌ |
477
485
 
478
486
  ## Configurations
479
487
 
@@ -481,23 +489,25 @@ In practice: turn the high tier on as `"error"` first, leave the medium tier as
481
489
 
482
490
  | Preset | Severity | Base Rules | JSX Rules | Total Rules |
483
491
  | -------------------- | -------- | ---------- | --------- | ----------- |
484
- | `base` | warn | 40 | 0 | 40 |
485
- | `base/recommended` | error | 40 | 0 | 40 |
486
- | `react` | warn | 40 | 19 | 59 |
487
- | `react/recommended` | error | 40 | 19 | 59 |
488
- | `nextjs` | warn | 40 | 19 | 59 |
489
- | `nextjs/recommended` | error | 40 | 19 | 59 |
492
+ | `base` | warn | 42 | 0 | 42 |
493
+ | `base/recommended` | error | 42 | 0 | 42 |
494
+ | `react` | warn | 42 | 23 | 65 |
495
+ | `react/recommended` | error | 42 | 23 | 65 |
496
+ | `nextjs` | warn | 42 | 23 | 65 |
497
+ | `nextjs/recommended` | error | 42 | 23 | 65 |
490
498
 
491
499
  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
500
 
493
- ### Base Configuration Rules (40 rules)
501
+ ### Base Configuration Rules (42 rules)
494
502
 
495
503
  Included in `base`, `base/recommended`, and all other presets:
496
504
 
497
505
  - `nextfriday/boolean-naming-prefix`
498
506
  - `nextfriday/enforce-camel-case`
499
507
  - `nextfriday/enforce-constant-case`
508
+ - `nextfriday/enforce-hook-filename`
500
509
  - `nextfriday/enforce-hook-naming`
510
+ - `nextfriday/enforce-test-filename`
501
511
  - `nextfriday/enforce-property-case`
502
512
  - `nextfriday/enforce-service-naming`
503
513
  - `nextfriday/enforce-sorted-destructuring`
@@ -535,16 +545,20 @@ Included in `base`, `base/recommended`, and all other presets:
535
545
  - `nextfriday/sort-type-alphabetically`
536
546
  - `nextfriday/sort-type-required-first`
537
547
 
538
- ### JSX Rules (19 rules)
548
+ ### JSX Rules (23 rules)
539
549
 
540
550
  Additionally included in `react`, `react/recommended`, `nextjs`, `nextjs/recommended`:
541
551
 
542
552
  - `nextfriday/enforce-props-suffix`
543
553
  - `nextfriday/enforce-readonly-component-props`
554
+ - `nextfriday/enforce-render-naming`
544
555
  - `nextfriday/jsx-newline-between-elements`
556
+ - `nextfriday/jsx-no-data-array`
557
+ - `nextfriday/jsx-no-data-object`
545
558
  - `nextfriday/jsx-no-inline-object-prop`
546
559
  - `nextfriday/jsx-no-newline-single-line-elements`
547
560
  - `nextfriday/jsx-no-non-component-function`
561
+ - `nextfriday/jsx-no-sub-interface`
548
562
  - `nextfriday/jsx-no-ternary-null`
549
563
  - `nextfriday/jsx-no-variable-in-callback`
550
564
  - `nextfriday/jsx-require-suspense`
@@ -0,0 +1,77 @@
1
+ # enforce-hook-filename
2
+
3
+ Enforce that files exporting custom hooks are named `*.hook.ts` or `*.hooks.ts`.
4
+
5
+ ## Rule Details
6
+
7
+ Any file that exports a custom hook (a function whose name starts with `use` followed by an uppercase letter) must have a `.hook.ts` or `.hooks.ts` suffix. This ensures hook files are consistently discoverable and that `no-helper-function-in-hook` can be reliably scoped to them.
8
+
9
+ ### Why?
10
+
11
+ Without enforced naming, hooks can silently live inside service files, utility files, or component files — breaking the `files` glob that powers `no-helper-function-in-hook` and making codebases harder to navigate.
12
+
13
+ ## Examples
14
+
15
+ ### Incorrect
16
+
17
+ ```ts
18
+ // use-user-data.ts ❌
19
+ export function useUserData() {
20
+ return null;
21
+ }
22
+ ```
23
+
24
+ ```ts
25
+ // user.service.ts ❌
26
+ export const useCurrentUser = () => null;
27
+ ```
28
+
29
+ ```ts
30
+ // UserCard.tsx ❌
31
+ export function useUserCard() {
32
+ return null;
33
+ }
34
+ ```
35
+
36
+ ### Correct
37
+
38
+ ```ts
39
+ // use-user-data.hook.ts ✅
40
+ export function useUserData() {
41
+ return null;
42
+ }
43
+ ```
44
+
45
+ ```ts
46
+ // user.hooks.ts ✅
47
+ export const useCurrentUser = () => null;
48
+ ```
49
+
50
+ Re-exporting from an index file is fine — only the file that defines the hook must follow the naming convention:
51
+
52
+ ```ts
53
+ // index.ts ✅
54
+ export { useUserData } from "./use-user-data.hook";
55
+ ```
56
+
57
+ ## What This Rule Checks
58
+
59
+ - `export function useFoo()` — named function declaration export
60
+ - `export const useFoo = () =>` — arrow function export
61
+ - `export const useFoo = function()` — function expression export
62
+ - `export default function useFoo()` — default function declaration export
63
+
64
+ A name is treated as a hook only when it starts with `use` followed by an uppercase letter (e.g. `useData`, not `user` or `use`).
65
+
66
+ ## Exceptions
67
+
68
+ Files already named `*.hook.ts` or `*.hooks.ts` are skipped entirely. Re-exports (`export { useFoo } from "..."`) are not flagged.
69
+
70
+ ## When Not To Use It
71
+
72
+ Disable per-file if you intentionally co-locate a small hook with its only consumer and do not plan to reuse it.
73
+
74
+ ## Related Rules
75
+
76
+ - [enforce-hook-naming](./ENFORCE_HOOK_NAMING.md) — enforces the `use` prefix on functions inside hook files
77
+ - [no-helper-function-in-hook](./NO_HELPER_FUNCTION_IN_HOOK.md) — disallows non-hook helpers inside hook files
@@ -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,61 @@
1
+ # enforce-test-filename
2
+
3
+ Enforce that files containing test code are named `*.test.ts` or `*.test.tsx`.
4
+
5
+ ## Rule Details
6
+
7
+ Any file that calls test runner globals (`describe`, `it`, `test`, `beforeEach`, `beforeAll`, `afterEach`, `afterAll`) must have a `.test.ts` or `.test.tsx` suffix. Files named `*.spec.ts` or any other pattern are not allowed.
8
+
9
+ ### Why?
10
+
11
+ Consistent naming makes test files predictable to find and reliable to target with `files` globs in ESLint configs (e.g. `no-helper-function-in-test`). Mixing `.spec.ts` and `.test.ts` breaks glob-based scoping.
12
+
13
+ ## Examples
14
+
15
+ ### Incorrect
16
+
17
+ ```ts
18
+ // user.spec.ts ❌
19
+ describe("user", () => {
20
+ it("works", () => {});
21
+ });
22
+ ```
23
+
24
+ ```ts
25
+ // user-tests.ts ❌
26
+ it("works", () => {});
27
+ ```
28
+
29
+ ### Correct
30
+
31
+ ```ts
32
+ // user.test.ts ✅
33
+ describe("user", () => {
34
+ it("works", () => {});
35
+ });
36
+ ```
37
+
38
+ ```ts
39
+ // UserCard.test.tsx ✅
40
+ test("renders", () => {});
41
+ ```
42
+
43
+ ## What This Rule Checks
44
+
45
+ Files that contain calls to any of the following globals trigger the requirement:
46
+
47
+ `describe`, `it`, `test`, `beforeEach`, `beforeAll`, `afterEach`, `afterAll`
48
+
49
+ Only one error is reported per file regardless of how many test calls are present.
50
+
51
+ ## Exceptions
52
+
53
+ Files already named `*.test.ts` or `*.test.tsx` are skipped entirely.
54
+
55
+ ## When Not To Use It
56
+
57
+ Disable if your project intentionally uses `.spec.ts` as the test file convention.
58
+
59
+ ## Related Rules
60
+
61
+ - [no-helper-function-in-test](./NO_HELPER_FUNCTION_IN_TEST.md) — disallows helper functions inside test files
@@ -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,86 @@
1
+ # no-helper-function-in-hook
2
+
3
+ Disallow non-hook helper function definitions in hook files.
4
+
5
+ ## Rule Details
6
+
7
+ Custom hook files should contain only the hook function itself (prefixed with `use`) and its supporting constants. Utility functions that are not hooks must be extracted to a separate file and imported.
8
+
9
+ ### Why?
10
+
11
+ Helper functions defined inside a hook file are invisible to the rest of the codebase, cannot be tested independently, and are harder to reuse. Keeping hook files focused on a single hook makes them easier to read and maintain.
12
+
13
+ ## Examples
14
+
15
+ ### Incorrect
16
+
17
+ ```ts
18
+ function buildQueryKey(id: string): string {
19
+ return `item-${id}`;
20
+ }
21
+
22
+ export function useItemData(id: string) {
23
+ const key = buildQueryKey(id);
24
+ // ...
25
+ }
26
+ ```
27
+
28
+ ```ts
29
+ const formatResponse = (data: unknown) => data;
30
+
31
+ export const useItemData = (id: string) => {
32
+ // ...
33
+ };
34
+ ```
35
+
36
+ ### Correct
37
+
38
+ ```ts
39
+ import { buildQueryKey } from "./item.utils";
40
+
41
+ export function useItemData(id: string) {
42
+ const key = buildQueryKey(id);
43
+ // ...
44
+ }
45
+ ```
46
+
47
+ Functions defined inside the hook body are fine:
48
+
49
+ ```ts
50
+ export function useItemData(id: string) {
51
+ function buildQueryKey() {
52
+ return `item-${id}`;
53
+ }
54
+ // ...
55
+ }
56
+ ```
57
+
58
+ ## What This Rule Checks
59
+
60
+ - Top-level `function` declarations whose name does not start with `use`
61
+ - Top-level `const`/`let`/`var` declarations assigned an arrow function or function expression whose name does not start with `use`
62
+
63
+ Both plain and `export`-prefixed declarations are checked.
64
+
65
+ ## Exceptions
66
+
67
+ - Hook functions starting with `use` are allowed at the top level
68
+ - Functions defined inside a hook body are not flagged
69
+ - Non-function constants (strings, numbers, arrays, objects) are not flagged
70
+
71
+ ## When Not To Use It
72
+
73
+ This rule should be scoped to hook files using the ESLint `files` glob:
74
+
75
+ ```js
76
+ {
77
+ files: ["**/*.hook.ts", "**/*.hooks.ts"],
78
+ rules: {
79
+ "nextfriday/no-helper-function-in-hook": "error",
80
+ },
81
+ }
82
+ ```
83
+
84
+ ## Related Rules
85
+
86
+ - [enforce-hook-naming](./ENFORCE_HOOK_NAMING.md) — enforces the `use` prefix on all functions in hook files