@slip-stream-kit/eslint-plugin 0.1.17 → 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 +338 -30
- 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.d.ts → props-type-name/props-type-name.d.ts} +0 -1
- 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 +14 -0
- package/package.json +3 -3
- package/readme.md +159 -0
package/dist/index.js
CHANGED
|
@@ -40,18 +40,18 @@ var getComponentName = (node) => {
|
|
|
40
40
|
}
|
|
41
41
|
return null;
|
|
42
42
|
};
|
|
43
|
-
var
|
|
43
|
+
var collectOwnReturnArguments = (node) => {
|
|
44
44
|
if (node.body.type !== "BlockStatement") {
|
|
45
|
-
return
|
|
45
|
+
return [node.body];
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
const args = [];
|
|
48
48
|
const visit = (current) => {
|
|
49
|
-
if (
|
|
49
|
+
if (!current || NESTED_SCOPES.has(current.type)) {
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
if (current.type === "ReturnStatement") {
|
|
53
|
-
if (
|
|
54
|
-
|
|
53
|
+
if (current.argument) {
|
|
54
|
+
args.push(current.argument);
|
|
55
55
|
}
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
@@ -81,7 +81,10 @@ var returnsJsx = (node) => {
|
|
|
81
81
|
}
|
|
82
82
|
};
|
|
83
83
|
node.body.body.forEach(visit);
|
|
84
|
-
return
|
|
84
|
+
return args;
|
|
85
|
+
};
|
|
86
|
+
var returnsJsx = (node) => {
|
|
87
|
+
return collectOwnReturnArguments(node).some(isJsxNode);
|
|
85
88
|
};
|
|
86
89
|
var isComponent = (node) => {
|
|
87
90
|
const name = getComponentName(node);
|
|
@@ -226,7 +229,7 @@ var matchesAnyGlob = (filename, patterns) => {
|
|
|
226
229
|
});
|
|
227
230
|
};
|
|
228
231
|
|
|
229
|
-
// src/rules/component-arrow-function.ts
|
|
232
|
+
// src/rules/component-arrow-function/component-arrow-function.ts
|
|
230
233
|
var ANONYMOUS_NAME = "component";
|
|
231
234
|
var reportName = (fn) => {
|
|
232
235
|
return getComponentName(fn) ?? ANONYMOUS_NAME;
|
|
@@ -272,7 +275,7 @@ var componentArrowFunction = {
|
|
|
272
275
|
docs: {
|
|
273
276
|
description: "Enforce that React components are declared as arrow functions, not `function` declarations or function expressions.",
|
|
274
277
|
recommended: true,
|
|
275
|
-
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin"
|
|
278
|
+
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin#component-arrow-function"
|
|
276
279
|
},
|
|
277
280
|
schema: [
|
|
278
281
|
{
|
|
@@ -317,7 +320,7 @@ var componentArrowFunction = {
|
|
|
317
320
|
}
|
|
318
321
|
};
|
|
319
322
|
|
|
320
|
-
// src/rules/component-file-order.ts
|
|
323
|
+
// src/rules/component-file-order/component-file-order.ts
|
|
321
324
|
var PROPS_SUFFIX = "Props";
|
|
322
325
|
var getDirectivePrologueCount = (body) => {
|
|
323
326
|
let count = 0;
|
|
@@ -371,6 +374,7 @@ var hasStrayBefore = (body, boundary, directiveCount, skipIndex) => {
|
|
|
371
374
|
return index < boundary && index >= directiveCount && index !== skipIndex && statement.type !== "ImportDeclaration";
|
|
372
375
|
});
|
|
373
376
|
};
|
|
377
|
+
var ANONYMOUS_COMPONENT = "component";
|
|
374
378
|
var findImportOrderViolations = (importIndices, importBoundary) => {
|
|
375
379
|
return importIndices.filter((importIndex) => {
|
|
376
380
|
return importIndex > importBoundary;
|
|
@@ -393,7 +397,11 @@ var findAdjacencyViolations = (components, declIndexByName) => {
|
|
|
393
397
|
}
|
|
394
398
|
const propsIndex = declIndexByName.get(propsName);
|
|
395
399
|
if (propsIndex !== void 0 && propsIndex !== component.index - 1) {
|
|
396
|
-
violations.push({
|
|
400
|
+
violations.push({
|
|
401
|
+
index: propsIndex,
|
|
402
|
+
messageId: "interfaceImmediatelyBeforeComponent",
|
|
403
|
+
data: { interface: propsName, component: component.name ?? ANONYMOUS_COMPONENT }
|
|
404
|
+
});
|
|
397
405
|
}
|
|
398
406
|
}
|
|
399
407
|
return violations;
|
|
@@ -404,14 +412,24 @@ var findAnchorViolations = (body, directiveCount, importIndices, first, firstPro
|
|
|
404
412
|
return importIndex < firstPropsIndex;
|
|
405
413
|
});
|
|
406
414
|
if (importsBeforeInterface && hasStrayBefore(body, firstPropsIndex, directiveCount, first.index)) {
|
|
407
|
-
violations.push({
|
|
415
|
+
violations.push({
|
|
416
|
+
index: firstPropsIndex,
|
|
417
|
+
messageId: "interfaceImmediatelyAfterImports",
|
|
418
|
+
// `firstPropsIndex !== undefined` implies `firstPropsName !== null` (it is derived from it).
|
|
419
|
+
data: { interface: firstPropsName, component: first.name ?? ANONYMOUS_COMPONENT }
|
|
420
|
+
});
|
|
408
421
|
}
|
|
409
422
|
const firstPropsImported = firstPropsIndex === void 0 && firstPropsName !== null && importedNames.has(firstPropsName);
|
|
410
423
|
const importsBeforeComponent = importIndices.every((importIndex) => {
|
|
411
424
|
return importIndex < first.index;
|
|
412
425
|
});
|
|
413
426
|
if (firstPropsImported && importsBeforeComponent && hasStrayBefore(body, first.index, directiveCount)) {
|
|
414
|
-
violations.push({
|
|
427
|
+
violations.push({
|
|
428
|
+
index: first.index,
|
|
429
|
+
messageId: "componentImmediatelyAfterImports",
|
|
430
|
+
// `firstPropsImported` requires `firstPropsName !== null`.
|
|
431
|
+
data: { interface: firstPropsName, component: first.name ?? ANONYMOUS_COMPONENT }
|
|
432
|
+
});
|
|
415
433
|
}
|
|
416
434
|
return violations;
|
|
417
435
|
};
|
|
@@ -421,7 +439,7 @@ var componentFileOrder = {
|
|
|
421
439
|
docs: {
|
|
422
440
|
description: "Enforce a strict top-level order in React component files: imports first, then \u2014 for each component \u2014 its props interface/type declared immediately before the component.",
|
|
423
441
|
recommended: true,
|
|
424
|
-
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin"
|
|
442
|
+
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin#component-file-order"
|
|
425
443
|
},
|
|
426
444
|
schema: [
|
|
427
445
|
{
|
|
@@ -442,10 +460,14 @@ var componentFileOrder = {
|
|
|
442
460
|
}
|
|
443
461
|
],
|
|
444
462
|
messages: {
|
|
463
|
+
// Generic on purpose: fires on a misplaced import, and the constraint is "before *every*
|
|
464
|
+
// component", so naming a single component would mislead in a multi-component file.
|
|
445
465
|
importsFirst: "Imports must come before the component interface and declaration.",
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
466
|
+
// "props type" (not "interface"): the rule matches both `interface` and `type` alias props
|
|
467
|
+
// declarations, so the neutral term reads correctly for either.
|
|
468
|
+
interfaceImmediatelyBeforeComponent: "Declare the props type `{{interface}}` immediately before component `{{component}}` (no declarations between them).",
|
|
469
|
+
interfaceImmediatelyAfterImports: "Declare the props type `{{interface}}` (for component `{{component}}`) immediately after the imports, with no other declarations in between.",
|
|
470
|
+
componentImmediatelyAfterImports: "Props type `{{interface}}` is imported, so declare component `{{component}}` immediately after the imports, with no other declarations in between."
|
|
449
471
|
}
|
|
450
472
|
},
|
|
451
473
|
create(context) {
|
|
@@ -484,14 +506,274 @@ var componentFileOrder = {
|
|
|
484
506
|
)
|
|
485
507
|
];
|
|
486
508
|
for (const violation of violations) {
|
|
487
|
-
context.report({ node: body[violation.index], messageId: violation.messageId });
|
|
509
|
+
context.report({ node: body[violation.index], messageId: violation.messageId, data: violation.data });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// src/rules/max-components-per-file/max-components-per-file.ts
|
|
517
|
+
var DEFAULT_MAX_COMPONENTS = 4;
|
|
518
|
+
var ANONYMOUS_NAME2 = "component";
|
|
519
|
+
var componentFunctionsIn = (declaration) => {
|
|
520
|
+
if (declaration.type === "FunctionDeclaration") {
|
|
521
|
+
return isComponent(declaration) ? [declaration] : [];
|
|
522
|
+
}
|
|
523
|
+
if (declaration.type === "VariableDeclaration") {
|
|
524
|
+
return declaration.declarations.map((declarator) => {
|
|
525
|
+
return getComponentFunction(declarator.init);
|
|
526
|
+
}).filter((fn2) => {
|
|
527
|
+
return fn2 !== null && isComponent(fn2);
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
const fn = getComponentFunction(declaration);
|
|
531
|
+
return fn && isComponent(fn) ? [fn] : [];
|
|
532
|
+
};
|
|
533
|
+
var collectComponents = (body) => {
|
|
534
|
+
return body.flatMap((statement) => {
|
|
535
|
+
const declaration = unwrapExport(statement);
|
|
536
|
+
return declaration ? componentFunctionsIn(declaration) : [];
|
|
537
|
+
});
|
|
538
|
+
};
|
|
539
|
+
var maxComponentsPerFile = {
|
|
540
|
+
meta: {
|
|
541
|
+
type: "suggestion",
|
|
542
|
+
docs: {
|
|
543
|
+
description: "Limit the number of React components declared in a single file; move extra components into their own files.",
|
|
544
|
+
recommended: true,
|
|
545
|
+
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin#max-components-per-file"
|
|
546
|
+
},
|
|
547
|
+
schema: [
|
|
548
|
+
{
|
|
549
|
+
type: "object",
|
|
550
|
+
properties: {
|
|
551
|
+
maxComponents: {
|
|
552
|
+
type: "integer",
|
|
553
|
+
minimum: 1,
|
|
554
|
+
description: "Maximum number of component declarations a single file may contain before the rule reports."
|
|
555
|
+
},
|
|
556
|
+
paths: {
|
|
557
|
+
type: "array",
|
|
558
|
+
items: { type: "string" },
|
|
559
|
+
description: "Optional glob patterns. When provided, the rule only runs for files whose path matches one of them."
|
|
560
|
+
},
|
|
561
|
+
ignore: {
|
|
562
|
+
type: "array",
|
|
563
|
+
items: { type: "string" },
|
|
564
|
+
description: "Optional glob patterns. The rule is skipped for files whose path matches one of them, even if it also matches `paths`."
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
additionalProperties: false
|
|
568
|
+
}
|
|
569
|
+
],
|
|
570
|
+
messages: {
|
|
571
|
+
// File-scoped problem → reported once, anchored to the first component over
|
|
572
|
+
// the limit so a human or AI fix loop has a concrete node to move out.
|
|
573
|
+
tooManyComponents: "This file declares {{count}} components (max {{max}}); move components such as {{name}} into separate files."
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
create(context) {
|
|
577
|
+
const options = context.options[0] ?? {};
|
|
578
|
+
const max = options.maxComponents ?? DEFAULT_MAX_COMPONENTS;
|
|
579
|
+
const paths = options.paths ?? [];
|
|
580
|
+
const ignore = options.ignore ?? [];
|
|
581
|
+
if (ignore.length > 0 && matchesAnyGlob(context.filename, ignore)) {
|
|
582
|
+
return {};
|
|
583
|
+
}
|
|
584
|
+
if (paths.length > 0 && !matchesAnyGlob(context.filename, paths)) {
|
|
585
|
+
return {};
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
Program(program) {
|
|
589
|
+
const components = collectComponents(program.body);
|
|
590
|
+
if (components.length <= max) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const offender = components[max];
|
|
594
|
+
if (!offender) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const name = getComponentName(offender) ?? ANONYMOUS_NAME2;
|
|
598
|
+
context.report({
|
|
599
|
+
node: offender,
|
|
600
|
+
messageId: "tooManyComponents",
|
|
601
|
+
data: { count: components.length, max, name }
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
// src/rules/max-jsx-return-size/max-jsx-return-size.ts
|
|
609
|
+
var DEFAULT_MAX_ELEMENTS = 20;
|
|
610
|
+
var ANONYMOUS_NAME3 = "component";
|
|
611
|
+
var isNode = (value) => {
|
|
612
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
613
|
+
};
|
|
614
|
+
var childNodes = (node, visitorKeys) => {
|
|
615
|
+
const children = [];
|
|
616
|
+
const keys = visitorKeys[node.type] ?? Object.keys(node);
|
|
617
|
+
for (const key of keys) {
|
|
618
|
+
if (key === "parent") {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
const value = node[key];
|
|
622
|
+
if (Array.isArray(value)) {
|
|
623
|
+
children.push(...value.filter(isNode));
|
|
624
|
+
} else if (isNode(value)) {
|
|
625
|
+
children.push(value);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return children;
|
|
629
|
+
};
|
|
630
|
+
var countJsxElements = (node, visitorKeys) => {
|
|
631
|
+
const self = node.type === "JSXElement" ? 1 : 0;
|
|
632
|
+
return childNodes(node, visitorKeys).reduce((total, child) => {
|
|
633
|
+
return total + countJsxElements(child, visitorKeys);
|
|
634
|
+
}, self);
|
|
635
|
+
};
|
|
636
|
+
var jsxNameToString = (node) => {
|
|
637
|
+
const name = node;
|
|
638
|
+
switch (name?.type) {
|
|
639
|
+
case "JSXIdentifier":
|
|
640
|
+
return typeof name.name === "string" ? name.name : "element";
|
|
641
|
+
case "JSXMemberExpression":
|
|
642
|
+
return `${jsxNameToString(name.object)}.${jsxNameToString(name.property)}`;
|
|
643
|
+
case "JSXNamespacedName":
|
|
644
|
+
return `${jsxNameToString(name.namespace)}:${jsxNameToString(name.name)}`;
|
|
645
|
+
default:
|
|
646
|
+
return "element";
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
var topLevelElements = (root, visitorKeys) => {
|
|
650
|
+
const elements = [];
|
|
651
|
+
const walk = (node, isRoot) => {
|
|
652
|
+
if (!isRoot && node.type === "JSXElement") {
|
|
653
|
+
elements.push(node);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
for (const child of childNodes(node, visitorKeys)) {
|
|
657
|
+
walk(child, false);
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
walk(root, true);
|
|
661
|
+
return elements;
|
|
662
|
+
};
|
|
663
|
+
var largestBlock = (root, visitorKeys) => {
|
|
664
|
+
let best = null;
|
|
665
|
+
for (const element2 of topLevelElements(root, visitorKeys)) {
|
|
666
|
+
const count = countJsxElements(element2, visitorKeys);
|
|
667
|
+
if (!best || count > best.count) {
|
|
668
|
+
best = { node: element2, count };
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (!best) {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
const element = best.node;
|
|
675
|
+
return { name: jsxNameToString(element.openingElement?.name), line: element.loc?.start.line ?? 0, count: best.count };
|
|
676
|
+
};
|
|
677
|
+
var componentFunctionsIn2 = (declaration) => {
|
|
678
|
+
if (declaration.type === "FunctionDeclaration") {
|
|
679
|
+
return isComponent(declaration) ? [declaration] : [];
|
|
680
|
+
}
|
|
681
|
+
if (declaration.type === "VariableDeclaration") {
|
|
682
|
+
return declaration.declarations.map((declarator) => {
|
|
683
|
+
return getComponentFunction(declarator.init);
|
|
684
|
+
}).filter((fn2) => {
|
|
685
|
+
return fn2 !== null && isComponent(fn2);
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
const fn = getComponentFunction(declaration);
|
|
689
|
+
return fn && isComponent(fn) ? [fn] : [];
|
|
690
|
+
};
|
|
691
|
+
var collectComponents2 = (body) => {
|
|
692
|
+
return body.flatMap((statement) => {
|
|
693
|
+
const declaration = unwrapExport(statement);
|
|
694
|
+
return declaration ? componentFunctionsIn2(declaration) : [];
|
|
695
|
+
});
|
|
696
|
+
};
|
|
697
|
+
var maxJsxReturnSize = {
|
|
698
|
+
meta: {
|
|
699
|
+
type: "suggestion",
|
|
700
|
+
docs: {
|
|
701
|
+
description: "Warn when a component return renders too many JSX elements; extract parts into variables or sub-components.",
|
|
702
|
+
recommended: true,
|
|
703
|
+
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin#max-jsx-return-size"
|
|
704
|
+
},
|
|
705
|
+
schema: [
|
|
706
|
+
{
|
|
707
|
+
type: "object",
|
|
708
|
+
properties: {
|
|
709
|
+
maxElements: {
|
|
710
|
+
type: "integer",
|
|
711
|
+
minimum: 1,
|
|
712
|
+
description: "Maximum number of JSX elements a single return may render before the rule reports."
|
|
713
|
+
},
|
|
714
|
+
paths: {
|
|
715
|
+
type: "array",
|
|
716
|
+
items: { type: "string" },
|
|
717
|
+
description: "Optional glob patterns. When provided, the rule only runs for files whose path matches one of them."
|
|
718
|
+
},
|
|
719
|
+
ignore: {
|
|
720
|
+
type: "array",
|
|
721
|
+
items: { type: "string" },
|
|
722
|
+
description: "Optional glob patterns. The rule is skipped for files whose path matches one of them, even if it also matches `paths`."
|
|
723
|
+
}
|
|
724
|
+
},
|
|
725
|
+
additionalProperties: false
|
|
726
|
+
}
|
|
727
|
+
],
|
|
728
|
+
messages: {
|
|
729
|
+
// Points at the single biggest block so a human or AI fix loop knows
|
|
730
|
+
// exactly what to lift out.
|
|
731
|
+
tooManyElements: "{{name}} renders {{count}} JSX elements in one return (max {{max}}). Extract the largest block \u2014 <{{largest}}> at line {{line}} ({{largestCount}} elements) \u2014 into a variable or a sub-component.",
|
|
732
|
+
// Fallback when no single block dominates (e.g. many flat siblings): there is
|
|
733
|
+
// nothing meaningful to point at, so advise splitting.
|
|
734
|
+
tooManyElementsFlat: "{{name}} renders {{count}} JSX elements in one return (max {{max}}). Split it into smaller sub-components or extract groups of elements into variables."
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
create(context) {
|
|
738
|
+
const options = context.options[0] ?? {};
|
|
739
|
+
const max = options.maxElements ?? DEFAULT_MAX_ELEMENTS;
|
|
740
|
+
const paths = options.paths ?? [];
|
|
741
|
+
const ignore = options.ignore ?? [];
|
|
742
|
+
if (ignore.length > 0 && matchesAnyGlob(context.filename, ignore)) {
|
|
743
|
+
return {};
|
|
744
|
+
}
|
|
745
|
+
if (paths.length > 0 && !matchesAnyGlob(context.filename, paths)) {
|
|
746
|
+
return {};
|
|
747
|
+
}
|
|
748
|
+
const visitorKeys = context.sourceCode.visitorKeys;
|
|
749
|
+
const checkComponent = (fn) => {
|
|
750
|
+
const name = getComponentName(fn) ?? ANONYMOUS_NAME3;
|
|
751
|
+
for (const argument of collectOwnReturnArguments(fn)) {
|
|
752
|
+
const count = countJsxElements(argument, visitorKeys);
|
|
753
|
+
if (count <= max) {
|
|
754
|
+
continue;
|
|
488
755
|
}
|
|
756
|
+
const largest = largestBlock(argument, visitorKeys);
|
|
757
|
+
if (largest && largest.count >= 2) {
|
|
758
|
+
context.report({
|
|
759
|
+
node: argument,
|
|
760
|
+
messageId: "tooManyElements",
|
|
761
|
+
data: { count, max, name, largest: largest.name, line: largest.line, largestCount: largest.count }
|
|
762
|
+
});
|
|
763
|
+
} else {
|
|
764
|
+
context.report({ node: argument, messageId: "tooManyElementsFlat", data: { count, max, name } });
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
return {
|
|
769
|
+
Program(program) {
|
|
770
|
+
collectComponents2(program.body).forEach(checkComponent);
|
|
489
771
|
}
|
|
490
772
|
};
|
|
491
773
|
}
|
|
492
774
|
};
|
|
493
775
|
|
|
494
|
-
// src/rules/props-destructuring-blank-line.ts
|
|
776
|
+
// src/rules/props-destructuring-blank-line/props-destructuring-blank-line.ts
|
|
495
777
|
var isPropsDestructuring = (statement) => {
|
|
496
778
|
if (statement.type !== "VariableDeclaration") {
|
|
497
779
|
return false;
|
|
@@ -506,7 +788,7 @@ var propsDestructuringBlankLine = {
|
|
|
506
788
|
docs: {
|
|
507
789
|
description: "Require a blank line after the `const { ... } = props` destructuring statement at the top of a React component body.",
|
|
508
790
|
recommended: true,
|
|
509
|
-
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin"
|
|
791
|
+
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin#props-destructuring-blank-line"
|
|
510
792
|
},
|
|
511
793
|
fixable: "whitespace",
|
|
512
794
|
schema: [],
|
|
@@ -554,7 +836,7 @@ var propsDestructuringBlankLine = {
|
|
|
554
836
|
}
|
|
555
837
|
};
|
|
556
838
|
|
|
557
|
-
// src/rules/props-destructuring-newline.ts
|
|
839
|
+
// src/rules/props-destructuring-newline/props-destructuring-newline.ts
|
|
558
840
|
var collectBoundNames = (node, names) => {
|
|
559
841
|
if (!node) {
|
|
560
842
|
return;
|
|
@@ -588,7 +870,7 @@ var propsDestructuringNewline = {
|
|
|
588
870
|
docs: {
|
|
589
871
|
description: "Require React components to accept a single props parameter and destructure it on its own line in the body, rather than destructuring inline in the parameter list.",
|
|
590
872
|
recommended: true,
|
|
591
|
-
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin"
|
|
873
|
+
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin#props-destructuring-newline"
|
|
592
874
|
},
|
|
593
875
|
fixable: "code",
|
|
594
876
|
schema: [],
|
|
@@ -665,7 +947,7 @@ ${baseIndent}}`
|
|
|
665
947
|
}
|
|
666
948
|
};
|
|
667
949
|
|
|
668
|
-
// src/rules/props-type-name.ts
|
|
950
|
+
// src/rules/props-type-name/props-type-name.ts
|
|
669
951
|
var PROPS_SUFFIX2 = "Props";
|
|
670
952
|
var propsTypeName = {
|
|
671
953
|
meta: {
|
|
@@ -673,7 +955,7 @@ var propsTypeName = {
|
|
|
673
955
|
docs: {
|
|
674
956
|
description: "Require a React component's props type to be named `<ComponentName>Props` (e.g. `ButtonProps` for `Button`).",
|
|
675
957
|
recommended: true,
|
|
676
|
-
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin"
|
|
958
|
+
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin#props-type-name"
|
|
677
959
|
},
|
|
678
960
|
schema: [
|
|
679
961
|
{
|
|
@@ -737,7 +1019,7 @@ var propsTypeName = {
|
|
|
737
1019
|
}
|
|
738
1020
|
};
|
|
739
1021
|
|
|
740
|
-
// src/rules/props-type-reference.ts
|
|
1022
|
+
// src/rules/props-type-reference/props-type-reference.ts
|
|
741
1023
|
var INLINE_OBJECT_TYPE = "TSTypeLiteral";
|
|
742
1024
|
var propsTypeReference = {
|
|
743
1025
|
meta: {
|
|
@@ -745,7 +1027,7 @@ var propsTypeReference = {
|
|
|
745
1027
|
docs: {
|
|
746
1028
|
description: "Require a React component's props parameter to use a named type (e.g. `ButtonProps`) instead of an inline object type literal.",
|
|
747
1029
|
recommended: true,
|
|
748
|
-
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin"
|
|
1030
|
+
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin#props-type-reference"
|
|
749
1031
|
},
|
|
750
1032
|
schema: [
|
|
751
1033
|
{
|
|
@@ -803,7 +1085,7 @@ var propsTypeReference = {
|
|
|
803
1085
|
}
|
|
804
1086
|
};
|
|
805
1087
|
|
|
806
|
-
// src/rules/require-component-stories.ts
|
|
1088
|
+
// src/rules/require-component-stories/require-component-stories.ts
|
|
807
1089
|
import { existsSync } from "node:fs";
|
|
808
1090
|
import path2 from "node:path";
|
|
809
1091
|
|
|
@@ -879,7 +1161,7 @@ var deriveExpectedStoryPaths = (filePath, opts) => {
|
|
|
879
1161
|
});
|
|
880
1162
|
};
|
|
881
1163
|
|
|
882
|
-
// src/rules/require-component-stories.ts
|
|
1164
|
+
// src/rules/require-component-stories/require-component-stories.ts
|
|
883
1165
|
var toStoryPathOptions = (options) => {
|
|
884
1166
|
const picked = {};
|
|
885
1167
|
if (options.storiesDir !== void 0) {
|
|
@@ -905,7 +1187,7 @@ var requireComponentStories = {
|
|
|
905
1187
|
docs: {
|
|
906
1188
|
description: "Require a co-located Storybook story for every dumb component.",
|
|
907
1189
|
recommended: true,
|
|
908
|
-
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin"
|
|
1190
|
+
url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin#require-component-stories"
|
|
909
1191
|
},
|
|
910
1192
|
schema: [
|
|
911
1193
|
{
|
|
@@ -1012,6 +1294,8 @@ var rules = {
|
|
|
1012
1294
|
"props-type-name": propsTypeName,
|
|
1013
1295
|
"component-file-order": componentFileOrder,
|
|
1014
1296
|
"component-arrow-function": componentArrowFunction,
|
|
1297
|
+
"max-components-per-file": maxComponentsPerFile,
|
|
1298
|
+
"max-jsx-return-size": maxJsxReturnSize,
|
|
1015
1299
|
"require-component-stories": requireComponentStories
|
|
1016
1300
|
};
|
|
1017
1301
|
|
|
@@ -1040,7 +1324,16 @@ plugin.configs.recommended = [
|
|
|
1040
1324
|
// Pages and routes are excluded: route/page modules commonly use `function`
|
|
1041
1325
|
// declarations (and framework conventions like default-exported page functions).
|
|
1042
1326
|
[`${PLUGIN_NAME}/component-arrow-function`]: ["error", { ignore: ["**/pages/**", "**/routes/**"] }],
|
|
1043
|
-
[`${PLUGIN_NAME}/require-component-stories`]: "error"
|
|
1327
|
+
[`${PLUGIN_NAME}/require-component-stories`]: "error",
|
|
1328
|
+
// Advisory: flags returns that render too many JSX elements; extract into a
|
|
1329
|
+
// variable or sub-component. Warn-class by nature (`type: 'suggestion'`);
|
|
1330
|
+
// severity confirmed against a repo-wide dry run at the default ceiling.
|
|
1331
|
+
[`${PLUGIN_NAME}/max-jsx-return-size`]: "error",
|
|
1332
|
+
// Caps component declarations per file; extra components belong in their own
|
|
1333
|
+
// files. Pages and routes are excluded (framework conventions co-locate
|
|
1334
|
+
// route trees and default-exported page functions). Ceiling confirmed
|
|
1335
|
+
// against a repo-wide dry run at the default.
|
|
1336
|
+
[`${PLUGIN_NAME}/max-components-per-file`]: ["error", { ignore: ["**/pages/**", "**/routes/**"] }]
|
|
1044
1337
|
}
|
|
1045
1338
|
},
|
|
1046
1339
|
// Storybook stories legitimately deviate from the component conventions: the
|
|
@@ -1055,6 +1348,21 @@ plugin.configs.recommended = [
|
|
|
1055
1348
|
[`${PLUGIN_NAME}/component-file-order`]: "off",
|
|
1056
1349
|
[`${PLUGIN_NAME}/props-type-name`]: "off"
|
|
1057
1350
|
}
|
|
1351
|
+
},
|
|
1352
|
+
// Dumb presentational files (`*-component.tsx`) follow the one-component-per-file
|
|
1353
|
+
// convention the props/order/stories rules already assume, so they get a tighter
|
|
1354
|
+
// ceiling of 1. This MUST come AFTER the global `**/*.tsx` block: flat config
|
|
1355
|
+
// REPLACES rule options across matching blocks (it does not merge), and `ignore`
|
|
1356
|
+
// is re-declared here so pages/routes dumb-components keep their exemption.
|
|
1357
|
+
// A repo-wide dry run found zero `*-component.tsx` files declaring >1 component.
|
|
1358
|
+
{
|
|
1359
|
+
files: ["**/*-component.tsx"],
|
|
1360
|
+
rules: {
|
|
1361
|
+
[`${PLUGIN_NAME}/max-components-per-file`]: [
|
|
1362
|
+
"error",
|
|
1363
|
+
{ maxComponents: 1, ignore: ["**/pages/**", "**/routes/**"] }
|
|
1364
|
+
]
|
|
1365
|
+
}
|
|
1058
1366
|
}
|
|
1059
1367
|
];
|
|
1060
1368
|
var meta = plugin.meta;
|