@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
@@ -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.19',
226
+ version: '1.20.20',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -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
- * The canonical queryKeys module. The rule takes no options and already hard-codes
8
- * this path as what makes a key valid, so the fixer writes the very same path
9
- * instead of guessing one when a file has no queryKeys import to copy from.
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 DEFAULT_QUERY_KEYS_SOURCE = '@/util/routing/queryKeys';
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., "@/util/routing/queryKeys", "src/util/routing/queryKeys", or a relative path ending in "/util/routing/queryKeys"). Use a QUERY_KEY_* constant instead of string literals.',
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
- DEFAULT_QUERY_KEYS_SOURCE,
39
- 'src/util/routing/queryKeys',
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
- return (validQueryKeySources.has(source) ||
64
- source.endsWith('/util/routing/queryKeys'));
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 = importDeclarations.filter((declaration) => isQueryKeysSource(String(declaration.source.value)));
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 = queryKeysDeclarations.length
140
- ? String(queryKeysDeclarations[0].source.value)
141
- : DEFAULT_QUERY_KEYS_SOURCE;
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
- fixes.unshift(buildImportFix(fixer, missingConstants));
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 "@/util/routing/queryKeys" (or its approved re-export) and pass that to useRouterState instead.',
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 "@/util/routing/queryKeys" (or its approved re-export) and use that value here instead of {{variableName}}.',
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
- return (validQueryKeySources.has(normalized) ||
52
- normalized.endsWith('util/routing/queryKeys'));
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 = `import { ${suggestedConstant} } from '@/util/routing/queryKeys';\n`;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.19",
3
+ "version": "1.20.20",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -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",