@blumintinc/eslint-plugin-blumint 1.20.17 → 1.20.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/lib/index.js
CHANGED
|
@@ -191,6 +191,40 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
191
191
|
* name (getRefreshToken must NOT match).
|
|
192
192
|
*/
|
|
193
193
|
const REFETCH_PATTERN = /^(refresh|reload|refetch|revalidate|resync|sync)/i;
|
|
194
|
+
/**
|
|
195
|
+
* Matches navigation callees by their leading verb. A route transition is an
|
|
196
|
+
* ordering barrier rather than a data dependency: the awaits around it are
|
|
197
|
+
* sequenced so their side effects land on the intended page. `await
|
|
198
|
+
* push(url)` followed by `await acceptInvite(...)` is written that way so the
|
|
199
|
+
* accept flow's dialogs mount on the destination page; Promise.all starts the
|
|
200
|
+
* accept flow concurrently with the route transition, so its dialogs open on
|
|
201
|
+
* the source page and are unmounted mid-navigation. The reverse order is
|
|
202
|
+
* equally load-bearing -- parallelizing `await save()` with a following
|
|
203
|
+
* `await push(url)` can navigate away before the save settles -- so a
|
|
204
|
+
* navigation anywhere in the run blocks the whole run. Anchored at the start
|
|
205
|
+
* so it fires on the callee's own verb (pushRoute, navigateTo,
|
|
206
|
+
* redirectToLogin) rather than on an arbitrary substring elsewhere in the
|
|
207
|
+
* name.
|
|
208
|
+
*/
|
|
209
|
+
const NAVIGATION_PATTERN = /^(push|replace|navigate|redirect|reroute|goto)/i;
|
|
210
|
+
/**
|
|
211
|
+
* Matches router-like receivers so that every method invoked on one counts
|
|
212
|
+
* as navigation (`router.back()`, `history.go(-1)`, `navigation.reset()`).
|
|
213
|
+
* Keyed on the receiver rather than the method because the remaining history
|
|
214
|
+
* verbs (back, forward, go) are far too generic to match on their own.
|
|
215
|
+
*/
|
|
216
|
+
const NAVIGATION_RECEIVER_PATTERN = /^(router|history|navigation|nav)$/i;
|
|
217
|
+
/**
|
|
218
|
+
* Checks whether an awaited call performs a route transition.
|
|
219
|
+
*/
|
|
220
|
+
function isNavigationCall(awaitExpr) {
|
|
221
|
+
const receiverName = getCalleeReceiverName(awaitExpr);
|
|
222
|
+
if (receiverName && NAVIGATION_RECEIVER_PATTERN.test(receiverName)) {
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
const methodName = getCalleeMethodName(awaitExpr);
|
|
226
|
+
return !!methodName && NAVIGATION_PATTERN.test(methodName);
|
|
227
|
+
}
|
|
194
228
|
/**
|
|
195
229
|
* Extracts the callee's method name (the identifier bearing the leading
|
|
196
230
|
* verb) from an await expression argument. Handles both direct
|
|
@@ -364,6 +398,22 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
364
398
|
return true;
|
|
365
399
|
}
|
|
366
400
|
}
|
|
401
|
+
// 6. Navigation ordering barrier. An awaited route transition sequences
|
|
402
|
+
// the awaits around it by UI lifetime rather than by data: the operations
|
|
403
|
+
// before it must settle on the source page, and the operations after it
|
|
404
|
+
// must mount on the destination page. Promise.all runs every operand
|
|
405
|
+
// concurrently, which races both of those against the route change --
|
|
406
|
+
// dialogs opened by a following await appear on the source page and are
|
|
407
|
+
// destroyed when the transition lands. Unlike the guard and refetch
|
|
408
|
+
// barriers, position does not matter: a navigation is a barrier whether it
|
|
409
|
+
// leads or trails the run. Captured results qualify too, since the hazard
|
|
410
|
+
// is the transition itself, not the value it returns.
|
|
411
|
+
for (const node of awaitNodes) {
|
|
412
|
+
const awaitExpr = getAwaitExpression(node);
|
|
413
|
+
if (awaitExpr && isNavigationCall(awaitExpr)) {
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
367
417
|
// If any node is a variable declaration with destructuring, consider it as having dependencies
|
|
368
418
|
for (const node of awaitNodes) {
|
|
369
419
|
if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
@@ -374,7 +424,7 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
374
424
|
}
|
|
375
425
|
}
|
|
376
426
|
}
|
|
377
|
-
//
|
|
427
|
+
// 7. Shared-receiver ordering barrier. Two awaited calls whose callees are
|
|
378
428
|
// member expressions on the SAME receiver identifier (e.g. `ref.set(x)`
|
|
379
429
|
// then `ref.get()`) can carry a read-after-write / write-after-write
|
|
380
430
|
// dependency: the later call may observe or overwrite state the earlier
|
|
@@ -1,11 +1,51 @@
|
|
|
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.preferCloneDeep = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
4
8
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
9
|
const createRule_1 = require("../utils/createRule");
|
|
6
10
|
const CLONE_DEEP_NAME = 'cloneDeep';
|
|
7
11
|
const CLONE_DEEP_MODULE = 'functions/src/util/cloneDeep';
|
|
12
|
+
const CLONE_DEEP_TARGET = 'src/util/cloneDeep';
|
|
13
|
+
const FUNCTIONS_TIER_SEGMENT = '/functions/src/';
|
|
14
|
+
const FUNCTIONS_ROOT_SEGMENT = '/functions/';
|
|
8
15
|
const INDENT_STEP = ' ';
|
|
16
|
+
const toPosixPath = (filePath) => filePath.replace(/\\/g, '/');
|
|
17
|
+
const ensureRelativeSpecifier = (specifier) => specifier.startsWith('.') ? specifier : `./${specifier}`;
|
|
18
|
+
const isWindowsDrivePath = (filePath) => /^[A-Za-z]:[\\/]/.test(filePath);
|
|
19
|
+
const isValidRelativePath = (relativePath) => relativePath !== '' &&
|
|
20
|
+
!path_1.default.isAbsolute(relativePath) &&
|
|
21
|
+
!isWindowsDrivePath(relativePath);
|
|
22
|
+
/**
|
|
23
|
+
* The helper lives in one place but the two TypeScript tiers reach it
|
|
24
|
+
* differently: the root tsconfig maps `functions/*` through `paths`, so files
|
|
25
|
+
* outside `functions/` resolve the bare specifier, while `functions/tsconfig.json`
|
|
26
|
+
* is rooted at `functions/` and declares no `paths`, leaving backend files able
|
|
27
|
+
* to reach a sibling util only by relative path. A single hardcoded specifier
|
|
28
|
+
* therefore emits an unresolvable import for every backend fix (#1389).
|
|
29
|
+
*
|
|
30
|
+
* Returns null when no correct specifier exists, which makes the caller decline
|
|
31
|
+
* the fix rather than write an import that cannot resolve.
|
|
32
|
+
*/
|
|
33
|
+
function buildCloneDeepSpecifier(sourceFilePath, cwd) {
|
|
34
|
+
const absoluteFilename = toPosixPath(path_1.default.isAbsolute(sourceFilePath)
|
|
35
|
+
? sourceFilePath
|
|
36
|
+
: path_1.default.join(cwd, sourceFilePath));
|
|
37
|
+
const tierIndex = absoluteFilename.indexOf(FUNCTIONS_TIER_SEGMENT);
|
|
38
|
+
if (tierIndex === -1) {
|
|
39
|
+
return CLONE_DEEP_MODULE;
|
|
40
|
+
}
|
|
41
|
+
const functionsRoot = absoluteFilename.slice(0, tierIndex + FUNCTIONS_ROOT_SEGMENT.length);
|
|
42
|
+
const targetPath = path_1.default.join(functionsRoot, CLONE_DEEP_TARGET);
|
|
43
|
+
const relativePath = path_1.default.relative(path_1.default.dirname(absoluteFilename), targetPath);
|
|
44
|
+
if (!isValidRelativePath(relativePath)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return ensureRelativeSpecifier(toPosixPath(relativePath));
|
|
48
|
+
}
|
|
9
49
|
/**
|
|
10
50
|
* Only BluMint's own `cloneDeep` accepts an overrides argument, so an existing
|
|
11
51
|
* binding coming from anywhere else (notably `lodash`) must not be reused by the
|
|
@@ -44,6 +84,8 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
|
|
|
44
84
|
// Track processed nodes to avoid duplicate reports
|
|
45
85
|
const processedNodes = new Set();
|
|
46
86
|
const sourceCode = context.sourceCode;
|
|
87
|
+
const cwd = typeof context.getCwd === 'function' ? context.getCwd() : process.cwd();
|
|
88
|
+
const cloneDeepSpecifier = buildCloneDeepSpecifier(context.getFilename(), cwd);
|
|
47
89
|
function normalizedTextOf(node) {
|
|
48
90
|
return sourceCode.getText(node).replace(/\s+/g, '');
|
|
49
91
|
}
|
|
@@ -324,7 +366,8 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
|
|
|
324
366
|
/**
|
|
325
367
|
* Returns the fixes required for `cloneDeep` to resolve, an empty list when
|
|
326
368
|
* it already does, or null when a conflicting binding of that name exists —
|
|
327
|
-
* shadowing it would silently call something else
|
|
369
|
+
* shadowing it would silently call something else — or when no import
|
|
370
|
+
* specifier that resolves from this file can be derived.
|
|
328
371
|
*/
|
|
329
372
|
function buildImportFixes(fixer, scope) {
|
|
330
373
|
const existing = utils_1.ASTUtils.findVariable(scope, CLONE_DEEP_NAME);
|
|
@@ -360,7 +403,12 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
|
|
|
360
403
|
const lastSpecifier = namedSpecifiers[namedSpecifiers.length - 1];
|
|
361
404
|
return [fixer.insertTextAfter(lastSpecifier, `, ${CLONE_DEEP_NAME}`)];
|
|
362
405
|
}
|
|
363
|
-
|
|
406
|
+
// Reusing an existing import needs no specifier of its own, so only a
|
|
407
|
+
// freshly written import depends on one being derivable.
|
|
408
|
+
if (cloneDeepSpecifier === null) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
const importText = `import { ${CLONE_DEEP_NAME} } from '${cloneDeepSpecifier}';\n`;
|
|
364
412
|
const [firstImport] = importDeclarations;
|
|
365
413
|
if (firstImport) {
|
|
366
414
|
return [fixer.insertTextBefore(firstImport, importText)];
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.19",
|
|
4
|
+
"date": "2026-07-29T18:36:35.802Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "prefer-clone-deep",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1389
|
|
11
|
+
],
|
|
12
|
+
"summary": "derive autofix import specifier from file tier (closes #1389)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.20.18",
|
|
18
|
+
"date": "2026-07-29T17:40:51.418Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "parallelize-async-operations",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1388
|
|
25
|
+
],
|
|
26
|
+
"summary": "treat route transitions as an ordering barrier (closes #1388)"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.20.17",
|
|
4
32
|
"date": "2026-07-29T10:47:56.581Z",
|