@slip-stream-kit/eslint-plugin 0.1.15 → 0.1.19

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.
Files changed (23) hide show
  1. package/dist/index.js +475 -51
  2. package/dist/index.js.map +4 -4
  3. package/dist/rules/{component-arrow-function.d.ts → component-arrow-function/component-arrow-function.d.ts} +0 -1
  4. package/dist/rules/component-arrow-function/index.d.ts +1 -0
  5. package/dist/rules/{component-file-order.d.ts → component-file-order/component-file-order.d.ts} +0 -1
  6. package/dist/rules/component-file-order/index.d.ts +1 -0
  7. package/dist/rules/max-components-per-file/index.d.ts +1 -0
  8. package/dist/rules/max-components-per-file/max-components-per-file.d.ts +2 -0
  9. package/dist/rules/max-jsx-return-size/index.d.ts +1 -0
  10. package/dist/rules/max-jsx-return-size/max-jsx-return-size.d.ts +2 -0
  11. package/dist/rules/props-destructuring-blank-line/index.d.ts +1 -0
  12. package/dist/rules/{props-destructuring-blank-line.d.ts → props-destructuring-blank-line/props-destructuring-blank-line.d.ts} +0 -1
  13. package/dist/rules/props-destructuring-newline/index.d.ts +1 -0
  14. package/dist/rules/{props-destructuring-newline.d.ts → props-destructuring-newline/props-destructuring-newline.d.ts} +0 -1
  15. package/dist/rules/props-type-name/index.d.ts +1 -0
  16. package/dist/rules/props-type-name/props-type-name.d.ts +2 -0
  17. package/dist/rules/props-type-reference/index.d.ts +1 -0
  18. package/dist/rules/{props-type-reference.d.ts → props-type-reference/props-type-reference.d.ts} +0 -1
  19. package/dist/rules/require-component-stories/index.d.ts +1 -0
  20. package/dist/rules/{require-component-stories.d.ts → require-component-stories/require-component-stories.d.ts} +0 -1
  21. package/dist/utils/component.d.ts +30 -0
  22. package/package.json +3 -3
  23. package/readme.md +220 -9
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@slip-stream-kit/eslint-plugin",
3
3
  "type": "module",
4
- "version": "0.1.15",
4
+ "version": "0.1.19",
5
5
  "description": "Custom ESLint rules enforcing the white-label frontend architecture conventions",
6
6
  "author": "Arthur Saenko <arthur.saenz7@gmail.com> (https://github.com/ArthurSaenz)",
7
7
  "license": "MIT",
@@ -52,10 +52,10 @@
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": "catalog:",
55
- "@typescript-eslint/parser": "^8.61.1",
55
+ "@typescript-eslint/parser": "^8.62.0",
56
56
  "@wl/eslint-config": "workspace:*",
57
57
  "esbuild": "^0.28.0",
58
- "eslint": "^10.5.0",
58
+ "eslint": "^10.6.0",
59
59
  "typescript": "^6.0.3",
60
60
  "vitest": "^4.1.9"
61
61
  }
package/readme.md CHANGED
@@ -11,8 +11,8 @@ pnpm add -D @wl/eslint-plugin
11
11
  ## Usage (flat config)
12
12
 
13
13
  Enable everything via the recommended preset. It is an array of config blocks
14
- (rules scoped to `*.tsx`, with `component-file-order` turned off for
15
- `*.stories.{ts,tsx}`), so spread it:
14
+ (rules scoped to `*.tsx`, with `component-file-order` and `props-type-name`
15
+ turned off for `*.stories.{ts,tsx}`), so spread it:
16
16
 
17
17
  ```js
18
18
  // eslint.config.js
@@ -136,12 +136,61 @@ matching files, `ignore` skips matching files (and takes precedence over
136
136
  }
137
137
  ```
138
138
 
