@blumintinc/eslint-plugin-blumint 1.20.4 → 1.20.5

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/lib/index.js CHANGED
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.4',
226
+ version: '1.20.5',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -1,6 +1,11 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.requirePropsComposition = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
4
9
  const utils_1 = require("@typescript-eslint/utils");
5
10
  const minimatch_1 = require("minimatch");
6
11
  const createRule_1 = require("../utils/createRule");
@@ -305,13 +310,38 @@ function getFirstParamTypeNode(funcNode) {
305
310
  }
306
311
  return null;
307
312
  }
313
+ /**
314
+ * The only wrappers that hand a component's props surface through unchanged, so
315
+ * the parameter list of the wrapped function still describes the binding's
316
+ * props. Every other call — `lazy`, `dynamic`, `styled(Box)`, `connect(...)()`,
317
+ * any `withX` — either forwards a *different* component's props or injects its
318
+ * own, so its argument proves nothing about the exported binding.
319
+ */
320
+ const PROPS_PRESERVING_HOCS = new Set(['memo', 'forwardRef', 'observer']);
321
+ /**
322
+ * Matches both the bare (`memo(...)`) and React-qualified (`React.memo(...)`)
323
+ * callee forms. A computed or deeper member expression, and a callee that is
324
+ * itself a call (`connect(mapState)(...)`, `styled(Box)(...)`), never match.
325
+ */
326
+ function isPropsPreservingHocCallee(callee) {
327
+ if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
328
+ return PROPS_PRESERVING_HOCS.has(callee.name);
329
+ }
330
+ return (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
331
+ !callee.computed &&
332
+ callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
333
+ callee.object.name === 'React' &&
334
+ callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
335
+ PROPS_PRESERVING_HOCS.has(callee.property.name));
336
+ }
308
337
  /**
309
338
  * Resolve the function node for a component name in the program, following a
310
339
  * single-identifier alias (`const Live = LiveUnmemoized`) and unwrapping a HOC
311
- * call (`memo((props) => ...)`). Returns null when no function is found. The
312
- * `seen` set guards against alias cycles.
340
+ * call (`memo((props) => ...)`). Returns null when no function is found.
313
341
  */
