@blumintinc/eslint-plugin-blumint 1.20.61 → 1.20.63

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.61',
226
+ version: '1.20.63',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.enforceAssertSafeObjectKey = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
7
8
  const path_1 = __importDefault(require("path"));
8
9
  const utils_1 = require("@typescript-eslint/utils");
9
10
  const createRule_1 = require("../utils/createRule");
@@ -30,6 +31,137 @@ function isAssertSafeSpecifier(specifier) {
30
31
  function normalizeModulePath(value) {
31
32
  return value.replace(/\\/g, '/').replace(/\.(tsx?|jsx?|mts|cts)$/i, '');
32
33
  }
34
+ /**
35
+ * Extensions whose module system the file name settles on its own, ahead of any
36
+ * package manifest: `.mjs` is ESM and `.cjs` is CommonJS by definition, while a
37
+ * TypeScript source — `.mts` included — is compiled before it runs and its
38
+ * specifiers are resolved by the compiler, which accepts the extensionless
39
+ * form. Only `.js` is ambiguous and has to ask the nearest manifest.
40
+ */
41
+ const NATIVE_ESM_EXTENSION = /\.mjs$/i;
42
+ const NON_NATIVE_ESM_EXTENSION = /\.(cjs|tsx?|mts|cts)$/i;
43
+ const AMBIGUOUS_JS_EXTENSION = /\.js$/i;
44
+ /**
45
+ * Whether the nearest `package.json` at or above `startDir` declares
46
+ * `"type": "module"`, which is what makes a `.js` file native ESM. Node consults
47
+ * only the first manifest found, and treats a missing `type` field as
48
+ * CommonJS — so the walk stops at the first manifest it can read, not at the
49
+ * first one that declares a type.
50
+ *
51
+ * Every filesystem touch is optional: a manifest that cannot be read is
52
+ * indistinguishable from an absent one and the walk continues, while a manifest
53
+ * that cannot be parsed declines the extension rather than guessing. Declining
54
+ * yields the bare specifier, which every non-ESM consumer resolves — the safer
55
+ * answer when the tree cannot say which kind of consumer this is.
56
+ */
57
+ function nearestManifestDeclaresModule(startDir) {
58
+ let dir = startDir;
59
+ for (;;) {
60
+ let manifest;
61
+ try {
62
+ manifest = fs_1.default.readFileSync(path_1.default.join(dir, 'package.json'), 'utf8');
63
+ }
64
+ catch {
65
+ const parent = path_1.default.dirname(dir);
66
+ if (parent === dir) {
67
+ return false;
68
+ }
69
+ dir = parent;
70
+ continue;
71
+ }
72
+ try {
73
+ return (JSON.parse(manifest)?.type === 'module');
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
79
+ }
80
+ /** Names that read as a positional sequence rather than a keyed record. */
81
+ const ARRAY_LIKE_NAME = /^(array|arr|items|elements|list|collection|data)s?$/i;
82
+ /**
83
+ * The name an indexed object is judged by. A collection reached as a field —
84
+ * `raster.data[i]`, `this.items[i]`, `a.b.list[i]` — carries its signal on the
85
+ * property rather than on the root object, so the property name is what the
86
+ * array-ish test sees there.
87
+ */
88
+ function indexedObjectName(node) {
89
+ if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
90
+ return node.name;
91
+ }
92
+ if (node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
93
+ !node.computed &&
94
+ node.property.type === utils_1.AST_NODE_TYPES.Identifier) {
95
+ return node.property.name;
96
+ }
97
+ return '';
98
+ }
99
+ /**
100
+ * Operators that coerce both operands with ToNumeric before operating, so the
101
+ * result is a number (or bigint) whatever the operands hold. `+` is absent: it
102
+ * keeps string concatenation, so it is judged from its operands instead.
103
+ */
104
+ const NUMERIC_BINARY_OPERATORS = new Set([
105
+ '-',
106
+ '*',
107
+ '/',
108
+ '%',
109
+ '**',
110
+ '<<',
111
+ '>>',
112
+ '>>>',
113
+ '&',
114
+ '|',
115
+ '^',
116
+ ]);
117
+ /** The compound assignments of NUMERIC_BINARY_OPERATORS, `+=` excluded. */
118
+ const NUMERIC_ASSIGNMENT_OPERATORS = new Set([
119
+ '-=',
120
+ '*=',
121
+ '/=',
122
+ '%=',
123
+ '**=',
124
+ '<<=',
125
+ '>>=',
126
+ '>>>=',
127
+ '&=',
128
+ '|=',
129
+ '^=',
130
+ ]);
131
+ /** Global conversions whose result is a number regardless of the argument. */
132
+ const NUMERIC_CALLEE_NAMES = new Set(['Number', 'parseInt', 'parseFloat']);
133
+ /**
134
+ * Whether the call is guaranteed to produce a number. Every `Math` member
135
+ * returns one, so the namespace is accepted wholesale rather than enumerated.
136
+ */
137
+ function isNumericCall(node) {
138
+ const { callee } = node;
139
+ if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
140
+ return NUMERIC_CALLEE_NAMES.has(callee.name);
141
+ }
142
+ return (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
143
+ !callee.computed &&
144
+ callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
145
+ callee.object.name === 'Math');
146
+ }
147
+ /**
148
+ * A `: number` annotation on a binding name. Only parameters and variable
149
+ * declarators carry one, and a declarator is judged from its writes, so this
150
+ * effectively identifies a numeric parameter.
151
+ */
152
+ function isNumberAnnotated(node) {
153
+ return (node.type === utils_1.AST_NODE_TYPES.Identifier &&
154
+ node.typeAnnotation?.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSNumberKeyword);
155
+ }
156
+ /**
157
+ * Whether the definition can hold a number: a declarator (whose value is proven
158
+ * by its writes) or a parameter annotated `: number`. Anything else — an
159
+ * import, a function or class name, a catch binding — is not.
160
+ */
161
+ function definesNumericBinding(def) {
162
+ return (def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator ||
163
+ isNumberAnnotated(def.name));
164
+ }
33
165
  exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
34
166
  name: 'enforce-assert-safe-object-key',
35
167
  meta: {
@@ -91,6 +223,28 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
91
223
  const fileRelToCwd = path_1.default.relative(cwd, rawFilename).replace(/\\/g, '/');
92
224
  return path_1.default.posix.dirname(fileRelToCwd);
93
225
  };
226
+ /**
227
+ * Whether the file being fixed runs as native ESM, where node's resolver
228
+ * takes a specifier literally and an extensionless one throws
229
+ * ERR_MODULE_NOT_FOUND at startup. TypeScript and bundler consumers resolve
230
+ * extensionless specifiers themselves, so they keep the bare form.
231
+ *
232
+ * The walk for an ambiguous `.js` file runs only while a fix is being built,
233
+ * costs a handful of stat-sized reads, and needs an absolute name to have a
234
+ * directory to start from.
235
+ */
236
+ const isNativeEsmFile = () => {
237
+ const filename = context.getFilename().replace(/\\/g, '/');
238
+ if (NATIVE_ESM_EXTENSION.test(filename)) {
239
+ return true;
240
+ }
241
+ if (NON_NATIVE_ESM_EXTENSION.test(filename) ||
242
+ !AMBIGUOUS_JS_EXTENSION.test(filename) ||
243
+ !path_1.default.isAbsolute(filename)) {
244
+ return false;
245
+ }
246
+ return nearestManifestDeclaresModule(path_1.default.dirname(filename));
247
+ };
94
248
  /**
95
249
  * Computes the module specifier for the injected assertSafe import.
96
250
  *
@@ -104,17 +258,22 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
104
258
  * emitted import resolves from that file's location.
105
259
  */
106
260
  const computeImportSpecifier = () => {
261
+ // The helper is authored as TypeScript and runs as its compiled output, so
262
+ // a native-ESM importer names the emitted `.js` file. TS resolves a `.js`
263
+ // specifier back to the `.ts` source under nodenext, which keeps the one
264
+ // spelling correct for both.
265
+ const extension = isNativeEsmFile() ? '.js' : '';
107
266
  const fileDir = fileDirFromRoot();
108
267
  // Emitting the configured path verbatim preserves the option's literal
109
268
  // value for non-file lints.
110
269
  if (fileDir === null) {
111
- return importPath;
270
+ return `${importPath}${extension}`;
112
271
  }
113
272
  let specifier = path_1.default.posix.relative(fileDir, assertSafeTarget);
114
273
  if (!specifier.startsWith('.')) {
115
274
  specifier = `./${specifier}`;
116
275
  }
117
- return specifier;
276
+ return `${specifier}${extension}`;
118
277
  };
119
278
  /**
120
279
  * Whether a module specifier written in the file denotes the same helper the
@@ -245,6 +404,86 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
245
404
  init.callee.name === 'assertSafe');
246
405
  });
247
406
  };
407
+ /**
408
+ * Whether the syntax alone proves the key is a number. `__proto__`,
409
+ * `constructor` and `prototype` are never the string form of a number, so a
410
+ * numeric key cannot reach the prototype surface assertSafe exists to
411
+ * guard: the call would be dead weight, and in an index-heavy loop a
412
+ * per-iteration coercion. The judgement stays syntactic — a key the syntax
413
+ * does not prove numeric keeps being reported.
414
+ */
415
+ const isStaticallyNumeric = (node, seen = new Set()) => {
416
+ switch (node.type) {
417
+ case utils_1.AST_NODE_TYPES.Literal:
418
+ return typeof node.value === 'number';
419
+ case utils_1.AST_NODE_TYPES.UpdateExpression:
420
+ return true;
421
+ case utils_1.AST_NODE_TYPES.UnaryExpression:
422
+ return (node.operator === '-' ||
423
+ node.operator === '+' ||
424
+ node.operator === '~');
425
+ case utils_1.AST_NODE_TYPES.BinaryExpression:
426
+ if (NUMERIC_BINARY_OPERATORS.has(node.operator)) {
427
+ return true;
428
+ }
429
+ return (node.operator === '+' &&
430
+ isStaticallyNumeric(node.left, seen) &&
431
+ isStaticallyNumeric(node.right, seen));
432
+ case utils_1.AST_NODE_TYPES.CallExpression:
433
+ return isNumericCall(node);
434
+ case utils_1.AST_NODE_TYPES.MemberExpression:
435
+ // `.length` is a number on arrays, typed arrays and strings alike.
436
+ return (!node.computed &&
437
+ node.property.type === utils_1.AST_NODE_TYPES.Identifier &&
438
+ node.property.name === 'length');
439
+ case utils_1.AST_NODE_TYPES.TSAsExpression:
440
+ case utils_1.AST_NODE_TYPES.TSNonNullExpression:
441
+ return isStaticallyNumeric(node.expression, seen);
442
+ case utils_1.AST_NODE_TYPES.Identifier:
443
+ return isNumericIdentifier(node, seen);
444
+ default:
445
+ return false;
446
+ }
447
+ };
448
+ /**
449
+ * Whether every declaration and every write of the resolved binding keeps it
450
+ * numeric. `seen` is copied per resolution step so a cycle
451
+ * (`let a = b; let b = a;`) terminates without a sibling occurrence of one
452
+ * variable (`arr[i + i]`) being mistaken for that cycle.
453
+ */
454
+ const isNumericIdentifier = (node, seen) => {
455
+ const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, node), node.name);
456
+ if (!variable || seen.has(variable)) {
457
+ return false;
458
+ }
459
+ const nextSeen = new Set(seen).add(variable);
460
+ if (variable.defs.length === 0 ||
461
+ !variable.defs.every(definesNumericBinding)) {
462
+ return false;
463
+ }
464
+ const writes = variable.references.filter((reference) => reference.isWrite());
465
+ const staysNumeric = writes.every((reference) => {
466
+ const { writeExpr } = reference;
467
+ // `i++`/`--i` write a number back with no expression to inspect.
468
+ if (!writeExpr) {
469
+ return true;
470
+ }
471
+ const assignment = writeExpr.parent;
472
+ if (assignment?.type === utils_1.AST_NODE_TYPES.AssignmentExpression &&
473
+ NUMERIC_ASSIGNMENT_OPERATORS.has(assignment.operator)) {
474
+ return true;
475
+ }
476
+ return isStaticallyNumeric(writeExpr, nextSeen);
477
+ });
478
+ if (!staysNumeric) {
479
+ return false;
480
+ }
481
+ // A `: number` parameter is numeric from its declaration; a declarator is
482
+ // only proven by a write, and its initializer is one — so `let k;` with no
483
+ // write anywhere holds undefined and stays unproven.
484
+ return (writes.length > 0 ||
485
+ variable.defs.every((def) => isNumberAnnotated(def.name)));
486
+ };
248
487
  return {
249
488
  // Handle computed property in object destructuring
250
489
  Property(node) {
@@ -305,14 +544,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
305
544
  return;
306
545
  }
307
546
  // Try to determine if this is likely an array or dictionary
308
- const objectNode = node.object;
309
- let objectName = '';
310
- let isLikelyArray = false;
311
- if (objectNode.type === utils_1.AST_NODE_TYPES.Identifier) {
312
- objectName = objectNode.name.toLowerCase();
313
- isLikelyArray =
314
- /^(array|arr|items|elements|list|collection|data)s?$/i.test(objectName);
315
- }
547
+ const isLikelyArray = ARRAY_LIKE_NAME.test(indexedObjectName(node.object));
316
548
  // Check for string literals - allow them for dictionaries but not for regular objects
317
549
  if (property.type === utils_1.AST_NODE_TYPES.Literal &&
318
550
  typeof property.value === 'string') {
@@ -325,6 +557,12 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
325
557
  // Numeric literals are fine, no need for assertSafe
326
558
  return;
327
559
  }
560
+ // A key the syntax proves numeric — a loop counter, an offset
561
+ // computation, Math.floor(...) — cannot name a prototype field, so
562
+ // validating it guards nothing.
563
+ if (isStaticallyNumeric(property)) {
564
+ return;
565
+ }
328
566
  // Check if we're using String(id) pattern
329
567
  if (property.type === utils_1.AST_NODE_TYPES.CallExpression &&
330
568
  property.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
@@ -1 +1,7 @@
1
- export declare const enforceVerbNounNaming: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"functionVerbPhrase", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
1
+ type Options = [
2
+ {
3
+ readonly externallyNamedExports?: readonly string[];
4
+ }
5
+ ];
6
+ export declare const enforceVerbNounNaming: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"functionVerbPhrase", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
7
+ export {};
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.enforceVerbNounNaming = void 0;
7
7
  const utils_1 = require("@typescript-eslint/utils");
8
+ const minimatch_1 = require("minimatch");
8
9
  const createRule_1 = require("../utils/createRule");
9
10
  const ASTHelpers_1 = require("../utils/ASTHelpers");
10
11
  const compromise_1 = __importDefault(require("compromise"));
@@ -3776,13 +3777,87 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
3776
3777
  requiresTypeChecking: false,
3777
3778
  extendsBaseRule: false,
3778
3779
  },
3779
- schema: [],
3780
+ schema: [
3781
+ {
3782
+ type: 'object',
3783
+ properties: {
3784
+ externallyNamedExports: {
3785
+ type: 'array',
3786
+ items: { type: 'string' },
3787
+ },
3788
+ },
3789
+ additionalProperties: false,
3790
+ },
3791
+ ],
3780
3792
  messages: {
3781
3793
  functionVerbPhrase: 'Function "{{name}}" should start with an action verb followed by the thing it acts on. Verb-first names tell readers this symbol performs work instead of representing data, which keeps APIs predictable and prevents accidental misuse. Rename "{{name}}" to a verb-noun phrase such as "fetchUsers" or "processRequest".',
3782
3794
  },
3783
3795
  },