139
+ ### `props-type-name`
140
+
141
+ A React component's props type must be named **`<ComponentName>Props`** (e.g.
142
+ `ButtonProps` for `Button`). This complements `props-type-reference`: that rule
143
+ requires a _named_ type (not an inline literal); this rule requires that name to
144
+ follow the convention. Report-only.
145
+
146
+ ```tsx
147
+ // ❌ Incorrect — props type does not match the component name
148
+ const Button = (props: Props) => <button>{props.label}</button>
149
+ function Card({ title }: CardConfig) {
150
+ return <div>{title}</div>
151
+ }
152
+
153
+ // ✅ Correct — `<ComponentName>Props`
154
+ const Button = (props: ButtonProps) => <button>{props.label}</button>
155
+ function Card({ title }: CardProps) {
156
+ return <div>{title}</div>
157
+ }
158
+ ```
159
+
160
+ The component is detected the same way as the other rules (PascalCase name
161
+ through `memo`/`forwardRef`/`observer` wrappers, or a JSX return). Only a simple
162
+ named type reference on the first parameter is checked: inline object types are
163
+ the `props-type-reference` rule's concern, and anonymous components, untyped
164
+ props, and qualified/generic annotations (`NS.Props`, `FC<Props>`) are left
165
+ alone. An imported props type with a non-conventional name is still flagged —
166
+ use `paths`/`ignore` to exempt it.
167
+
168
+ The recommended preset turns this rule **off for `*.stories.{ts,tsx}`**: story
169
+ templates legitimately reference the component's own props type (e.g.
170
+ `const Template = (args: ButtonProps) => ...`) rather than `<TemplateName>Props`.
171
+
172
+ #### Option: `paths` / `ignore` (optional)
173
+
174
+ Same glob semantics as `component-file-order`: `paths` restricts the rule to
175
+ matching files, `ignore` skips matching files (and takes precedence over
176
+ `paths`).
177
+
178
+ ```js
179
+ {
180
+ rules: {
181
+ '@wl/props-type-name': ['error', { ignore: ['**/*.stories.tsx'] }],
182
+ },
183
+ }
184
+ ```
185
+
139
186
  ### `component-file-order`
140
187
 
141
188
  Enforce a strict top-level order in files that contain a React component:
142
- **imports → component props interface/type → component declaration**. Constants
143
- and helpers between the interface and the component are allowed. Report-only (it
144
- does not auto-reorder code).
189
+ **imports → component props interface/type → component declaration**, with the
190
+ props interface declared **immediately before** the component no constants,
191
+ helpers, or other declarations wedged between them. Helpers are allowed _after_
192
+ the component (or between two separate component blocks). Report-only (it does
193
+ not auto-reorder code).
145
194
 
146
195
  ```tsx
147
196
  // ❌ Incorrect — interface before imports, or component before its interface
@@ -162,15 +211,18 @@ const Card = (props: CardProps) => {
162
211
  }
163
212
  ```
164
213
 
165
- The rule activates only when the file actually contains a component. The "props
166
- interface" is any top-level `interface`/`type` whose name ends in `Props`.
214
+ The rule activates only when the file actually contains a component. A
215
+ component's props interface is matched by the **type its parameter actually
216
+ references** (e.g. `Props` in `(props: Props)`), not by a name convention — so an
217
+ interface named anything is enforced, as long as the component uses it. (When the
218
+ parameter has no resolvable named type, the rule falls back to looking for a
219
+ `<ComponentName>Props` interface.)
167
220
 
168
221
  When the first component's props type is **imported** (e.g.
169
222
  `import type { CardProps } from './types'`) instead of declared in the file,
170
223
  there is no in-file interface to anchor against — so the component itself must
171
224
  sit immediately after the imports, with no stray top-level definitions wedged in
172
- between. Only the first component is anchored this way; the props binding must be
173
- the conventional `<ComponentName>Props` name for the check to apply.
225
+ between. Only the first component is anchored this way.
174
226
 
