@slip-stream-kit/eslint-plugin 0.1.17 → 0.1.20

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 (26) hide show
  1. package/dist/index.js +634 -32
  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.d.ts → props-type-name/props-type-name.d.ts} +0 -1
  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/rules/require-jsdoc-example/index.d.ts +1 -0
  22. package/dist/rules/require-jsdoc-example/require-jsdoc-example.d.ts +2 -0
  23. package/dist/utils/cognitive-complexity.d.ts +14 -0
  24. package/dist/utils/component.d.ts +14 -0
  25. package/package.json +3 -3
  26. package/readme.md +159 -0
@@ -1,3 +1,2 @@
1
1
  import type { Rule } from 'eslint';
2
2
  export declare const propsTypeName: Rule.RuleModule;
3
- export default propsTypeName;
@@ -0,0 +1 @@
1
+ export { propsTypeReference } from './props-type-reference';
@@ -1,3 +1,2 @@
1
1
  import type { Rule } from 'eslint';
2
2
  export declare const propsTypeReference: Rule.RuleModule;
3
- export default propsTypeReference;
@@ -0,0 +1 @@
1
+ export { requireComponentStories } from './require-component-stories';
@@ -1,3 +1,2 @@
1
1
  import type { Rule } from 'eslint';
2
2
  export declare const requireComponentStories: Rule.RuleModule;
3
- export default requireComponentStories;
@@ -0,0 +1 @@
1
+ export { requireJsdocExample } from './require-jsdoc-example';
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const requireJsdocExample: Rule.RuleModule;
@@ -0,0 +1,14 @@
1
+ import type * as ESTree from 'estree';
2
+ export type FunctionNode = ESTree.ArrowFunctionExpression | ESTree.FunctionDeclaration | ESTree.FunctionExpression;
3
+ /**
4
+ * Cognitive complexity of a function per the SonarSource white-paper model.
5
+ *
6
+ * Each `if`/ternary/switch/loop/`catch` adds 1 plus the current nesting level;
7
+ * `else`/`else if` adds 1 with no nesting bump; each run of a logical operator
8
+ * adds 1; recursion (a direct call to the enclosing function by name) adds 1.
9
+ * Nesting deepens inside every branch, loop, switch, `catch`, and nested function.
10
+ *
11
+ * @example
12
+ * cognitiveComplexity(node) // 0 for a flat function, 1 for a single `if`
13
+ */
14
+ export declare const cognitiveComplexity: (fn: FunctionNode, enclosingName?: string | null) => number;
@@ -1,11 +1,25 @@
1
1
  import type * as ESTree from 'estree';
2
2
  export type ComponentFunction = ESTree.ArrowFunctionExpression | ESTree.FunctionDeclaration | ESTree.FunctionExpression;
3
+ export declare const isJsxNode: (node: ESTree.Node | null | undefined) => boolean;
3
4
  /**
4
5
  * Resolve the declared name of a function, looking through component wrappers
5
6
  * such as `memo`/`forwardRef` so that `const Comp = memo(({ a }) => ...)` is
6
7
  * still recognised by its PascalCase variable name.
7
8
  */
8
9
  export declare const getComponentName: (node: ComponentFunction) => string | null;
10
+ /**
11
+ * Collect every *own* return argument of a function — the expression of each
12
+ * `return <expr>` reachable without entering a nested function scope, plus the
13
+ * implicit-return body of an expression-bodied arrow.
14
+ *
15
+ * Bare `return;` (a null argument) contributes nothing, so callers never receive
16
+ * a null and can safely walk each result. This is the multi-return counterpart
17
+ * to the boolean `returnsJsx`, which is defined in terms of it; the deliberate
18
+ * "skip nested scopes" behaviour answers *which* returns belong to this function
19
+ * (a `return` inside an inline `.map`/IIFE callback is that callback's return,
20
+ * not this one's).
21
+ */
22
+ export declare const collectOwnReturnArguments: (node: ComponentFunction) => ESTree.Expression[];
9
23
  /** A function is treated as a React component when it is PascalCase-named or returns JSX. */
10
24
  export declare const isComponent: (node: ComponentFunction) => boolean;
11
25
  /**
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.17",
4
+ "version": "0.1.20",
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
@@ -308,6 +308,165 @@ to add your own, e.g. an `app/` router):
308
308
  }
309
309
  ```
310
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
+
311
470
  ### `require-component-stories`
312
471
 
313
472
  Require a co-located Storybook story for every dumb component. By default it enforces two layouts,