@blumintinc/eslint-plugin-blumint 1.20.19 → 1.20.20
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
|
@@ -1,14 +1,62 @@
|
|
|
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.enforceQueryKeyTs = 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");
|
|
10
|
+
// The module's path below the project root doubles as the bare specifier,
|
|
11
|
+
// which is precisely why the root tsconfig `paths` and the Jest mapper resolve
|
|
12
|
+
// it.
|
|
13
|
+
const QUERY_KEYS_MODULE = 'src/util/routing/queryKeys';
|
|
14
|
+
const QUERY_KEYS_SUFFIX = 'util/routing/queryKeys';
|
|
15
|
+
const SRC_TIER_SEGMENT = '/src/';
|
|
6
16
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
17
|
+
* An `@/`-aliased specifier resolves under none of tsc, webpack or Jest, so it
|
|
18
|
+
* is never emitted; it stays recognized because a consumer that does declare
|
|
19
|
+
* the alias must still have its existing imports understood.
|
|
10
20
|
*/
|
|
11
|
-
const
|
|
21
|
+
const ALIASED_QUERY_KEYS_MODULE = '@/util/routing/queryKeys';
|
|
22
|
+
const toPosixPath = (filePath) => filePath.replace(/\\/g, '/');
|
|
23
|
+
const ensureRelativeSpecifier = (specifier) => specifier.startsWith('.') ? specifier : `./${specifier}`;
|
|
24
|
+
const isWindowsDrivePath = (filePath) => /^[A-Za-z]:[\\/]/.test(filePath);
|
|
25
|
+
const isValidRelativePath = (relativePath) => relativePath !== '' &&
|
|
26
|
+
!path_1.default.isAbsolute(relativePath) &&
|
|
27
|
+
!isWindowsDrivePath(relativePath);
|
|
28
|
+
const toAbsoluteFilename = (sourceFilePath, cwd) => toPosixPath(path_1.default.isAbsolute(sourceFilePath)
|
|
29
|
+
? sourceFilePath
|
|
30
|
+
: path_1.default.join(cwd, sourceFilePath));
|
|
31
|
+
/**
|
|
32
|
+
* `queryKeys.ts` lives at `src/util/routing/queryKeys.ts` and is reachable by
|
|
33
|
+
* exactly two forms: a relative path, and the bare `src/…` specifier that the
|
|
34
|
+
* root tsconfig `paths` and the Jest `moduleNameMapper` both resolve. A
|
|
35
|
+
* hardcoded `@/`-aliased specifier therefore turns every fix into a broken
|
|
36
|
+
* import (#1391).
|
|
37
|
+
*
|
|
38
|
+
* Files under a `src/` segment take the relative form, which dominates the
|
|
39
|
+
* codebase and stays correct even where `paths` are unavailable; the `../`
|
|
40
|
+
* count comes from the file's own depth below the root that owns its `src/`
|
|
41
|
+
* segment. Returns null when no correct specifier exists, which makes the
|
|
42
|
+
* caller decline the fix rather than write an import that cannot resolve.
|
|
43
|
+
*/
|
|
44
|
+
function buildQueryKeysSpecifier(sourceFilePath, cwd) {
|
|
45
|
+
const absoluteFilename = toAbsoluteFilename(sourceFilePath, cwd);
|
|
46
|
+
const tierIndex = absoluteFilename.indexOf(SRC_TIER_SEGMENT);
|
|
47
|
+
if (tierIndex === -1) {
|
|
48
|
+
return QUERY_KEYS_MODULE;
|
|
49
|
+
}
|
|
50
|
+
// The project root is everything up to and including the separator that
|
|
51
|
+
// precedes the file's own `src/` segment.
|
|
52
|
+
const projectRoot = absoluteFilename.slice(0, tierIndex + 1);
|
|
53
|
+
const targetPath = path_1.default.join(projectRoot, QUERY_KEYS_MODULE);
|
|
54
|
+
const relativePath = path_1.default.relative(path_1.default.dirname(absoluteFilename), targetPath);
|
|
55
|
+
if (!isValidRelativePath(relativePath)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return ensureRelativeSpecifier(toPosixPath(relativePath));
|
|
59
|
+
}
|
|
12
60
|
/**
|
|
13
61
|
* Rule to enforce the use of centralized router state key constants imported from
|
|
14
62
|
* `src/util/routing/queryKeys.ts` instead of arbitrary string literals when calling
|
|
@@ -25,18 +73,21 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
25
73
|
fixable: 'code',
|
|
26
74
|
schema: [],
|
|
27
75
|
messages: {
|
|
28
|
-
enforceQueryKeyImport: 'Router state key must come from queryKeys.ts (e.g., "
|
|
76
|
+
enforceQueryKeyImport: 'Router state key must come from queryKeys.ts (e.g., "src/util/routing/queryKeys" or a relative path to that module). Use a QUERY_KEY_* constant instead of string literals.',
|
|
29
77
|
enforceQueryKeyConstant: 'Router state key must use a QUERY_KEY_* constant from queryKeys.ts. Variable "{{variableName}}" is not imported from the correct source.',
|
|
30
78
|
},
|
|
31
79
|
},
|
|
32
80
|
defaultOptions: [],
|
|
33
81
|
create(context) {
|
|
82
|
+
const cwd = typeof context.getCwd === 'function' ? context.getCwd() : process.cwd();
|
|
83
|
+
const absoluteFilename = toAbsoluteFilename(context.getFilename(), cwd);
|
|
84
|
+
const queryKeysSpecifier = buildQueryKeysSpecifier(context.getFilename(), cwd);
|
|
34
85
|
// Track imports from queryKeys.ts
|
|
35
86
|
const queryKeyImports = new Map();
|
|
36
87
|
const localUseRouterStateNames = new Set(['useRouterState']);
|
|
37
88
|
const validQueryKeySources = new Set([
|
|
38
|
-
|
|
39
|
-
|
|
89
|
+
ALIASED_QUERY_KEYS_MODULE,
|
|
90
|
+
QUERY_KEYS_MODULE,
|
|
40
91
|
]);
|
|
41
92
|
const allowedQueryKeyFactories = new Set(['makeQueryKey', 'getQueryKey']);
|
|
42
93
|
const sourceCode = context.getSourceCode();
|
|
@@ -60,8 +111,21 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
60
111
|
* Check if a source path refers to queryKeys.ts
|
|
61
112
|
*/
|
|
62
113
|
function isQueryKeysSource(source) {
|
|
63
|
-
|
|
64
|
-
source.endsWith(
|
|
114
|
+
if (validQueryKeySources.has(source) ||
|
|
115
|
+
source.endsWith(`/${QUERY_KEYS_SUFFIX}`)) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
// A relative specifier can name the module without spelling out
|
|
119
|
+
// `util/routing`: a sibling reaches it as `./queryKeys`, and a file two
|
|
120
|
+
// directories below `src/util` as `../../routing/queryKeys`. Resolving
|
|
121
|
+
// against the linted file recognizes those, which also keeps the fix's own
|
|
122
|
+
// relative output recognized on the next pass so a second violation
|
|
123
|
+
// extends that import instead of duplicating it.
|
|
124
|
+
if (!source.startsWith('.')) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const resolved = toPosixPath(path_1.default.resolve(path_1.default.dirname(absoluteFilename), source));
|
|
128
|
+
return resolved.endsWith(`/${QUERY_KEYS_SUFFIX}`);
|
|
65
129
|
}
|
|
66
130
|
function importDeclarationsOf() {
|
|
67
131
|
return sourceCode.ast.body.filter((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
@@ -120,13 +184,28 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
120
184
|
}
|
|
121
185
|
return 'bound';
|
|
122
186
|
}
|
|
187
|
+
function queryKeysDeclarationsOf() {
|
|
188
|
+
return importDeclarationsOf().filter((declaration) => isQueryKeysSource(String(declaration.source.value)));
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The path by which this file reaches queryKeys.ts. An existing declaration
|
|
192
|
+
* is proof of a path that resolves here — including an `@/` one, in a
|
|
193
|
+
* consumer that declares that alias — so it wins over anything derived.
|
|
194
|
+
* Null means the module is unreachable by any specifier this rule can write.
|
|
195
|
+
*/
|
|
196
|
+
function importSourceOf() {
|
|
197
|
+
const [declaration] = queryKeysDeclarationsOf();
|
|
198
|
+
return declaration
|
|
199
|
+
? String(declaration.source.value)
|
|
200
|
+
: queryKeysSpecifier;
|
|
201
|
+
}
|
|
123
202
|
/**
|
|
124
203
|
* Make the substituted constants resolve: extend the file's queryKeys import
|
|
125
204
|
* when there is one to extend, otherwise add a fresh import statement.
|
|
126
205
|
*/
|
|
127
206
|
function buildImportFix(fixer, constants) {
|
|
128
207
|
const importDeclarations = importDeclarationsOf();
|
|
129
|
-
const queryKeysDeclarations =
|
|
208
|
+
const queryKeysDeclarations = queryKeysDeclarationsOf();
|
|
130
209
|
const reusable = queryKeysDeclarations.find((declaration) => declaration.importKind !== 'type' &&
|
|
131
210
|
declaration.specifiers.some(isValueImportSpecifier));
|
|
132
211
|
if (reusable) {
|
|
@@ -136,9 +215,10 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
136
215
|
}
|
|
137
216
|
// A namespace or type-only queryKeys import cannot take named value
|
|
138
217
|
// specifiers, but its path is proof of how this file reaches the module.
|
|
139
|
-
const source =
|
|
140
|
-
|
|
141
|
-
|
|
218
|
+
const source = importSourceOf();
|
|
219
|
+
if (source === null) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
142
222
|
const importText = `import { ${constants.join(', ')} } from '${source}';\n`;
|
|
143
223
|
const [firstImport] = importDeclarations;
|
|
144
224
|
if (firstImport) {
|
|
@@ -152,6 +232,7 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
152
232
|
function flushReports() {
|
|
153
233
|
const resolutions = new Map();
|
|
154
234
|
const missingConstants = [];
|
|
235
|
+
const canImport = importSourceOf() !== null;
|
|
155
236
|
for (const report of pendingReports) {
|
|
156
237
|
if (!report.substitution) {
|
|
157
238
|
continue;
|
|
@@ -159,6 +240,12 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
159
240
|
const { constant, scope } = report.substitution;
|
|
160
241
|
const name = localNameOf(constant);
|
|
161
242
|
const state = resolveBinding(scope, name);
|
|
243
|
+
// Substituting a constant whose import cannot be written would leave
|
|
244
|
+
// the file referencing an undefined identifier, which is worse than the
|
|
245
|
+
// literal it replaced; leaving the report unresolved declines its fix.
|
|
246
|
+
if (state === 'missing' && !canImport) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
162
249
|
resolutions.set(report, { name, state });
|
|
163
250
|
if (state === 'missing' && !missingConstants.includes(name)) {
|
|
164
251
|
missingConstants.push(name);
|
|
@@ -188,7 +275,11 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
188
275
|
fixer.replaceText(substitution.keyNode, resolution.name),
|
|
189
276
|
];
|
|
190
277
|
if (report === importCarrier && missingConstants.length > 0) {
|
|
191
|
-
|
|
278
|
+
const importFix = buildImportFix(fixer, missingConstants);
|
|
279
|
+
if (!importFix) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
fixes.unshift(importFix);
|
|
192
283
|
}
|
|
193
284
|
return fixes;
|
|
194
285
|
},
|
|
@@ -1,8 +1,56 @@
|
|
|
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.preferGlobalRouterStateKey = 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");
|
|
10
|
+
// The module's path below the project root doubles as the bare specifier,
|
|
11
|
+
// which is precisely why the root tsconfig `paths` and the Jest mapper resolve
|
|
12
|
+
// it.
|
|
13
|
+
const QUERY_KEYS_MODULE = 'src/util/routing/queryKeys';
|
|
14
|
+
const QUERY_KEYS_SUFFIX = 'util/routing/queryKeys';
|
|
15
|
+
const SRC_TIER_SEGMENT = '/src/';
|
|
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
|
+
const toAbsoluteFilename = (sourceFilePath, cwd) => toPosixPath(path_1.default.isAbsolute(sourceFilePath)
|
|
23
|
+
? sourceFilePath
|
|
24
|
+
: path_1.default.join(cwd, sourceFilePath));
|
|
25
|
+
/**
|
|
26
|
+
* `queryKeys.ts` lives at `src/util/routing/queryKeys.ts` and is reachable by
|
|
27
|
+
* exactly two forms: a relative path, and the bare `src/…` specifier that the
|
|
28
|
+
* root tsconfig `paths` and the Jest `moduleNameMapper` both resolve. An
|
|
29
|
+
* `@/`-aliased specifier resolves under none of tsc, webpack or Jest, so a
|
|
30
|
+
* hardcoded one turns every fix into a broken import (#1390).
|
|
31
|
+
*
|
|
32
|
+
* Files under a `src/` segment take the relative form, which dominates the
|
|
33
|
+
* codebase and stays correct even where `paths` are unavailable; the `../`
|
|
34
|
+
* count comes from the file's own depth below the root that owns its `src/`
|
|
35
|
+
* segment. Returns null when no correct specifier exists, which makes the
|
|
36
|
+
* caller decline the fix rather than write an import that cannot resolve.
|
|
37
|
+
*/
|
|
38
|
+
function buildQueryKeysSpecifier(sourceFilePath, cwd) {
|
|
39
|
+
const absoluteFilename = toAbsoluteFilename(sourceFilePath, cwd);
|
|
40
|
+
const tierIndex = absoluteFilename.indexOf(SRC_TIER_SEGMENT);
|
|
41
|
+
if (tierIndex === -1) {
|
|
42
|
+
return QUERY_KEYS_MODULE;
|
|
43
|
+
}
|
|
44
|
+
// The project root is everything up to and including the separator that
|
|
45
|
+
// precedes the file's own `src/` segment.
|
|
46
|
+
const projectRoot = absoluteFilename.slice(0, tierIndex + 1);
|
|
47
|
+
const targetPath = path_1.default.join(projectRoot, QUERY_KEYS_MODULE);
|
|
48
|
+
const relativePath = path_1.default.relative(path_1.default.dirname(absoluteFilename), targetPath);
|
|
49
|
+
if (!isValidRelativePath(relativePath)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return ensureRelativeSpecifier(toPosixPath(relativePath));
|
|
53
|
+
}
|
|
6
54
|
/**
|
|
7
55
|
* Rule to enforce the use of centralized router state key constants imported from
|
|
8
56
|
* `src/util/routing/queryKeys.ts` instead of arbitrary string literals when calling
|
|
@@ -19,13 +67,16 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
|
|
|
19
67
|
fixable: 'code',
|
|
20
68
|
schema: [],
|
|
21
69
|
messages: {
|
|
22
|
-
preferGlobalRouterStateKey: 'Router state key {{keyValue}} is a string literal. String literals bypass the shared queryKeys.ts QUERY_KEY_* constants, which leads to duplicate router cache entries and makes allowed keys hard to discover. Import the corresponding QUERY_KEY_* constant from "
|
|
23
|
-
invalidQueryKeySource: 'Router state key variable "{{variableName}}" is not sourced from queryKeys.ts. useRouterState keys must come from QUERY_KEY_* exports so routing cache keys stay stable and traceable. Import the matching constant from "
|
|
70
|
+
preferGlobalRouterStateKey: 'Router state key {{keyValue}} is a string literal. String literals bypass the shared queryKeys.ts QUERY_KEY_* constants, which leads to duplicate router cache entries and makes allowed keys hard to discover. Import the corresponding QUERY_KEY_* constant from "src/util/routing/queryKeys" (a relative path to that module, or an approved re-export) and pass that to useRouterState instead.',
|
|
71
|
+
invalidQueryKeySource: 'Router state key variable "{{variableName}}" is not sourced from queryKeys.ts. useRouterState keys must come from QUERY_KEY_* exports so routing cache keys stay stable and traceable. Import the matching constant from "src/util/routing/queryKeys" (a relative path to that module, or an approved re-export) and use that value here instead of {{variableName}}.',
|
|
24
72
|
},
|
|
25
73
|
},
|
|
26
74
|
defaultOptions: [],
|
|
27
75
|
create(context) {
|
|
28
76
|
const sourceCode = context.sourceCode;
|
|
77
|
+
const cwd = typeof context.getCwd === 'function' ? context.getCwd() : process.cwd();
|
|
78
|
+
const absoluteFilename = toAbsoluteFilename(context.getFilename(), cwd);
|
|
79
|
+
const queryKeysSpecifier = buildQueryKeysSpecifier(context.getFilename(), cwd);
|
|
29
80
|
// Prevent duplicate import insertions when multiple fixes target the same query key constant.
|
|
30
81
|
const scheduledQueryKeyNamedImports = new Set();
|
|
31
82
|
// Track imports from queryKeys.ts
|
|
@@ -48,8 +99,21 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
|
|
|
48
99
|
const normalized = source
|
|
49
100
|
.replace(/^@\/|^src\//, '')
|
|
50
101
|
.replace(/^(\.\/|\.\.\/)+/, '');
|
|
51
|
-
|
|
52
|
-
normalized.endsWith(
|
|
102
|
+
if (validQueryKeySources.has(normalized) ||
|
|
103
|
+
normalized.endsWith(QUERY_KEYS_SUFFIX)) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
// A relative specifier can name the module without spelling out
|
|
107
|
+
// `util/routing`: a sibling reaches it as `./queryKeys`, and a file two
|
|
108
|
+
// directories below `src/util` as `../../routing/queryKeys`. Resolving
|
|
109
|
+
// against the linted file recognizes those, which also keeps the fix's own
|
|
110
|
+
// relative output recognized on the next pass so a second violation
|
|
111
|
+
// extends that import instead of duplicating it.
|
|
112
|
+
if (!source.startsWith('.')) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
const resolved = toPosixPath(path_1.default.resolve(path_1.default.dirname(absoluteFilename), source));
|
|
116
|
+
return resolved.endsWith(`/${QUERY_KEYS_SUFFIX}`);
|
|
53
117
|
}
|
|
54
118
|
/**
|
|
55
119
|
* Check if an identifier is a valid QUERY_KEY constant
|
|
@@ -284,7 +348,9 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
|
|
|
284
348
|
if (scheduledQueryKeyNamedImports.has(suggestedConstant)) {
|
|
285
349
|
return fixes;
|
|
286
350
|
}
|
|
287
|
-
const importText =
|
|
351
|
+
const importText = queryKeysSpecifier === null
|
|
352
|
+
? null
|
|
353
|
+
: `import { ${suggestedConstant} } from '${queryKeysSpecifier}';\n`;
|
|
288
354
|
const queryKeysNamedImport = sourceCode.ast.body.find((n) => n.type ===
|
|
289
355
|
utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
290
356
|
n.importKind !== 'type' &&
|
|
@@ -304,6 +370,13 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
|
|
|
304
370
|
const lastSpecifier = importSpecifiers[importSpecifiers.length - 1];
|
|
305
371
|
fixes.push(fixer.insertTextAfter(lastSpecifier, `, ${suggestedConstant}`));
|
|
306
372
|
}
|
|
373
|
+
else if (importText === null) {
|
|
374
|
+
// Extending an existing named import needs no
|
|
375
|
+
// specifier of its own, so only a freshly
|
|
376
|
+
// written import statement depends on one being
|
|
377
|
+
// derivable.
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
307
380
|
else if (sideEffectImport) {
|
|
308
381
|
fixes.push(fixer.replaceText(sideEffectImport, importText.trimEnd()));
|
|
309
382
|
}
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.20",
|
|
4
|
+
"date": "2026-07-29T19:25:11.467Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-querykey-ts",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1391
|
|
11
|
+
],
|
|
12
|
+
"summary": "derive the inserted queryKeys import specifier (closes #1391)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "prefer-global-router-state-key",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1390
|
|
19
|
+
],
|
|
20
|
+
"summary": "derive queryKeys import specifier from file path (closes #1390)"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
2
24
|
{
|
|
3
25
|
"version": "1.20.19",
|
|
4
26
|
"date": "2026-07-29T18:36:35.802Z",
|