175
227
  ```tsx
176
228
  // ❌ Incorrect — props imported, but a stray const sits before the component
@@ -256,6 +308,165 @@ to add your own, e.g. an `app/` router):
256
308
  }
257
309
  ```
258
310
 
311
+ ### `max-jsx-return-size`
312
+
313
+ Warn when a single component **return** renders too many JSX elements. Large
314
+ return blocks are hard to scan; the fix is to extract part of the markup into a
315
+ variable or a sub-component. Report-only — the remedy is left to the developer
316
+ (no autofix), because safely extracting JSX touches scope, hooks, and keys.
317
+
318
+ ```tsx
319
+ // ❌ Incorrect — one return renders too many elements (default max 20)
320
+ const Dashboard = () => (
321
+ <div>
322
+ <header>…</header>
323
+ <main>… lots of nested markup …</main>
324
+ <footer>…</footer>
325
+ </div>
326
+ )
327
+
328
+ // ✅ Correct — extract parts into variables or sub-components
329
+ const Dashboard = () => {
330
+ const header = <header>…</header>
331
+ const footer = <footer>…</footer>
332
+
333
+ return (
334
+ <div>
335
+ {header}
336
+ <Main />
337
+ {footer}
338
+ </div>
339
+ )
340
+ }
341
+ ```
342
+
343
+ The metric is a **count of `JSXElement` nodes in the returned expression** —
344
+ formatting-independent (Prettier reflow never changes the verdict). Each return
345
+ in a component is measured on its own, so a small guard such as
346
+ `if (loading) return <Spinner />` is never penalised by a large sibling return.
347
+
348
+ Counting rules:
349
+
350
+ - **Extraction lowers the count.** JSX hoisted into a variable is referenced as
351
+ `{header}` (a JSX expression container, not a `JSXElement`), so it is not
352
+ counted — extracting strictly reduces the number.
353
+ - **Fragments are free.** `<>…</>` contributes `0`; its children still count.
354
+ - **Inline-callback JSX counts** in the parent return: `<ul>{items.map(() => <li />)}</ul>`
355
+ counts `<ul>` and `<li>` (extract a `<Row />` sub-component to reduce it).
356
+ - **Conditional branches are summed:** `cond ? <A /> : <B />` counts both sides.
357
+ - **JSX in attributes is counted:** `<Foo icon={<Icon />} />` counts `Foo` and `Icon`.
358
+
359
+ Only **top-level declared** components are inspected (same as
360
+ `component-arrow-function`), so anonymous inline callbacks are never reported on
361
+ their own. A top-level JSX-returning helper (e.g. `const renderRow = () => <li />`)
362
+ is treated as a component and measured. The message names the component when
363
+ resolvable and uses a generic `component` for anonymous defaults.
364
+
365
+ **Actionable message.** When one block dominates the return, the message points
366
+ at it — its tag, line, and element count — so a human (or an automated lint →
367
+ fix → lint loop) knows exactly what to lift out:
368
+
369
+ ```
370
+ Dashboard renders 28 JSX elements in one return (max 20). Extract the largest
371
+ block — <section> at line 14 (12 elements) — into a variable or a sub-component.
372
+ ```
373
+
374
+ When no single block dominates (e.g. many flat sibling elements), there is
375
+ nothing useful to point at, so the message instead advises splitting the return
376
+ into smaller sub-components.
377
+
378
+ #### Option: `maxElements` (optional)
379
+
380
+ The element ceiling before the rule reports. Defaults to `20`. Only counts
381
+ strictly greater than the ceiling are reported (`count === max` is allowed).
382
+
383
+ ```js
384
+ {
385
+ rules: {
386
+ '@wl/max-jsx-return-size': ['error', { maxElements: 25 }],
387
+ },
388
+ }
389
+ ```
390
+
391
+ #### Option: `paths` / `ignore` (optional)
392
+
393
+ Same glob semantics as the other rules: `paths` restricts the rule to matching
394
+ files, `ignore` skips matching files (and takes precedence over `paths`).
395
+
396
+ ### `max-components-per-file`
397
+
398
+ Caps how many React components a single file may declare; extra components
399
+ belong in their own files. This keeps files focused and discoverable instead of
400
+ growing into multi-component junk drawers.
401
+
402
+ ```tsx
403
+ // ❌ Incorrect — 5 components in one file (default ceiling is 4)
404
+ const A = () => <div />
405
+ const B = () => <div />
406
+ const C = () => <div />
407
+ const D = () => <div />
408
+ const E = () => <div /> // reported here: "This file declares 5 components (max 4)"
409
+
410
+ // ✅ Correct — split the extra component into its own file
411
+ ```
412
+
413
+ Only **top-level** declarations are counted. A multi-declarator statement
414
+ (`const A = () => …, B = () => …`) counts each component separately. Re-exports
415
+ (`export { X } from './x'`) declare nothing and are not counted, and
416
+ `styled.div\`…\`` tagged templates are not component functions, so they are not
417
+ counted either. Nested / in-render components are intentionally out of scope —
418
+ that is a different concern (component identity / re-render stability), better
419
+ served by `react/no-unstable-nested-components`.
420
+
421
+ Detection uses the same heuristic as the other rules (PascalCase name through
422
+ `memo`/`forwardRef`/`observer` wrappers, or a JSX return). A consequence worth
423
+ knowing: a PascalCase-named function that returns a non-JSX value (e.g. a factory
424
+ `const Make = () => ({ … })`) is counted as a component, because the name
425
+ short-circuits the check. This is consistent across the plugin.
426
+
427
+ The rule reports **once per file**, anchored to the first component over the
428
+ limit, rather than once per excess component — there is no autofix, so a single
429
+ file-scoped diagnostic is more useful than N copies of the same advice.
430
+
431
+ In the recommended preset the ceiling is `4` for `*.tsx` generally and tightened
432
+ to `1` for dumb `*-component.tsx` files (matching the one-component-per-file
433
+ convention the props/order/stories rules already assume); `**/pages/**` and
434
+ `**/routes/**` are exempt, since route/page modules legitimately co-locate
435
+ multiple route or layout components. For a file that genuinely needs to break the
436
+ ceiling, use an inline `// eslint-disable-next-line @wl/max-components-per-file`.
437
+
438
+ #### Why a custom rule (vs `react/no-multi-comp`)
439
+
440
+ `eslint-plugin-react`'s `no-multi-comp` covers similar ground but effectively
441
+ enforces a fixed ceiling of 1 (it flags the 2nd+ component) and cannot be
442
+ configured to an arbitrary limit. This rule exists because it (1) supports a
443
+ configurable `maxComponents` ceiling, (2) supports tiered per-file-type limits
444
+ via flat-config layering, and (3) reuses this plugin's centralized component
445
+ detection so its behavior matches the sibling `@wl` rules. (`eslint-plugin-react`
446
+ is not a dependency of this repo.)
447
+
448
+ #### Option: `maxComponents` (optional)
449
+
450
+ The component ceiling before the rule reports. Defaults to `4`. Only counts
451
+ strictly greater than the ceiling are reported (`count === max` is allowed).
452
+
453
+ ```js
454
+ {
455
+ rules: {
456
+ '@wl/max-components-per-file': ['error', { maxComponents: 2 }],
457
+ },
458
+ }
459
+ ```
460
+
461
+ #### Option: `paths` / `ignore` (optional)
462
+
463
+ Same glob semantics as the other rules: `paths` restricts the rule to matching
464
+ files, `ignore` skips matching files (and takes precedence over `paths`).
465
+
466
+ > **Flat-config note:** options are **replaced**, not merged, across matching
467
+ > config blocks. If you override `maxComponents` for a glob, re-declare `ignore`
468
+ > in that same block or its exemptions are lost.
469
+
259
470
  ### `require-component-stories`
260
471
 
261
472
  Require a co-located Storybook story for every dumb component. By default it enforces two layouts,