3784
- defaultOptions: [],
3785
- create(context) {
3796
+ defaultOptions: [{}],
3797
+ create(context, [options]) {
3798
+ const externallyNamedExports = options?.externallyNamedExports ?? [];
3799
+ const rawFilename = context.filename ?? context.getFilename();
3800
+ const normalizedFilename = rawFilename.replace(/\\/g, '/');
3801
+ // A leading `**` consumes the extra leading segments of an absolute path,
3802
+ // so one match handles both repo-relative and absolute filenames. Malformed
3803
+ // globs are treated as non-matching by minimatch rather than throwing, which
3804
+ // keeps a bad pattern from aborting the whole lint run.
3805
+ const hasExternallyNamedExports = externallyNamedExports.some((glob) => (0, minimatch_1.minimatch)(normalizedFilename, glob, { dot: true, matchBase: true }));
3806
+ // `export { local as exported }` and `export default local` name the module
3807
+ // binding from a distance, so the local name still belongs to the external
3808
+ // specification. Collected once per file, and only when a glob matched.
3809
+ let deferredExportedNames;
3810
+ function getDeferredExportedNames() {
3811
+ if (deferredExportedNames) {
3812
+ return deferredExportedNames;
3813
+ }
3814
+ const names = new Set();
3815
+ for (const statement of context.sourceCode.ast.body) {
3816
+ if (statement.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration &&
3817
+ !statement.source) {
3818
+ for (const specifier of statement.specifiers) {
3819
+ const local = specifier.local;
3820
+ if (local.type === utils_1.AST_NODE_TYPES.Identifier) {
3821
+ names.add(local.name);
3822
+ }
3823
+ }
3824
+ }
3825
+ else if (statement.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration &&
3826
+ statement.declaration.type === utils_1.AST_NODE_TYPES.Identifier) {
3827
+ names.add(statement.declaration.name);
3828
+ }
3829
+ }
3830
+ deferredExportedNames = names;
3831
+ return names;
3832
+ }
3833
+ /**
3834
+ * Exported symbols in a file whose names are specified elsewhere (a runtime
3835
+ * registry key, a CLI verb, an artifact path) cannot be renamed without
3836
+ * desynchronizing them from that specification. Non-exported symbols in the
3837
+ * same file are ordinary local code and stay checked.
3838
+ */
3839
+ function isExternallyNamedExport(node, name) {
3840
+ if (!hasExternallyNamedExports) {
3841
+ return false;
3842
+ }
3843
+ if (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
3844
+ const parent = node.parent;
3845
+ if (parent?.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
3846
+ parent?.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration) {
3847
+ return true;
3848
+ }
3849
+ // Only a module-level binding can be the target of a deferred export.
3850
+ return (parent?.type === utils_1.AST_NODE_TYPES.Program &&
3851
+ getDeferredExportedNames().has(name));
3852
+ }
3853
+ const declaration = node.parent;
3854
+ const declarationParent = declaration?.parent;
3855
+ if (declarationParent?.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration) {
3856
+ return true;
3857
+ }
3858
+ return (declarationParent?.type === utils_1.AST_NODE_TYPES.Program &&
3859
+ getDeferredExportedNames().has(name));
3860
+ }
3786
3861
  function extractFirstWord(name) {
3787
3862
  const firstChar = name.charAt(0);
3788
3863
  const rest = name.slice(1);
@@ -3925,6 +4000,9 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
3925
4000
  if (isReactComponent(node)) {
3926
4001
  return;
3927
4002
  }
4003
+ if (isExternallyNamedExport(node, node.id.name)) {
4004
+ return;
4005
+ }
3928
4006
  if (!isVerbPhrase(node.id.name)) {
3929
4007
  context.report({
3930
4008
  node: node.id,
@@ -3942,6 +4020,9 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
3942
4020
  if (isReactComponent(node.init)) {
3943
4021
  return;
3944
4022
  }
4023
+ if (isExternallyNamedExport(node, node.id.name)) {
4024
+ return;
4025
+ }
3945
4026
  if (!isVerbPhrase(node.id.name)) {
3946
4027
  context.report({
3947
4028
  node: node.id,
@@ -757,6 +757,18 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
757
757
  }
758
758
  return false;
759
759
  }
760
+ /**
761
+ * Returns the leading whitespace of the source line a node starts on.
762
+ *
763
+ * A statement that shares its line with earlier code yields that line's
764
+ * indentation rather than the node's own column, which keeps the generated
765
+ * block aligned with the enclosing statement instead of with an arbitrary
766
+ * mid-line offset.
767
+ */
768
+ function getIndentationOf(node) {
769
+ const line = sourceCode.lines[node.loc.start.line - 1] ?? '';
770
+ return /^[ \t]*/.exec(line)?.[0] ?? '';
771
+ }
760
772
  /**
761
773
  * Generates a fix for sequential awaits
762
774
  *
@@ -788,6 +800,24 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
788
800
  idsText.push('');
789
801
  }
790
802
  }
803
+ // The replacement range starts at the first await's own start offset, so
804
+ // ESLint leaves that line's leading whitespace in place and only the
805
+ // FIRST line inherits the surrounding indentation. Every continuation
806
+ // line the fixer emits has to carry that indentation itself, otherwise
807
+ // the array elements and closing bracket land at column 2 and column 0
808
+ // no matter how deeply the original awaits were nested.
809
+ const baseIndent = getIndentationOf(awaitNodes[0]);
810
+ // Match the file's own indentation character instead of assuming spaces,
811
+ // so a tab-indented file does not end up with mixed tabs and spaces. The
812
+ // space fallback is two wide to match the repo's prettier config.
813
+ const indentUnit = baseIndent.includes('\t') ? '\t' : ' ';
814
+ const elementIndent = `${baseIndent}${indentUnit}`;
815
+ // Arguments are spliced in verbatim: an argument spanning multiple lines
816
+ // keeps its original interior indentation because that interior can be
817
+ // the contents of a template literal, where whitespace is significant
818
+ // data rather than formatting and re-indenting would silently change the
819
+ // produced string.
820
+ const elementsText = awaitArguments.join(`,\n${elementIndent}`);
791
821
  let promiseAllText;
792
822
  if (hasVariableDeclarations) {
793
823
  if (declKinds.size !== 1) {
@@ -798,11 +828,11 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
798
828
  }
799
829
  const destructuringPattern = idsText.join(', ');
800
830
  const declKind = Array.from(declKinds)[0];
801
- promiseAllText = `${declKind} [${destructuringPattern}] = await Promise.all([\n ${awaitArguments.join(',\n ')}\n]);`;
831
+ promiseAllText = `${declKind} [${destructuringPattern}] = await Promise.all([\n${elementIndent}${elementsText}\n${baseIndent}]);`;
802
832
  }
803
833
  else {
804
834
  // Simple Promise.all without variable assignments
805
- promiseAllText = `await Promise.all([\n ${awaitArguments.join(',\n ')}\n]);`;
835
+ promiseAllText = `await Promise.all([\n${elementIndent}${elementsText}\n${baseIndent}]);`;
806
836
  }
807
837
  const startPos = awaitNodes[0].range[0];
808
838
  const endPos = awaitNodes[awaitNodes.length - 1].range[1];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.61",
3
+ "version": "1.20.63",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,41 @@
1
1
  [
2
+ {
3
+ "version": "1.20.63",
4
+ "date": "2026-08-01T09:37:04.617Z",
5
+ "rules": [
6
+ {
7
+ "name": "parallelize-async-operations",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1557
11
+ ],
12
+ "summary": "indent Promise.all output relative to the replaced statements (closes #1557)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.20.62",
18
+ "date": "2026-08-01T09:01:13.989Z",
19
+ "rules": [
20
+ {
21
+ "name": "enforce-assert-safe-object-key",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 1554,
25
+ 1556
26
+ ],
27
+ "summary": "append .js to the injected specifier for native-ESM consumers (closes #1556); exempt statically numeric keys and array-ish member objects (closes #1554)"
28
+ },
29
+ {
30
+ "name": "enforce-verb-noun-naming",
31
+ "changeType": "fix",
32
+ "issues": [
33
+ 1555
34
+ ],
35
+ "summary": "add externallyNamedExports glob option for externally-specified names (closes #1555)"
36
+ }
37
+ ]
38
+ },
2
39
  {
3
40
  "version": "1.20.61",
4
41
  "date": "2026-08-01T06:30:50.860Z",