@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.
- package/dist/index.js +475 -51
- package/dist/index.js.map +4 -4
- package/dist/rules/{component-arrow-function.d.ts → component-arrow-function/component-arrow-function.d.ts} +0 -1
- package/dist/rules/component-arrow-function/index.d.ts +1 -0
- package/dist/rules/{component-file-order.d.ts → component-file-order/component-file-order.d.ts} +0 -1
- package/dist/rules/component-file-order/index.d.ts +1 -0
- package/dist/rules/max-components-per-file/index.d.ts +1 -0
- package/dist/rules/max-components-per-file/max-components-per-file.d.ts +2 -0
- package/dist/rules/max-jsx-return-size/index.d.ts +1 -0
- package/dist/rules/max-jsx-return-size/max-jsx-return-size.d.ts +2 -0
- package/dist/rules/props-destructuring-blank-line/index.d.ts +1 -0
- package/dist/rules/{props-destructuring-blank-line.d.ts → props-destructuring-blank-line/props-destructuring-blank-line.d.ts} +0 -1
- package/dist/rules/props-destructuring-newline/index.d.ts +1 -0
- package/dist/rules/{props-destructuring-newline.d.ts → props-destructuring-newline/props-destructuring-newline.d.ts} +0 -1
- package/dist/rules/props-type-name/index.d.ts +1 -0
- package/dist/rules/props-type-name/props-type-name.d.ts +2 -0
- package/dist/rules/props-type-reference/index.d.ts +1 -0
- package/dist/rules/{props-type-reference.d.ts → props-type-reference/props-type-reference.d.ts} +0 -1
- package/dist/rules/require-component-stories/index.d.ts +1 -0
- package/dist/rules/{require-component-stories.d.ts → require-component-stories/require-component-stories.d.ts} +0 -1
- package/dist/utils/component.d.ts +30 -0
- package/package.json +3 -3
- 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.
|
|
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.
|
|
55
|
+
"@typescript-eslint/parser": "^8.62.0",
|
|
56
56
|
"@wl/eslint-config": "workspace:*",
|
|
57
57
|
"esbuild": "^0.28.0",
|
|
58
|
-
"eslint": "^10.
|
|
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`
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
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.
|
|
166
|
-
interface
|
|
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
|
|
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,
|