@blumintinc/eslint-plugin-blumint 1.20.130 → 1.20.131

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.130',
226
+ version: '1.20.131',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -22,5 +22,23 @@ type Options = [
22
22
  */
23
23
  export declare const DEFAULT_IGNORED_LIBRARIES: string[];
24
24
  export declare const DEFAULT_INTERNAL_PREFIXES: string[];
25
+ /**
26
+ * Builds an O(1) + glob matcher from a list of library patterns.
27
+ *
28
+ * With `coverSubpaths`, a non-glob entry stands for the package *and*
29
+ * everything published under it. A package's subpath entry point is the same
30
+ * dependency as its root — `fast-deep-equal/es6` is upstream's documented ESM
31
+ * build, and the spelling `fast-deep-equal-over-microdiff` steers code toward —
32
+ * so exempting the root while enforcing the subpath left the pair of rules
33
+ * jointly unsatisfiable (#1845).
34
+ *
35
+ * The boundary is `entry + '/'`, never a bare substring: `fast-deep-equal-extra`
36
+ * is a different package on the registry and stays enforced. Glob entries keep
37
+ * their minimatch semantics untouched, since a pattern already says how far it
38
+ * reaches.
39
+ */
40
+ export declare const buildLibraryMatcher: (list: string[], { coverSubpaths }: {
41
+ coverSubpaths: boolean;
42
+ }) => (source: string) => boolean;
25
43
  declare const _default: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"dynamicImportRequired", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
26
44
  export default _default;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_INTERNAL_PREFIXES = exports.DEFAULT_IGNORED_LIBRARIES = exports.RULE_NAME = void 0;
3
+ exports.buildLibraryMatcher = exports.DEFAULT_INTERNAL_PREFIXES = exports.DEFAULT_IGNORED_LIBRARIES = exports.RULE_NAME = void 0;
4
4
  const createRule_1 = require("../utils/createRule");
5
5
  const minimatch_1 = require("minimatch");
6
6
  const module_1 = require("module");
@@ -43,6 +43,42 @@ exports.DEFAULT_IGNORED_LIBRARIES = [
43
43
  'fast-deep-equal', // fast-deep-equal-over-microdiff, for files already on upstream
44
44
  ];
45
45
  exports.DEFAULT_INTERNAL_PREFIXES = ['src/', 'functions/'];
46
+ /**
47
+ * Builds an O(1) + glob matcher from a list of library patterns.
48
+ *
49
+ * With `coverSubpaths`, a non-glob entry stands for the package *and*
50
+ * everything published under it. A package's subpath entry point is the same
51
+ * dependency as its root — `fast-deep-equal/es6` is upstream's documented ESM
52
+ * build, and the spelling `fast-deep-equal-over-microdiff` steers code toward —
53
+ * so exempting the root while enforcing the subpath left the pair of rules
54
+ * jointly unsatisfiable (#1845).
55
+ *
56
+ * The boundary is `entry + '/'`, never a bare substring: `fast-deep-equal-extra`
57
+ * is a different package on the registry and stays enforced. Glob entries keep
58
+ * their minimatch semantics untouched, since a pattern already says how far it
59
+ * reaches.
60
+ */
61
+ const buildLibraryMatcher = (list, { coverSubpaths }) => {
62
+ const exactSet = new Set();
63
+ const subpathPrefixes = [];
64
+ const globs = [];
65
+ for (const lib of list) {
66
+ const mm = new minimatch_1.Minimatch(lib);
67
+ if (mm.hasMagic()) {
68
+ globs.push(mm);
69
+ }
70
+ else {
71
+ exactSet.add(lib);
72
+ if (coverSubpaths) {
73
+ subpathPrefixes.push(lib.endsWith('/') ? lib : `${lib}/`);
74
+ }
75
+ }
76
+ }
77
+ return (source) => exactSet.has(source) ||
78
+ subpathPrefixes.some((prefix) => source.startsWith(prefix)) ||
79
+ globs.some((mm) => mm.match(source));
80
+ };
81
+ exports.buildLibraryMatcher = buildLibraryMatcher;
46
82
  // Pre-built set of Node.js core module names for O(1) lookup.
47
83
  const NODE_BUILTINS = new Set(module_1.builtinModules);
48
84
  // Returns true for any source that resolves to a Node builtin: bare name
@@ -106,25 +142,22 @@ exports.default = (0, createRule_1.createRule)({
106
142
  // When `libraries` is absent, enforce-by-default mode applies:
107
143
  // all external imports are flagged unless in `ignoredLibraries`.
108
144
  const isWhitelistMode = libraries !== undefined;
109
- // Build an O(1) + glob matcher from a list of library patterns.
110
- const buildMatcher = (list) => {
111
- const exactSet = new Set();
112
- const globs = [];
113
- for (const lib of list) {
114
- const mm = new minimatch_1.Minimatch(lib);
115
- if (mm.hasMagic()) {
116
- globs.push(mm);
117
- }
118
- else {
119
- exactSet.add(lib);
120
- }
121
- }
122
- return (source) => exactSet.has(source) || globs.some((mm) => mm.match(source));
123
- };
124
145
  // In whitelist mode, `libraries` is defined (checked above). In
125
146
  // enforce-by-default mode, `ignoredLibraries` is used instead.
126
- const isListedInWhitelist = buildMatcher(libraries ?? []);
127
- const isIgnoredLibrary = buildMatcher(ignoredLibraries);
147
+ //
148
+ // Subpath covering is asymmetric between the two lists because the lists
149
+ // point in opposite directions. Widening `ignoredLibraries` only ever
150
+ // REMOVES reports, so it can safely absorb a package's subpath entry
151
+ // points. Widening `libraries` would ADD reports — a consumer who listed
152
+ // `pkg` to restore pre-1.16.0 behaviour would start failing on `pkg/sub` —
153
+ // so the whitelist keeps exact + glob semantics, and consumers who do want
154
+ // the subpaths enforced spell that as a glob (`pkg/**`), which still works.
155
+ const isListedInWhitelist = (0, exports.buildLibraryMatcher)(libraries ?? [], {
156
+ coverSubpaths: false,
157
+ });
158
+ const isIgnoredLibrary = (0, exports.buildLibraryMatcher)(ignoredLibraries, {
159
+ coverSubpaths: true,
160
+ });
128
161
  // A source is external only if it looks like an npm package specifier AND
129
162
  // is not a known-internal path. Node builtins and configured internal
130
163
  // prefixes (e.g. src/, functions/) are excluded to avoid false positives
@@ -8,6 +8,189 @@ const renameFixes_1 = require("../utils/renameFixes");
8
8
  const LOWERCASE_TYPES = ['ReactNode', 'JSX.Element'];
9
9
  // Types that should have uppercase variable names
10
10
  const UPPERCASE_TYPES = ['ComponentType', 'FC', 'FunctionComponent'];
11
+ /**
12
+ * `global-const-style` owns the NAME of a module-scope `const`, and the two
13
+ * rules cannot both be satisfied there: it demands UPPER_SNAKE_CASE, this rule
14
+ * demands a lowercase initial for `ReactNode`/`JSX.Element`. Every spelling
15
+ * reports under one or the other, so a consumer running both — they are both
16
+ * `'error'` in `recommended` — cannot write the line at all.
17
+ *
18
+ * Unexported, both renamers also autofix, so `--fix` oscillates (`element` ->
19
+ * `ELEMENT` -> `eLEMENT` -> `E_LEMENT` -> `e_LEMENT` -> …) until ESLint's
20
+ * ten-pass cap and writes the mangled identifier to disk (Issue #1846).
21
+ * EXPORTED, both withhold the rename — an exported name is a cross-file
22
+ * contract a single-file fixer cannot complete — so `--fix` is a no-op and the
23
+ * damage is only the unsatisfiable report pair (Issue #1847).
24
+ *
25
+ * The pair is resolved by this rule yielding: module-scope constant naming is
26
+ * `global-const-style`'s universal contract, while this rule's purpose —
27
+ * telling an element VALUE apart from a COMPONENT — is about local and
28
+ * parameter naming, where nothing competes with it. Do not re-open the
29
+ * carve-out without changing `global-const-style` in the same breath.
30
+ *
31
+ * GOVERNANCE FOLLOWS WHICH RULE REPORTS ON THE NAME, NOT WHICH ONE FIXES IT.
32
+ * #1846 drew the boundary at the fixer war and so excluded exports; that left
33
+ * the exported form unsatisfiable, because `global-const-style` withholds only
34
+ * its FIX there (#1700) and still emits `upperSnakeCase`. The predicate below
35
+ * therefore mirrors that rule's ACTUAL reporting gates, which is also why it
36
+ * cannot be simplified to "module-scope const" — it declines on several shapes,
37
+ * and yielding on one of those would leave the declaration governed by nothing:
38
+ *
39
+ * - `let`/`var`, and any non-module scope, are outside it entirely;
40
+ * - a declaration whose parent is neither `Program` nor an
41
+ * `ExportNamedDeclaration` (a block, a `for` head, a namespace body) never
42
+ * reaches its check;
43
+ * - an exported Next.js reserved name (`config`, `getStaticProps`, …) has its
44
+ * rename declined outright (#1257), so this rule keeps its report there —
45
+ * report-only, since its own fixer stands down for exports too;
46
+ * - a function value or a `memo`/`forwardRef` call makes it skip the whole
47
+ * declaration list — `const button: FC = () => …` is this rule's alone;
48
+ * - an absent initializer, a dynamic value, a binding alias and a
49
+ * `jest.Mock*` cast each silence its rename check.
50
+ *
51
+ * When any of that cannot be established the answer is `false`: keeping a
52
+ * report is recoverable, silently governing nothing is not.
53
+ */
54
+ const VALUE_WRAPPER_TYPES = new Set([
55
+ utils_1.AST_NODE_TYPES.TSAsExpression,
56
+ utils_1.AST_NODE_TYPES.TSTypeAssertion,
57
+ utils_1.AST_NODE_TYPES.TSSatisfiesExpression,
58
+ utils_1.AST_NODE_TYPES.TSNonNullExpression,
59
+ ]);
60
+ const isValueWrapper = (node) => VALUE_WRAPPER_TYPES.has(node.type);
61
+ const unwrapValueWrappers = (node) => {
62
+ let target = node;
63
+ while (isValueWrapper(target)) {
64
+ target = target.expression;
65
+ }
66
+ return target;
67
+ };
68
+ // `global-const-style` unwraps only `as`/`<T>` casts before classifying an
69
+ // initializer as dynamic or as a binding alias, so the mirror does the same.
70
+ const unwrapCasts = (node) => {
71
+ let target = node;
72
+ while (target.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
73
+ target.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
74
+ target = target.expression;
75
+ }
76
+ return target;
77
+ };
78
+ const COMPONENT_FACTORY_NAMES = new Set(['forwardRef', 'memo']);
79
+ const isComponentFactoryCall = (node) => {
80
+ if (node.type !== utils_1.AST_NODE_TYPES.CallExpression) {
81
+ return false;
82
+ }
83
+ const { callee } = node;
84
+ if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
85
+ return COMPONENT_FACTORY_NAMES.has(callee.name);
86
+ }
87
+ return (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
88
+ !callee.computed &&
89
+ callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
90
+ COMPONENT_FACTORY_NAMES.has(callee.property.name));
91
+ };
92
+ const isFunctionValue = (node) => node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
93
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression;
94
+ const isDynamicValue = (node) => {
95
+ const target = unwrapCasts(node);
96
+ if (target.type === utils_1.AST_NODE_TYPES.CallExpression ||
97
+ target.type === utils_1.AST_NODE_TYPES.NewExpression ||
98
+ target.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
99
+ return true;
100
+ }
101
+ if (target.type === utils_1.AST_NODE_TYPES.ChainExpression) {
102
+ return isDynamicValue(target.expression);
103
+ }
104
+ if (target.type === utils_1.AST_NODE_TYPES.MemberExpression) {
105
+ return isDynamicValue(target.object);
106
+ }
107
+ return false;
108
+ };
109
+ const PRIMITIVE_VALUE_GLOBALS = new Set(['undefined', 'NaN', 'Infinity']);
110
+ const isBindingAlias = (node) => {
111
+ const target = unwrapCasts(node);
112
+ return (target.type === utils_1.AST_NODE_TYPES.Identifier &&
113
+ !PRIMITIVE_VALUE_GLOBALS.has(target.name));
114
+ };
115
+ const JEST_MOCK_TYPE_NAMES = new Set([
116
+ 'Mock',
117
+ 'MockedFunction',
118
+ 'Mocked',
119
+ 'MockedClass',
120
+ ]);
121
+ const isJestMockTypeReference = (typeAnnotation) => {
122
+ if (typeAnnotation.type !== utils_1.AST_NODE_TYPES.TSTypeReference) {
123
+ return false;
124
+ }
125
+ const { typeName } = typeAnnotation;
126
+ return (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName &&
127
+ typeName.left.type === utils_1.AST_NODE_TYPES.Identifier &&
128
+ typeName.left.name === 'jest' &&
129
+ typeName.right.type === utils_1.AST_NODE_TYPES.Identifier &&
130
+ JEST_MOCK_TYPE_NAMES.has(typeName.right.name));
131
+ };
132
+ const isJestMockCast = (node) => {
133
+ let current = node;
134
+ while (isValueWrapper(current)) {
135
+ if (current.type === utils_1.AST_NODE_TYPES.TSAsExpression &&
136
+ isJestMockTypeReference(current.typeAnnotation)) {
137
+ return true;
138
+ }
139
+ current = current.expression;
140
+ }
141
+ return false;
142
+ };
143
+ // Mirrors `global-const-style`'s own list. Next.js recognizes these export
144
+ // names by their literal identifier, so that rule declines the rename outright
145
+ // rather than breaking the framework contract (#1257) — nothing there governs
146
+ // the name, so this rule keeps its report. Only the EXPORT name matters to
147
+ // Next.js, exactly as the sibling gates it.
148
+ const NEXTJS_RESERVED_EXPORTS = new Set([
149
+ 'config',
150
+ 'getServerSideProps',
151
+ 'getStaticProps',
152
+ 'getStaticPaths',
153
+ 'getInitialProps',
154
+ 'middleware',
155
+ ]);
156
+ const isGlobalConstStyleGoverned = (declarator) => {
157
+ const declaration = declarator.parent;
158
+ if (!declaration ||
159
+ declaration.type !== utils_1.AST_NODE_TYPES.VariableDeclaration ||
160
+ declaration.kind !== 'const') {
161
+ return false;
162
+ }
163
+ // The sibling's own scope gate: module scope, whether written bare or behind
164
+ // an inline `export`. Exports are INCLUDED because it reports `upperSnakeCase`
165
+ // on them — it withholds only the FIX (#1700) — so leaving them out kept the
166
+ // pair unsatisfiable for `export const element: JSX.Element = …` (#1847).
167
+ const isExported = declaration.parent?.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration;
168
+ if (declaration.parent?.type !== utils_1.AST_NODE_TYPES.Program && !isExported) {
169
+ return false;
170
+ }
171
+ if (isExported &&
172
+ declarator.id.type === utils_1.AST_NODE_TYPES.Identifier &&
173
+ NEXTJS_RESERVED_EXPORTS.has(declarator.id.name)) {
174
+ return false;
175
+ }
176
+ // The function-value / component-factory skip is evaluated over the whole
177
+ // declaration LIST there, so `const a = () => {}, b = <div />;` exempts both.
178
+ const listSkipped = declaration.declarations.some((one) => {
179
+ if (one.id.type !== utils_1.AST_NODE_TYPES.Identifier || !one.init) {
180
+ return false;
181
+ }
182
+ const target = unwrapValueWrappers(one.init);
183
+ return isFunctionValue(target) || isComponentFactoryCall(target);
184
+ });
185
+ if (listSkipped) {
186
+ return false;
187
+ }
188
+ const { init } = declarator;
189
+ if (!init) {
190
+ return false;
191
+ }
192
+ return (!isDynamicValue(init) && !isBindingAlias(init) && !isJestMockCast(init));
193
+ };
11
194
  exports.enforceReactTypeNaming = (0, createRule_1.createRule)({
12
195
  name: 'enforce-react-type-naming',
13
196
  meta: {
@@ -123,6 +306,12 @@ exports.enforceReactTypeNaming = (0, createRule_1.createRule)({
123
306
  // Skip destructured variables
124
307
  if (isDestructured(id))
125
308
  return;
309
+ // Yield the name to `global-const-style` where it governs (Issue #1846).
310
+ // Both branches yield: its UPPER_SNAKE_CASE target already satisfies the
311
+ // component branch's "starts uppercase", so nothing is lost there, and
312
+ // the element branch is the one that cannot coexist with it at all.
313
+ if (isGlobalConstStyleGoverned(node))
314
+ return;
126
315
  const variableName = id.name;
127
316
  // Get the type annotation
128
317
  const typeAnnotation = id.typeAnnotation?.typeAnnotation;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.130",
3
+ "version": "1.20.131",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,27 @@
1
1
  [
2
+ {
3
+ "version": "1.20.131",
4
+ "date": "2026-08-07T10:28:22.079Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-dynamic-imports",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1845
11
+ ],
12
+ "summary": "cover a package's subpaths from one ignoredLibraries entry (closes #1845)"
13
+ },
14
+ {
15
+ "name": "enforce-react-type-naming",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 1846,
19
+ 1847
20
+ ],
21
+ "summary": "yield exported module-scope constants to global-const-style (closes #1847); yield module-scope constants to global-const-style (closes #1846)"
22
+ }
23
+ ]
24
+ },
2
25
  {
3
26
  "version": "1.20.130",
4
27
  "date": "2026-08-07T08:38:15.008Z",