@blumintinc/eslint-plugin-blumint 1.20.19 → 1.20.21

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.21',
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,41 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
120
184
  }
121
185
  return 'bound';
122
186
  }
187
+ /**
188
+ * A parameter binding holds a different value on every call, so no single
189
+ * `QUERY_KEY_*` constant can stand in for it: a hook that iterates a
190
+ * constant array of keys hands each one to a callback parameter by design
191
+ * (#1393). Reporting such an identifier demands a substitution that does not
192
+ * exist, and the enclosing function is where the caller — not this file —
193
+ * decides which key is passed.
194
+ */
195
+ function isParameterBinding(identifier) {
196
+ const variable = utils_1.ASTUtils.findVariable(scopeOf(identifier), identifier);
197
+ const definition = variable?.defs[0];
198
+ return definition?.type === utils_1.TSESLint.Scope.DefinitionType.Parameter;
199
+ }
200
+ function queryKeysDeclarationsOf() {
201
+ return importDeclarationsOf().filter((declaration) => isQueryKeysSource(String(declaration.source.value)));
202
+ }
203
+ /**
204
+ * The path by which this file reaches queryKeys.ts. An existing declaration
205
+ * is proof of a path that resolves here — including an `@/` one, in a
206
+ * consumer that declares that alias — so it wins over anything derived.
207
+ * Null means the module is unreachable by any specifier this rule can write.
208
+ */
209
+ function importSourceOf() {
210
+ const [declaration] = queryKeysDeclarationsOf();
211
+ return declaration
212
+ ? String(declaration.source.value)
213
+ : queryKeysSpecifier;
214
+ }
123
215
  /**
124
216
  * Make the substituted constants resolve: extend the file's queryKeys import
125
217
  * when there is one to extend, otherwise add a fresh import statement.
126
218
  */