314
- function findComponentFunction(program, name, seen = new Set()) {
342
+ function findComponentFunction(program, name, lookup = {}) {
343
+ const seen = lookup.seen ?? new Set();
344
+ const nextLookup = { ...lookup, seen };
315
345
  if (seen.has(name))
316
346
  return null;
317
347
  seen.add(name);
@@ -338,6 +368,12 @@ function findComponentFunction(program, name, seen = new Set()) {
338
368
  return init;
339
369
  }
340
370
  if (init.type === utils_1.AST_NODE_TYPES.CallExpression) {
371
+ if (lookup.propsPreservingHocsOnly &&
372
+ !isPropsPreservingHocCallee(init.callee)) {
373
+ // The binding IS this call, and the call is not known to preserve
374
+ // props — so nothing about its props surface is provable.
375
+ return null;
376
+ }
341
377
  const arg0 = init.arguments[0];
342
378
  if (arg0 &&
343
379
  (arg0.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
@@ -346,7 +382,7 @@ function findComponentFunction(program, name, seen = new Set()) {
346
382
  }
347
383
  }
348
384
  if (init.type === utils_1.AST_NODE_TYPES.Identifier) {
349
- return findComponentFunction(program, init.name, seen);
385
+ return findComponentFunction(program, init.name, nextLookup);
350
386
  }
351
387
  }
352
388
  }
@@ -360,9 +396,406 @@ function findComponentFunction(program, name, seen = new Set()) {
360
396
  * used; imported children are left to the normal composition check.
361
397
  */
362
398
  function isZeroPropComponent(program, name) {
363
- const fn = findComponentFunction(program, name);
399
+ const fn = findComponentFunction(program, name, {
400
+ propsPreservingHocsOnly: true,
401
+ });
402
+ return fn !== null && fn.params.length === 0;
403
+ }
404
+ const RELATIVE_SOURCE = /^\.\.?\//;
405
+ const MODULE_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js'];
406
+ const DEFAULT_BINDING = 'default';
407
+ /**
408
+ * A whole-repo run re-visits the same parent/child pairs many times, so both the
409
+ * module resolution (misses included) and the per-binding verdict are memoized to
410
+ * hold the disk work to one stat/read per module. Keys are JSON-encoded tuples so
411
+ * no separator character can collide with a directory or module name. Only the
412
+ * verdicts are retained — a parsed child program is discarded as soon as it has
413
+ * answered, so a whole-repo run never accumulates foreign ASTs.
414
+ *
415
+ * The resolution cache stores the candidate search only (which path a specifier
416
+ * picks), never the file's contents or state: a resolution *miss* is safe to
417
+ * keep because a file created later stays unresolved, which reports. The verdict
418
+ * cache instead carries the resolved file's mtime/size stamp as its VALUE, so a
419
+ * child edited under a long-lived host (the VS Code ESLint extension, eslint_d)
420
+ * is re-read on the next lint rather than answering from a stale verdict. The
421
+ * stamp lives in the value, not the key, so an edited file replaces its entry
422
+ * instead of accumulating one per revision.
423
+ */
424
+ const moduleResolutionCache = new Map();
425
+ const propLessBindingCache = new Map();
426
+ /**
427
+ * Stat a candidate path, returning its identity stamp when it is a file. The
428
+ * single stat both resolves existence and stamps the file, so proving a child
429
+ * prop-less never pays for two.
430
+ */
431
+ function statModule(candidate) {
432
+ try {
433
+ const stats = fs_1.default.statSync(candidate);
434
+ return stats.isFile()
435
+ ? { filePath: candidate, mtimeMs: stats.mtimeMs, size: stats.size }
436
+ : null;
437
+ }
438
+ catch {
439
+ return null;
440
+ }
441
+ }
442
+ /**
443
+ * Resolve a relative import specifier the way a bundler would: sibling file
444
+ * first, then the directory's index file. Package specifiers never reach here —
445
+ * only a file proven to exist on disk can relax the rule.
446
+ *
447
+ * The chosen path is memoized, but the returned stamp never is: the stat that
448
+ * confirms the resolved file still exists is the same stat that dates it, so a
449
+ * cache hit costs exactly one stat (against up to eight on a miss) and a deleted
450
+ * file resolves to null again, which reports.
451
+ */
452
+ function resolveRelativeModule(fromDir, source) {
453
+ const cacheKey = JSON.stringify([fromDir, source]);
454
+ const cached = moduleResolutionCache.get(cacheKey);
455
+ if (cached !== undefined) {
456
+ return cached === null ? null : statModule(cached);
457
+ }
458
+ const base = path_1.default.resolve(fromDir, source);
459
+ let resolved = null;
460
+ for (const extension of MODULE_EXTENSIONS) {
461
+ resolved = statModule(`${base}${extension}`);
462
+ if (resolved) {
463
+ break;
464
+ }
465
+ }
466
+ if (!resolved) {
467
+ for (const extension of MODULE_EXTENSIONS) {
468
+ resolved = statModule(path_1.default.join(base, `index${extension}`));
469
+ if (resolved) {
470
+ break;
471
+ }
472
+ }
473
+ }
474
+ moduleResolutionCache.set(cacheKey, resolved?.filePath ?? null);
475
+ return resolved;
476
+ }
477
+ /**
478
+ * `undefined` means the parser has not been looked up yet; `null` means the
479
+ * lookup failed and must not be retried.
480
+ */
481
+ let foreignParse;
482
+ /**
483
+ * The child module is read with the parser that backs `@typescript-eslint/utils`
484
+ * itself, loaded lazily and memoized (failure included) so the resolution cost is
485
+ * paid once per process rather than per file. A consumer install that somehow
486
+ * lacks the parser degrades to "not provable", which leaves the dependency in the
487
+ * set and keeps the rule reporting.
488
+ */
489
+ function getForeignParse() {
490
+ if (foreignParse === undefined) {
491
+ try {
492
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
493
+ const estree = require('@typescript-eslint/typescript-estree');
494
+ foreignParse = estree?.parse ?? null;
495
+ }
496
+ catch {
497
+ foreignParse = null;
498
+ }
499
+ }
500
+ return foreignParse;
501
+ }
502
+ /**
503
+ * Parse a child module into an AST. Only a real parse can decide whether a file
504
+ * declares a zero-parameter component: text that merely looks like a declaration
505
+ * — inside a string, a template literal, a comment, a nested scope, or a
506
+ * TypeScript overload signature — must never stand in for the definition.
507
+ * A file that cannot be read or parsed yields null, i.e. proves nothing.
508
+ */
509
+ function parseModuleProgram(filePath) {
510
+ const parse = getForeignParse();
511
+ if (!parse) {
512
+ return null;
513
+ }
514
+ try {
515
+ return parse(fs_1.default.readFileSync(filePath, 'utf8'), {
516
+ jsx: true,
517
+ loc: false,
518
+ range: false,
519
+ comment: false,
520
+ });
521
+ }
522
+ catch {
523
+ return null;
524
+ }
525
+ }
526
+ /**
527
+ * Map an exported name to the local binding that defines it, covering both
528
+ * `export const X` / `export function X` and the `const X = …; export { X as Y }`
529
+ * split. A re-export (`export { X } from './y'`) is deliberately left
530
+ * unresolved: the definition lives in another module, so nothing is proven.
531
+ */
532
+ function findExportedLocalName(program, exported) {
533
+ for (const stmt of program.body) {
534
+ if (stmt.type !== utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
535
+ stmt.exportKind === 'type' ||
536
+ stmt.source) {
537
+ continue;
538
+ }
539
+ const declaration = stmt.declaration;
540
+ if (declaration) {
541
+ if (declaration.type === utils_1.AST_NODE_TYPES.FunctionDeclaration &&
542
+ declaration.id?.name === exported) {
543
+ return exported;
544
+ }
545
+ if (declaration.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
546
+ for (const declarator of declaration.declarations) {
547
+ if (declarator.id.type === utils_1.AST_NODE_TYPES.Identifier &&
548
+ declarator.id.name === exported) {
549
+ return exported;
550
+ }
551
+ }
552
+ }
553
+ continue;
554
+ }
555
+ for (const specifier of stmt.specifiers) {
556
+ if (specifier.exported.name === exported) {
557
+ return specifier.local.name;
558
+ }
559
+ }
560
+ }
561
+ return null;
562
+ }
563
+ /**
564
+ * Resolve `export default <expr>` to the component function it defines,
565
+ * unwrapping a HOC call and following an identifier alias the same way
566
+ * findComponentFunction does for named bindings. `lookup` carries the same
567
+ * restrictions, so `export default lazy(() => import('./X'))` is as unprovable
568
+ * as its named counterpart.
569
+ */
570
+ function findDefaultExportFunction(program, lookup = {}) {
571
+ for (const stmt of program.body) {
572
+ if (stmt.type !== utils_1.AST_NODE_TYPES.ExportDefaultDeclaration) {
573
+ continue;
574
+ }
575
+ const declaration = stmt.declaration;
576
+ if (declaration.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
577
+ declaration.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
578
+ declaration.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
579
+ return declaration;
580
+ }
581
+ if (declaration.type === utils_1.AST_NODE_TYPES.Identifier) {
582
+ return findComponentFunction(program, declaration.name, lookup);
583
+ }
584
+ if (declaration.type === utils_1.AST_NODE_TYPES.CallExpression) {
585
+ if (lookup.propsPreservingHocsOnly &&
586
+ !isPropsPreservingHocCallee(declaration.callee)) {
587
+ return null;
588
+ }
589
+ const arg0 = declaration.arguments[0];
590
+ if (!arg0) {
591
+ return null;
592
+ }
593
+ if (arg0.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
594
+ arg0.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
595
+ return arg0;
596
+ }
597
+ if (arg0.type === utils_1.AST_NODE_TYPES.Identifier) {
598
+ return findComponentFunction(program, arg0.name, lookup);
599
+ }
600
+ }
601
+ return null;
602
+ }
603
+ return null;
604
+ }
605
+ /**
606
+ * Whether the module at `filePath` provably exports `binding` as a component
607
+ * declared with an empty parameter list. The verdict comes from
608
+ * findComponentFunction — the same resolution the rule applies to in-file
609
+ * children — run against the child's own parsed program, so `const X = () => …`,
610
+ * `function X() {}`, props-preserving HOC wrappers and identifier aliases all
611
+ * behave identically whether the child lives in this file or a sibling one.
612
+ */
613
+ function isPropLessExport(filePath, binding) {
614
+ const program = parseModuleProgram(filePath);
615
+ if (!program) {
616
+ return false;
617
+ }
618
+ const lookup = { propsPreservingHocsOnly: true };
619
+ let fn = null;
620
+ if (binding === DEFAULT_BINDING) {
621
+ fn = findDefaultExportFunction(program, lookup);
622
+ }
623
+ else {
624
+ const local = findExportedLocalName(program, binding);
625
+ fn = local === null ? null : findComponentFunction(program, local, lookup);
626
+ }
364
627
  return fn !== null && fn.params.length === 0;
365
628
  }
629
+ /**
630
+ * Locate the relative import that introduces `localName`. Package imports
631
+ * (`@mui/material`, `react`), namespace imports, type-only imports and free
632
+ * identifiers all return null, so they keep the rule's normal behavior.
633
+ */
634
+ function findRelativeImport(program, localName) {
635
+ for (const stmt of program.body) {
636
+ if (stmt.type !== utils_1.AST_NODE_TYPES.ImportDeclaration ||
637
+ stmt.importKind === 'type' ||
638
+ typeof stmt.source.value !== 'string' ||
639
+ !RELATIVE_SOURCE.test(stmt.source.value)) {
640
+ continue;
641
+ }
642
+ for (const specifier of stmt.specifiers) {
643
+ if (specifier.local.name !== localName) {
644
+ continue;
645
+ }
646
+ if (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
647
+ return { source: stmt.source.value, binding: DEFAULT_BINDING };
648
+ }
649
+ if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
650
+ specifier.importKind !== 'type' &&
651
+ specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier) {
652
+ return { source: stmt.source.value, binding: specifier.imported.name };
653
+ }
654
+ return null;
655
+ }
656
+ }
657
+ return null;
658
+ }
659
+ /**
660
+ * Whether `name` is bound by a destructuring/assignment pattern. Every binding
661
+ * form a parameter or declarator can take is walked, so a name introduced by
662
+ * `const { X } = …` or `([X]) => …` counts as a binding just like `const X = …`.
663
+ */
664
+ function patternBindsName(pattern, name) {
665
+ if (!pattern)
666
+ return false;
667
+ switch (pattern.type) {
668
+ case utils_1.AST_NODE_TYPES.Identifier:
669
+ return pattern.name === name;
670
+ case utils_1.AST_NODE_TYPES.ObjectPattern:
671
+ return pattern.properties.some((property) => property.type === utils_1.AST_NODE_TYPES.RestElement
672
+ ? patternBindsName(property.argument, name)
673
+ : patternBindsName(property.value, name));
674
+ case utils_1.AST_NODE_TYPES.ArrayPattern:
675
+ return pattern.elements.some((element) => patternBindsName(element, name));
676
+ case utils_1.AST_NODE_TYPES.AssignmentPattern:
677
+ return patternBindsName(pattern.left, name);
678
+ case utils_1.AST_NODE_TYPES.RestElement:
679
+ return patternBindsName(pattern.argument, name);
680
+ default:
681
+ return false;
682
+ }
683
+ }
684
+ /**
685
+ * Whether a declaration of `name` appears anywhere inside `root`.
686
+ *
687
+ * A module-level import only describes the JSX name when nothing between the
688
+ * import and the JSX site re-declares it. Scope boundaries inside the component
689
+ * are deliberately ignored: over-detecting a shadow costs nothing but a report
690
+ * (the fail-safe direction), whereas missing one silently drops a dependency the
691
+ * import never described (issue #1316).
692
+ */
693
+ function isNameDeclaredWithin(root, name) {
694
+ let found = false;
695
+ function visit(node) {
696
+ if (found || !node || typeof node !== 'object')
697
+ return;
698
+ switch (node.type) {
699
+ case utils_1.AST_NODE_TYPES.VariableDeclarator:
700
+ if (patternBindsName(node.id, name))
701
+ found = true;
702
+ break;
703
+ case utils_1.AST_NODE_TYPES.FunctionDeclaration:
704
+ case utils_1.AST_NODE_TYPES.ClassDeclaration:
705
+ if (node.id?.name === name)
706
+ found = true;
707
+ break;
708
+ case utils_1.AST_NODE_TYPES.CatchClause:
709
+ if (patternBindsName(node.param, name))
710
+ found = true;
711
+ break;
712
+ default:
713
+ break;
714
+ }
715
+ if (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
716
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
717
+ node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
718
+ if (node.params.some((param) => patternBindsName(param, name))) {
719
+ found = true;
720
+ }
721
+ }
722
+ if (found)
723
+ return;
724
+ for (const key of Object.keys(node)) {
725
+ if (key === 'parent')
726
+ continue;
727
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
728
+ const child = node[key];
729
+ if (Array.isArray(child)) {
730
+ for (const item of child) {
731
+ if (item && typeof item === 'object' && 'type' in item) {
732
+ visit(item);
733
+ }
734
+ }
735
+ }
736
+ else if (child && typeof child === 'object' && 'type' in child) {
737
+ visit(child);
738
+ }
739
+ }
740
+ }
741
+ visit(root);
742
+ return found;
743
+ }
744
+ /**
745
+ * A child imported from a sibling module can be just as prop-less as an in-file
746
+ * one (issue #1316: `<BestOfText />`, whose module declares
747
+ * `export const BestOfText = () => …` and no BestOfTextProps anywhere). Demanding
748
+ * composition with a props type that cannot exist is unfixable, so such a child
749
+ * is not a composition dependency.
750
+ *
751
+ * The relaxation is deliberately narrow — it applies only when the child is
752
+ * imported from a *relative* path, that path resolves to a file on disk, that
753
+ * file's own AST positively proves a zero-parameter component, and nothing
754
+ * inside the rendering component re-declares the name. Every other dependency
755
+ * (package imports, unresolvable modules, free identifiers, shadowed names,
756
+ * anything the parse cannot decide with certainty) still reports, so the rule
757
+ * cannot be silently disabled by an unresolvable name.
758
+ */
759
+ function isPropLessImportedComponent(program, localName, filename, componentRoot) {
760
+ const relativeImport = findRelativeImport(program, localName);
761
+ if (!relativeImport) {
762
+ return false;
763
+ }
764
+ // A local declaration of the same name inside the component means the JSX
765
+ // resolves to that binding, not the import, so the import proves nothing.
766
+ if (isNameDeclaredWithin(componentRoot, localName)) {
767
+ return false;
768
+ }
769
+ try {
770
+ const absolute = path_1.default.isAbsolute(filename)
771
+ ? filename
772
+ : path_1.default.resolve(process.cwd(), filename);
773
+ const resolved = resolveRelativeModule(path_1.default.dirname(absolute), relativeImport.source);
774
+ if (!resolved) {
775
+ return false;
776
+ }
777
+ const cacheKey = JSON.stringify([
778
+ resolved.filePath,
779
+ relativeImport.binding,
780
+ ]);
781
+ const cached = propLessBindingCache.get(cacheKey);
782
+ if (cached !== undefined &&
783
+ cached.mtimeMs === resolved.mtimeMs &&
784
+ cached.size === resolved.size) {
785
+ return cached.propLess;
786
+ }
787
+ const propLess = isPropLessExport(resolved.filePath, relativeImport.binding);
788
+ propLessBindingCache.set(cacheKey, {
789
+ mtimeMs: resolved.mtimeMs,
790
+ size: resolved.size,
791
+ propLess,
792
+ });
793
+ return propLess;
794
+ }
795
+ catch {
796
+ return false;
797
+ }
798
+ }
366
799
  /**
367
800
  * Resolve the type node that defines a rendered dependency's props: its
368
801
  * `{Dep}Props` alias if one exists, otherwise the dependency component's
@@ -431,7 +864,10 @@ exports.requirePropsComposition = (0, createRule_1.createRule)({
431
864
  // an absolute path — on any platform. Normalize backslashes, then match each
432
865
  // pattern against both the full path and the repo-relative slice (from
433
866
  // `/src/`) so absolute POSIX and Windows paths both resolve (issue #1268).
434
- const filename = context.getFilename().replace(/\\/g, '/');
867
+ // Glob matching needs forward slashes, while resolving a relative import off
868
+ // disk needs the platform-native path — keep both.
869
+ const rawFilename = context.getFilename();
870
+ const filename = rawFilename.replace(/\\/g, '/');
435
871
  const matchesTargetPath = targetPaths.some((pattern) => {
436
872
  if ((0, minimatch_1.minimatch)(filename, pattern, { matchBase: false })) {
437
873
  return true;
@@ -564,9 +1000,19 @@ exports.requirePropsComposition = (0, createRule_1.createRule)({
564
1000
  return;
565
1001
  }
566
1002
  }
1003
+ // Drop dependencies proven prop-less on disk (issue #1316 reopened). This
1004
+ // runs only on the about-to-report path so a compliant file never pays for
1005
+ // the file reads, and it drops the dep from the *reported* set rather than
1006
+ // suppressing the whole report — a sibling child that genuinely needs
1007
+ // composition still fires.
1008
+ const reportableDeps = depComponents.filter((dep) => !isPropLessImportedComponent(prog, dep, rawFilename, funcNode));
1009
+ if (reportableDeps.length < minDependencyCount) {
1010
+ return;
1011
+ }
1012
+ const reportable = new Set(reportableDeps);
567
1013
  const flaggedDeps = requireAllDependencies
568
- ? missingComposition
569
- : depComponents;
1014
+ ? missingComposition.filter((dep) => reportable.has(dep))
1015
+ : reportableDeps;
570
1016
  if (flaggedDeps.length === 0)
571
1017
  return;
572
1018
  context.report({
@@ -575,7 +1021,7 @@ exports.requirePropsComposition = (0, createRule_1.createRule)({
575
1021
  data: {
576
1022
  componentName,
577
1023
  propsTypeName: propsTypeName ?? `${componentName}Props`,
578
- dependencyList: depComponents.map((d) => `'${d}'`).join(', '),
1024
+ dependencyList: reportableDeps.map((d) => `'${d}'`).join(', '),
579
1025
  missingList: flaggedDeps
580
1026
  .map((d) => `'${toPropsTypeName(d)}'`)
581
1027
  .join(', '),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.4",
3
+ "version": "1.20.5",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -54,6 +54,7 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@types/pluralize": "0.0.33",
57
+ "@typescript-eslint/typescript-estree": "5.59.6",
57
58
  "compromise": "14.14.4",
58
59
  "minimatch": "10.0.1",
59
60
  "pluralize": "8.0.0",
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "version": "1.20.5",
4
+ "date": "2026-07-28T04:46:19.315Z",
5
+ "rules": [
6
+ {
7
+ "name": "require-props-composition",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1316
11
+ ],
12
+ "summary": "prove imported zero-prop children off disk (closes #1316)"
13
+ }
14
+ ]
15
+ },
2
16
  {
3
17
  "version": "1.20.4",
4
18
  "date": "2026-07-25T03:08:45.043Z",