127
219
  function buildImportFix(fixer, constants) {
128
220
  const importDeclarations = importDeclarationsOf();
129
- const queryKeysDeclarations = importDeclarations.filter((declaration) => isQueryKeysSource(String(declaration.source.value)));
221
+ const queryKeysDeclarations = queryKeysDeclarationsOf();
130
222
  const reusable = queryKeysDeclarations.find((declaration) => declaration.importKind !== 'type' &&
131
223
  declaration.specifiers.some(isValueImportSpecifier));
132
224
  if (reusable) {
@@ -136,9 +228,10 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
136
228
  }
137
229
  // A namespace or type-only queryKeys import cannot take named value
138
230
  // 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;
231
+ const source = importSourceOf();
232
+ if (source === null) {
233
+ return null;
234
+ }
142
235
  const importText = `import { ${constants.join(', ')} } from '${source}';\n`;
143
236
  const [firstImport] = importDeclarations;
144
237
  if (firstImport) {
@@ -152,6 +245,7 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
152
245
  function flushReports() {
153
246
  const resolutions = new Map();
154
247
  const missingConstants = [];
248
+ const canImport = importSourceOf() !== null;
155
249
  for (const report of pendingReports) {
156
250
  if (!report.substitution) {
157
251
  continue;
@@ -159,6 +253,12 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
159
253
  const { constant, scope } = report.substitution;
160
254
  const name = localNameOf(constant);
161
255
  const state = resolveBinding(scope, name);
256
+ // Substituting a constant whose import cannot be written would leave
257
+ // the file referencing an undefined identifier, which is worse than the
258
+ // literal it replaced; leaving the report unresolved declines its fix.
259
+ if (state === 'missing' && !canImport) {
260
+ continue;
261
+ }
162
262
  resolutions.set(report, { name, state });
163
263
  if (state === 'missing' && !missingConstants.includes(name)) {
164
264
  missingConstants.push(name);
@@ -188,7 +288,11 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
188
288
  fixer.replaceText(substitution.keyNode, resolution.name),
189
289
  ];
190
290
  if (report === importCarrier && missingConstants.length > 0) {
191
- fixes.unshift(buildImportFix(fixer, missingConstants));
291
+ const importFix = buildImportFix(fixer, missingConstants);
292
+ if (!importFix) {
293
+ return null;
294
+ }
295
+ fixes.unshift(importFix);
192
296
  }
193
297
  return fixes;
194
298
  },
@@ -403,7 +507,8 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
403
507
  : undefined,
404
508
  });
405
509
  }
406
- else if (keyValue.type === utils_1.AST_NODE_TYPES.Identifier) {
510
+ else if (keyValue.type === utils_1.AST_NODE_TYPES.Identifier &&
511
+ !isParameterBinding(keyValue)) {
407
512
  // Report variables that aren't from the correct source
408
513
  pendingReports.push({
409
514
  node: keyValue,
@@ -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
@@ -57,6 +121,28 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
57
121
  function isValidQueryKeyConstant(name) {
58
122
  return name.startsWith('QUERY_KEY_');
59
123
  }
124
+ /**
125
+ * `SourceCode#getScope` supersedes the deprecated `context.getScope`; the
126
+ * fallback keeps the rule working on ESLint versions that predate it.
127
+ */
128
+ function scopeOf(node) {
129
+ const scoped = sourceCode;
130
+ return typeof scoped.getScope === 'function'
131
+ ? scoped.getScope(node)
132
+ : context.getScope();
133
+ }
134
+ /**
135
+ * A parameter binding holds a different value on every call, so no single
136
+ * `QUERY_KEY_*` constant can stand in for it: a hook that iterates a
137
+ * constant array of keys hands each one to a callback parameter by design
138
+ * (#1394). Reporting such an identifier demands a substitution that does not
139
+ * exist, and the caller — not this file — decides which key is passed.
140
+ */
141
+ function isParameterBinding(identifier) {
142
+ const variable = utils_1.ASTUtils.findVariable(scopeOf(identifier), identifier);
143
+ const definition = variable?.defs[0];
144
+ return definition?.type === utils_1.TSESLint.Scope.DefinitionType.Parameter;
145
+ }
60
146
  /**
61
147
  * Check if a node represents a valid query key usage
62
148
  */
@@ -284,7 +370,9 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
284
370
  if (scheduledQueryKeyNamedImports.has(suggestedConstant)) {
285
371
  return fixes;
286
372
  }
287
- const importText = `import { ${suggestedConstant} } from '@/util/routing/queryKeys';\n`;
373
+ const importText = queryKeysSpecifier === null
374
+ ? null
375
+ : `import { ${suggestedConstant} } from '${queryKeysSpecifier}';\n`;
288
376
  const queryKeysNamedImport = sourceCode.ast.body.find((n) => n.type ===
289
377
  utils_1.AST_NODE_TYPES.ImportDeclaration &&
290
378
  n.importKind !== 'type' &&
@@ -304,6 +392,13 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
304
392
  const lastSpecifier = importSpecifiers[importSpecifiers.length - 1];
305
393
  fixes.push(fixer.insertTextAfter(lastSpecifier, `, ${suggestedConstant}`));
306
394
  }
395
+ else if (importText === null) {
396
+ // Extending an existing named import needs no
397
+ // specifier of its own, so only a freshly
398
+ // written import statement depends on one being
399
+ // derivable.
400
+ return null;
401
+ }
307
402
  else if (sideEffectImport) {
308
403
  fixes.push(fixer.replaceText(sideEffectImport, importText.trimEnd()));
309
404
  }
@@ -335,7 +430,8 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
335
430
  },
336
431
  });
337
432
  }
338
- else if (keyValue.type === utils_1.AST_NODE_TYPES.Identifier) {
433
+ else if (keyValue.type === utils_1.AST_NODE_TYPES.Identifier &&
434
+ !isParameterBinding(keyValue)) {
339
435
  context.report({
340
436
  node: keyValue,
341
437
  messageId: 'invalidQueryKeySource',
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.21",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,48 @@
1
1
  [
2
+ {
3
+ "version": "1.20.21",
4
+ "date": "2026-07-29T23:50:58.620Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-querykey-ts",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1393
11
+ ],
12
+ "summary": "exempt parameter bindings from the constant requirement (closes #1393)"
13
+ },
14
+ {
15
+ "name": "prefer-global-router-state-key",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 1394
19
+ ],
20
+ "summary": "exempt parameter bindings from the constant requirement (closes #1394)"
21
+ }
22
+ ]
23
+ },
24
+ {
25
+ "version": "1.20.20",
26
+ "date": "2026-07-29T19:25:11.467Z",
27
+ "rules": [
28
+ {
29
+ "name": "enforce-querykey-ts",
30
+ "changeType": "fix",
31
+ "issues": [
32
+ 1391
33
+ ],
34
+ "summary": "derive the inserted queryKeys import specifier (closes #1391)"
35
+ },
36
+ {
37
+ "name": "prefer-global-router-state-key",
38
+ "changeType": "fix",
39
+ "issues": [
40
+ 1390
41
+ ],
42
+ "summary": "derive queryKeys import specifier from file path (closes #1390)"
43
+ }
44
+ ]
45
+ },
2
46
  {
3
47
  "version": "1.20.19",
4
48
  "date": "2026-07-29T18:36:35.802Z",