@anzusystems/common-admin 1.47.0-beta.dev-1790102667 → 1.49.0

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/package.json CHANGED
@@ -3,28 +3,24 @@
3
3
  "packageManager": "yarn@4.18.0",
4
4
  "files": [
5
5
  "dist",
6
- "src/eslint/**"
6
+ "src/eslint"
7
7
  ],
8
- "module": "./dist/common-admin.js",
8
+ "module": "./dist/common-admin.es.js",
9
9
  "types": "./dist/common-admin.d.ts",
10
10
  "exports": {
11
11
  ".": {
12
12
  "import": "./dist/common-admin.js",
13
13
  "types": "./dist/common-admin.d.ts"
14
14
  },
15
- "./styles": {
16
- "types": "./dist/common-admin.css.d.ts",
17
- "default": "./dist/common-admin.css"
18
- },
15
+ "./styles": "./dist/common-admin.css",
19
16
  "./labs": {
20
17
  "import": "./dist/labs.js",
21
18
  "types": "./dist/labs.d.ts"
22
19
  },
23
20
  "./eslint": "./src/eslint/plugin.mjs",
24
- "./oxlint": "./src/eslint/oxlint.mjs",
25
21
  "./*": "./*"
26
22
  },
27
- "version": "1.47.0-beta.dev-1790102667",
23
+ "version": "1.49.0",
28
24
  "license": "Apache-2.0",
29
25
  "repository": {
30
26
  "type": "git",
@@ -34,31 +30,29 @@
34
30
  "prepare": "node -e \"if (process.env.SKIP_PLAYWRIGHT_INSTALL !== 'true') { require('child_process').execSync('playwright install chromium --with-deps', { stdio: 'inherit' }) }\"",
35
31
  "docs:dev": "vitepress dev docs",
36
32
  "docs:build-only": "vitepress build docs",
37
- "docs:build": "run-s build docs:build-only",
33
+ "docs:build": "yarn build ; yarn docs:build-only",
38
34
  "docs:preview": "vitepress preview docs",
39
35
  "playground:dev": "vite",
40
36
  "playground:build": "vite build",
41
37
  "playground:build:nm": "vite build --minify false",
42
- "build": "vite build --config vite.config.lib.mts",
43
- "build:dev": "vite build --minify false --sourcemap true --config vite.config.lib.mts",
44
- "lib:build": "run-s --print-name lint:tsc build",
45
- "ci": "run-p --aggregate-output --continue-on-error --print-label --silent lint:tsc format:check lint:oxlint lint:eslint lint:stylelint",
38
+ "build": "vite build --config vite.config.lib.ts",
39
+ "build:dev": "vite build --minify false --sourcemap true --config vite.config.lib.ts",
40
+ "lib:build": "vite build --config vite.config.lib.ts",
41
+ "ci": "run-s --print-name lint:tsc lint:oxlint lint:eslint lint:stylelint",
46
42
  "lint": "[ \"$*\" = \"--fix\" ] && yarn lint:fix || yarn ci",
47
- "lint:fix": "run-s --print-name lint:tsc lint:oxlint:fix lint:eslint:fix lint:stylelint:fix format",
43
+ "lint:fix": "run-s --print-name lint:tsc format lint:oxlint:fix lint:eslint:fix lint:stylelint:fix",
48
44
  "lint:tsc": "NODE_OPTIONS=--max-old-space-size=4096 vue-tsc --build",
49
45
  "lint:tsc:test": "NODE_OPTIONS=--max-old-space-size=4096 vue-tsc --build tsconfig.test.json",
50
- "lint:eslint": "eslint --cache --cache-strategy content",
51
- "lint:eslint:fix": "eslint --fix --cache --cache-strategy content",
52
- "lint:oxlint": "oxlint . --disable-nested-config",
53
- "lint:oxlint:fix": "oxlint . --fix --disable-nested-config",
54
- "lint:stylelint": "stylelint src/**/*.{scss,vue} --cache --cache-strategy content",
55
- "lint:stylelint:fix": "stylelint src/**/*.{scss,vue} --fix --cache --cache-strategy content",
56
- "format": "oxfmt --disable-nested-config src vite.config.lib.mts vite.config.mts vitest.config.mts eslint.config.mjs",
57
- "format:check": "oxfmt --disable-nested-config src vite.config.lib.mts vite.config.mts vitest.config.mts eslint.config.mjs --check",
46
+ "lint:eslint": "eslint",
47
+ "lint:eslint:fix": "eslint --fix",
48
+ "lint:oxlint": "oxlint .",
49
+ "lint:oxlint:fix": "oxlint . --fix",
50
+ "lint:stylelint": "stylelint src/**/*.{scss,vue}",
51
+ "lint:stylelint:fix": "stylelint src/**/*.{scss,vue} --fix",
52
+ "format": "oxfmt src/",
53
+ "format:check": "oxfmt src/ --check",
58
54
  "test": "vitest",
59
- "test:browser": "vitest run",
60
- "test:eslint": "vitest run --config vitest.config.node.mts",
61
- "test:run": "run-s --print-name test:browser test:eslint",
55
+ "test:run": "vitest run",
62
56
  "test:ui": "vitest --ui"
63
57
  },
64
58
  "devDependencies": {
@@ -66,75 +60,75 @@
66
60
  "@intlify/unplugin-vue-i18n": "^11.2.5",
67
61
  "@kyvg/vue3-notification": "^3.4.2",
68
62
  "@mdi/font": "^7.4.47",
69
- "@microsoft/api-extractor": "^7.59.2",
70
- "@sentry/vue": "^10.75.2",
63
+ "@microsoft/api-extractor": "^7.59.0",
64
+ "@sentry/vue": "^10.73.0",
71
65
  "@shikijs/vitepress-twoslash": "^4.4.3",
66
+ "@stylistic/eslint-plugin": "^5.10.0",
72
67
  "@tsconfig/node22": "^22.0.6",
73
- "@types/node": "^24.13.6",
68
+ "@types/node": "^24.13.3",
74
69
  "@types/rusha": "^0.8.3",
75
70
  "@types/sortablejs": "^1.15.9",
76
71
  "@types/webfontloader": "^1.6.38",
77
- "@vitejs/plugin-vue": "^6.0.9",
78
- "@vitest/browser": "^5.0.1",
79
- "@vitest/browser-playwright": "^5.0.1",
80
- "@vitest/ui": "^5.0.1",
81
- "@vue/compiler-dom": "3.5.43",
72
+ "@vitejs/plugin-vue": "^6.0.8",
73
+ "@vitest/browser": "^4.1.11",
74
+ "@vitest/browser-playwright": "^4.1.11",
75
+ "@vitest/ui": "^4.1.11",
76
+ "@vue/compiler-dom": "3.5.42",
82
77
  "@vue/devtools-api": "^8.2.1",
83
78
  "@vue/eslint-config-typescript": "^14.9.0",
84
79
  "@vue/language-server": "3.3.11",
85
- "@vue/test-utils": "^2.5.1",
80
+ "@vue/test-utils": "^2.5.0",
86
81
  "@vue/tsconfig": "0.9.1",
87
82
  "@vuelidate/core": "^2.0.3",
88
83
  "@vuelidate/validators": "^2.0.4",
89
- "@vueuse/core": "15.0.0",
90
- "@vueuse/integrations": "15.0.0",
84
+ "@vueuse/core": "14.4.0",
85
+ "@vueuse/integrations": "14.4.0",
91
86
  "axios": "1.20.0",
92
87
  "cropperjs": "^1.6.3",
93
- "cropperjs2": "npm:cropperjs@2.2.0",
94
88
  "dayjs": "1.11.23",
95
- "eslint": "^10.11.0",
96
- "eslint-plugin-oxlint": "1.85.0",
97
- "eslint-plugin-vue": "10.11.0",
89
+ "eslint": "^10.9.1",
90
+ "eslint-plugin-oxlint": "1.80.0",
91
+ "eslint-plugin-vue": "10.10.0",
98
92
  "eslint-plugin-vuetify": "^2.7.2",
99
93
  "npm-run-all2": "^9.0.3",
100
- "oxfmt": "^0.70.0",
101
- "oxlint": "1.85.0",
94
+ "oxfmt": "^0.65.0",
95
+ "oxlint": "1.80.0",
102
96
  "pinia": "4.0.3",
103
- "playwright": "^1.63.0",
104
- "postcss": "^8.5.28",
97
+ "playwright": "^1.62.1",
98
+ "postcss": "^8.5.26",
105
99
  "postcss-html": "^2.0.0",
106
- "postcss-prefix-selector": "^2.2.1",
100
+ "postcss-prefix-selector": "^2.2.0",
107
101
  "rusha": "^0.8.14",
108
- "sass": "1.105.0",
102
+ "sass": "1.103.1",
109
103
  "socket.io-client": "4.8.3",
110
104
  "sortablejs": "^1.15.7",
111
- "stylelint": "17.15.0",
105
+ "stylelint": "17.14.1",
112
106
  "stylelint-config-html": "^2.0.0",
113
107
  "stylelint-config-recommended": "^18.0.0",
114
108
  "stylelint-config-recommended-vue": "^2.0.0",
115
109
  "stylelint-config-standard-scss": "^17.0.0",
116
- "typescript": "~6.0.3",
117
- "unplugin": "3.4.0",
118
- "unplugin-dts": "^1.1.1",
110
+ "typescript": "5.9.3",
111
+ "unplugin": "3.3.0",
112
+ "unplugin-dts": "^1.1.0",
119
113
  "uuid": "^14.0.2",
120
- "vite": "8.3.0",
114
+ "vite": "7.3.6",
121
115
  "vite-plugin-vuetify": "^2.1.3",
122
116
  "vitepress": "1.6.4",
123
- "vitest": "^5.0.1",
124
- "vue": "3.5.43",
117
+ "vitest": "^4.1.11",
118
+ "vue": "3.5.42",
125
119
  "vue-eslint-parser": "^10.4.1",
126
- "vue-i18n": "11.4.12",
127
- "vue-router": "5.3.1",
120
+ "vue-i18n": "11.4.10",
121
+ "vue-router": "5.3.0",
128
122
  "vue-tsc": "3.3.11",
129
- "vuetify": "4.2.1",
123
+ "vuetify": "4.1.12",
130
124
  "webfontloader": "^1.6.28"
131
125
  },
132
126
  "peerDependencies": {
133
127
  "@sentry/vue": "^10.0.0",
134
128
  "@vuelidate/core": "^2.0.3",
135
129
  "@vuelidate/validators": "^2.0.4",
136
- "@vueuse/core": "^15.0.0",
137
- "@vueuse/integrations": "^15.0.0",
130
+ "@vueuse/core": "^14.1.0",
131
+ "@vueuse/integrations": "^14.1.0",
138
132
  "axios": "^1.13.4",
139
133
  "dayjs": "^1.11.19",
140
134
  "pinia": "^4.0.2",
@@ -21,13 +21,6 @@ const DEFAULT_DEPRECATED_IMPORTS = [
21
21
  'Pagination',
22
22
  'makeFilterHelper',
23
23
  'apiFetchList',
24
- 'apiGenerateListQuery',
25
- 'apiFetchListBatch',
26
- 'usePaginationAutoHide',
27
- 'fetchDamUserList',
28
- 'fetchDamExtSystemList',
29
- 'fetchDamAssetLicenceList',
30
- 'fetchDamAssetLicenceGroupList',
31
24
  'FilterBag',
32
25
  'Filter',
33
26
  'apiFetchByIds',
@@ -41,39 +34,15 @@ const DEFAULT_DEPRECATED_IMPORTS = [
41
34
  const DEFAULT_INTERNAL_DEPRECATED_IMPORTS = [
42
35
  {
43
36
  path: '@/services/api/apiFetchList',
44
- imports: ['apiFetchList', 'apiGenerateListQuery'],
37
+ imports: ['apiFetchList'],
45
38
  },
46
39
  {
47
40
  path: '@/services/api/apiFetchListBatch',
48
41
  imports: ['apiFetchListBatch'],
49
42
  },
50
- {
51
- path: '@/services/api/apiFetchOne',
52
- imports: ['apiFetchOne'],
53
- },
54
- {
55
- path: '@/services/api/apiCreateOne',
56
- imports: ['apiCreateOne'],
57
- },
58
- {
59
- path: '@/services/api/apiUpdateOne',
60
- imports: ['apiUpdateOne'],
61
- },
62
- {
63
- path: '@/services/api/apiDeleteOne',
64
- imports: ['apiDeleteOne'],
65
- },
66
- {
67
- path: '@/services/api/apiFetchByIds',
68
- imports: ['apiFetchByIds'],
69
- },
70
- {
71
- path: '@/services/api/apiAnyRequest',
72
- imports: ['apiAnyRequest'],
73
- },
74
43
  {
75
44
  path: '@/composables/system/pagination',
76
- imports: ['usePagination', 'Pagination', 'usePaginationAutoHide'],
45
+ imports: ['usePagination', 'Pagination'],
77
46
  },
78
47
  {
79
48
  path: '@/composables/filter/filterHelpers',
@@ -153,39 +122,6 @@ const DEFAULT_INTERNAL_DEPRECATED_IMPORTS = [
153
122
  },
154
123
  ]
155
124
 
156
- // Shared by `prefer-api-command` and `prefer-api-fetch-items`: both ask the same question -- is this
157
- // call the `useApiRequest` helper, and what did it say it answers with.
158
- const isHelperSource = (source) =>
159
- typeof source === 'string' && (source.includes('labs/api/useApiRequest') || source.endsWith('common-admin/labs'))
160
-
161
- // Resolved through the scope rather than matched by name. Matching the name fires on anyone
162
- // else's function called `useApiRequest` and on a parameter that shadows the import inside
163
- // one function, and stays silent when the real helper is imported under another name.
164
- const isHelperBinding = (context, node) => {
165
- let scope = context.sourceCode.getScope(node)
166
- while (scope !== null) {
167
- const variable = scope.variables.find((candidate) => candidate.name === node.name)
168
- if (variable) {
169
- return variable.defs.some(
170
- (def) =>
171
- def.type === 'ImportBinding' &&
172
- isHelperSource(def.parent?.source?.value) &&
173
- // The imported name, not the local one: an alias may call it anything, and a
174
- // different export from the same module is a different function.
175
- def.node?.imported?.name === 'useApiRequest'
176
- )
177
- }
178
- scope = scope.upper
179
- }
180
-
181
- return false
182
- }
183
-
184
- const firstTypeArgument = (node) => {
185
- const args = node.typeArguments ?? node.typeParameters
186
- return args?.params?.[0]
187
- }
188
-
189
125
  const anzuPlugin = {
190
126
  rules: {
191
127
  'no-ts-extension': {
@@ -267,52 +203,41 @@ const anzuPlugin = {
267
203
  return rule.path
268
204
  })
269
205
 
270
- // Index rules by the module specifier they match, so one import is
271
- // resolved by a single Map lookup instead of scanning every rule.
272
- const rulesBySource = new Map()
273
- for (const rule of deprecationRules) {
274
- const matchPath = rule.path || rule.module
275
- if (!matchPath) continue
276
- const entry = { matchPath, imports: new Set(rule.imports) }
277
- const existing = rulesBySource.get(matchPath)
278
- if (existing) existing.push(entry)
279
- else rulesBySource.set(matchPath, [entry])
280
- }
281
-
282
- // Skipping depends only on the file name, so resolve it once per file
283
- // instead of once per import declaration.
284
- let fileSkipped = null
285
- const isFileSkipped = () => {
286
- if (fileSkipped !== null) return fileSkipped
287
- const normalizedFilename = context.filename.replace(/\\/g, '/')
288
- fileSkipped =
289
- skipFiles.some((skip) => normalizedFilename.endsWith(skip)) ||
290
- ruleFilePaths.some(
291
- (rulePath) =>
292
- normalizedFilename.endsWith(rulePath + '.ts') ||
293
- normalizedFilename.endsWith(rulePath + '.js') ||
294
- normalizedFilename.endsWith(rulePath + '.vue') ||
295
- normalizedFilename.endsWith(rulePath)
296
- )
297
- return fileSkipped
298
- }
299
-
300
206
  return {
301
207
  ImportDeclaration(node) {
208
+ const filename = context.filename
209
+ const normalizedFilename = filename.replace(/\\/g, '/')
210
+
211
+ // Check manual skip list
212
+ if (skipFiles.some((skip) => normalizedFilename.endsWith(skip))) return
213
+
214
+ // Auto-skip source files of path-based rules
215
+ if (
216
+ ruleFilePaths.some(
217
+ (rulePath) =>
218
+ normalizedFilename.endsWith(rulePath + '.ts') ||
219
+ normalizedFilename.endsWith(rulePath + '.js') ||
220
+ normalizedFilename.endsWith(rulePath + '.vue') ||
221
+ normalizedFilename.endsWith(rulePath),
222
+ )
223
+ )
224
+ return
225
+
302
226
  const source = node.source.value
303
227
  if (typeof source !== 'string') return
304
228
 
305
- const entries = rulesBySource.get(source)
306
- if (!entries) return
307
- if (isFileSkipped()) return
229
+ for (const rule of deprecationRules) {
230
+ const matchPath = rule.path || rule.module
231
+ if (!matchPath || source !== matchPath) continue
308
232
 
309
- for (const { matchPath, imports } of entries) {
310
- for (const spec of node.specifiers) {
311
- if (spec.type !== 'ImportSpecifier') continue
312
- if (!imports.has(spec.imported.name)) continue
233
+ const deprecatedImports = node.specifiers
234
+ .filter((spec) => spec.type === 'ImportSpecifier')
235
+ .filter((spec) => rule.imports.includes(spec.imported.name))
236
+
237
+ for (const importSpec of deprecatedImports) {
313
238
  context.report({
314
- node: spec,
315
- message: `'${spec.imported.name}' from '${matchPath}' is deprecated`,
239
+ node: importSpec,
240
+ message: `'${importSpec.imported.name}' from '${matchPath}' is deprecated`,
316
241
  })
317
242
  }
318
243
  }
@@ -321,170 +246,26 @@ const anzuPlugin = {
321
246
  },
322
247
  },
323
248
 
324
- 'prefer-api-command': {
325
- meta: {
326
- type: 'problem',
327
- docs: {
328
- description:
329
- 'A request that reads nothing back is `useApiCommand`. `useApiRequest` is for an endpoint ' +
330
- 'that always answers with a body, and refuses a response without one.',
331
- },
332
- schema: [],
333
- },
334
- create(context) {
335
- const reportedMethods = new Set(['DELETE', 'delete'])
336
-
337
- // `any` and `never` satisfy the response constraint without meaning anything, and a call
338
- // with no type argument at all infers it -- so the type cannot speak for these three.
339
- const unhelpfulTypeArgument = (node) => {
340
- const first = firstTypeArgument(node)
341
- if (!first) return true
342
-
343
- return first.type === 'TSAnyKeyword' || first.type === 'TSNeverKeyword'
344
- }
345
-
346
- const methodLiteral = (node) => {
347
- const arg = node.arguments[0]
348
- if (!arg || arg.type !== 'ObjectExpression') return null
349
- const property = arg.properties.find(
350
- (candidate) =>
351
- candidate.type === 'Property' &&
352
- !candidate.computed &&
353
- (candidate.key.name ?? candidate.key.value) === 'method'
354
- )
355
- if (!property || property.value.type !== 'Literal') return null
356
-
357
- return property.value.value
358
- }
359
-
360
- return {
361
- CallExpression(node) {
362
- if (node.callee.type !== 'Identifier' || !isHelperBinding(context, node.callee)) return
363
-
364
- const method = methodLiteral(node)
365
- const isDelete = typeof method === 'string' && reportedMethods.has(method)
366
- if (!isDelete && !unhelpfulTypeArgument(node)) return
367
-
368
- context.report({
369
- node,
370
- message: isDelete
371
- ? 'A delete usually reads nothing back: use `useApiCommand`. If this endpoint really answers ' +
372
- 'with the deleted entity, disable this line and say so.'
373
- : 'State what this answers with. `useApiRequest<Entity>` for a body, `useApiCommand` for none, ' +
374
- '`optionalBody: true` for either.',
375
- })
376
- },
377
- }
378
- },
379
- },
380
-
381
- 'prefer-api-fetch-items': {
382
- meta: {
383
- type: 'problem',
384
- docs: {
385
- description:
386
- 'A request whose answer is a list -- a bare array or a list envelope -- is ' +
387
- '`useApiFetchItems`; `useApiRequest` checks nothing about the body it hands back.',
388
- },
389
- schema: [],
390
- },
391
- create(context) {
392
- const isArrayType = (node) => {
393
- if (!node) return false
394
- if (node.type === 'TSArrayType') return true
395
-
396
- return (
397
- node.type === 'TSTypeReference' &&
398
- node.typeName?.type === 'Identifier' &&
399
- (node.typeName.name === 'Array' || node.typeName.name === 'ReadonlyArray') &&
400
- (node.typeArguments ?? node.typeParameters)?.params?.length === 1
401
- )
402
- }
403
-
404
- const envelopeGeneric = (node) => {
405
- if (node.type !== 'TSTypeReference' || node.typeName?.type !== 'Identifier') return false
406
- if (node.typeName.name !== 'ApiResponseList' && node.typeName.name !== 'ApiInfiniteResponseList') {
407
- return false
408
- }
409
- const params = (node.typeArguments ?? node.typeParameters)?.params
410
-
411
- // `ApiResponseList<T>` is `{ totalCount; data: T }`, so only an array in that slot makes
412
- // `data` a list. `ApiResponseList<Item>` is something this helper would refuse.
413
- return params?.length === 1 && isArrayType(params[0])
414
- }
415
-
416
- const envelopeLiteral = (node) => {
417
- if (node.type !== 'TSTypeLiteral') return false
418
-
419
- // Exactly one member, on purpose: a literal that also spells out `hasNextPage` or
420
- // `totalCount` is an author saying they read the metadata, which is a reason to stay on
421
- // `useApiRequest`. The generic form carries no such signal, so it is reported and the one
422
- // legitimate reader takes a disable with a reason.
423
- if (node.members.length !== 1) return false
424
- const member = node.members[0]
425
-
426
- return (
427
- member.type === 'TSPropertySignature' &&
428
- member.computed === false &&
429
- (member.key?.name ?? member.key?.value) === 'data' &&
430
- isArrayType(member.typeAnnotation?.typeAnnotation)
431
- )
432
- }
433
-
434
- return {
435
- CallExpression(node) {
436
- if (node.callee.type !== 'Identifier' || !isHelperBinding(context, node.callee)) return
437
-
438
- // No type argument at all is `prefer-api-command`'s case, not this one.
439
- const first = firstTypeArgument(node)
440
- if (!first) return
441
-
442
- if (isArrayType(first)) {
443
- context.report({
444
- node,
445
- message:
446
- "This answers with a bare array: use `useApiFetchItems` with `shape: 'array'`. If the " +
447
- 'endpoint really answers with something that only looks like a list, disable this line and say so.',
448
- })
449
-
450
- return
451
- }
452
-
453
- if (envelopeGeneric(first) || envelopeLiteral(first)) {
454
- context.report({
455
- node,
456
- message:
457
- 'This answers with a list envelope: use `useApiFetchItems`, or `useApiFetchList` when the ' +
458
- "user drives the paging. If this call reads the envelope's metadata, disable this line and say so.",
459
- })
460
- }
461
- },
462
- }
463
- },
464
- },
465
-
466
249
  'url-params-match-template': {
467
250
  meta: {
468
251
  type: 'problem',
469
252
  docs: {
470
253
  description:
471
254
  'Ensure urlParams keys match the :placeholders declared in urlTemplate ' +
472
- 'for useApiRequest / useApiCommand / useApiFetchList / useApiFetchByIds / ' +
473
- 'useApiFetchItems / useApiFetchListBatch calls.',
255
+ 'for useApiRequest / useApiFetchList / useApiFetchByIds / useApiFetchListBatch calls.',
474
256
  },
475
257
  schema: [],
476
258
  },
477
259
  create(context) {
478
260
  const TARGET_CALLEES = new Set([
479
261
  'useApiRequest',
480
- // The commands are where most of the fleet's `:id` templates are -- every delete is one.
481
- 'useApiCommand',
482
262
  'useApiFetchList',
483
263
  'useApiFetchByIds',
484
- 'useApiFetchItems',
485
264
  'useApiFetchListBatch',
486
265
  ])
487
266
 
267
+ const PLACEHOLDER_RE = /:([a-zA-Z_][\w]*)/g
268
+
488
269
  const getCalleeName = (callee) => {
489
270
  if (callee.type === 'Identifier') return callee.name
490
271
  if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {
@@ -497,7 +278,8 @@ const anzuPlugin = {
497
278
  for (const prop of objectExpr.properties) {
498
279
  if (prop.type !== 'Property' || prop.computed) continue
499
280
  const key = prop.key
500
- const keyName = key.type === 'Identifier' ? key.name : key.type === 'Literal' ? key.value : null
281
+ const keyName =
282
+ key.type === 'Identifier' ? key.name : key.type === 'Literal' ? key.value : null
501
283
  if (keyName === name) return prop
502
284
  }
503
285
  return null
@@ -579,27 +361,11 @@ const anzuPlugin = {
579
361
  const resolved = resolveToString(templateProp.value)
580
362
  if (resolved === null) return
581
363
 
582
- // The path only, because that is all `stringUrlTemplateReplace` substitutes into: it
583
- // splits the template on `?` and rejoins the query untouched. Scanning the query too
584
- // would fail CI on a perfectly good `...?locale=:locale`, and adding `locale` to
585
- // `urlParams` to silence it would change nothing at runtime.
586
- const [path] = resolved.split('?')
587
-
588
- // Exactly what `stringUrlTemplateReplace` does, and nothing else: split the path on `/`,
589
- // skip any part that does not START with `:`, and take everything after that colon as the
590
- // key. Both halves matter and both were got wrong in turn. Hunting for `:name` anywhere
591
- // matches inside `prefix-:id`, which the runtime leaves alone, so the rule demanded a key
592
- // that is never filled in. Requiring the key to look like an identifier misses `:asset-id`,
593
- // which the runtime substitutes happily, so the rule reported the caller's correct
594
- // `urlParams` as having no placeholder to match.
595
364
  const placeholders = new Set()
596
- for (const segment of path.split('/')) {
597
- if (!segment.startsWith(':')) continue
598
- // No guard on the name, because the runtime has none: a bare `:` takes `''` as its key
599
- // and fills the segment in from `params['']`. Nobody writes that on purpose, which is
600
- // the point -- it is a typo, and the rule reporting a missing key is how it gets seen
601
- // rather than shipped as a literal colon in the url.
602
- placeholders.add(segment.slice(1))
365
+ let match
366
+ PLACEHOLDER_RE.lastIndex = 0
367
+ while ((match = PLACEHOLDER_RE.exec(resolved)) !== null) {
368
+ placeholders.add(match[1])
603
369
  }
604
370
 
605
371
  const paramKeys = collectStaticKeys(paramsProp.value)
@@ -611,7 +377,9 @@ const anzuPlugin = {
611
377
  if (!paramKeySet.has(placeholder)) {
612
378
  context.report({
613
379
  node: paramsProp,
614
- message: `urlParams is missing key '${placeholder}' required by urlTemplate ` + `'${resolved}'.`,
380
+ message:
381
+ `urlParams is missing key '${placeholder}' required by urlTemplate ` +
382
+ `'${resolved}'.`,
615
383
  })
616
384
  }
617
385
  }
@@ -621,7 +389,8 @@ const anzuPlugin = {
621
389
  context.report({
622
390
  node: paramsProp,
623
391
  message:
624
- `urlParams key '${key}' has no matching ':${key}' placeholder in urlTemplate ` + `'${resolved}'.`,
392
+ `urlParams key '${key}' has no matching ':${key}' placeholder in urlTemplate ` +
393
+ `'${resolved}'.`,
625
394
  })
626
395
  }
627
396
  }
@@ -655,21 +424,21 @@ const anzuPlugin = {
655
424
  parts.unshift(current)
656
425
 
657
426
  const hasFatalCheck = parts.some(
658
- (part) => part.type === 'CallExpression' && part.callee.name === 'isAnzuFatalError'
427
+ (part) => part.type === 'CallExpression' && part.callee.name === 'isAnzuFatalError',
659
428
  )
660
429
  const hasInstanceofErrorCheck = parts.some(
661
430
  (part) =>
662
431
  part.type === 'BinaryExpression' &&
663
432
  part.operator === 'instanceof' &&
664
433
  part.right.type === 'Identifier' &&
665
- part.right.name === 'Error'
434
+ part.right.name === 'Error',
666
435
  )
667
436
  const hasAxiosCheck = parts.some(
668
437
  (part) =>
669
438
  part.type === 'CallExpression' &&
670
439
  part.callee.type === 'MemberExpression' &&
671
440
  part.callee.object.name === 'axios' &&
672
- part.callee.property.name === 'isAxiosError'
441
+ part.callee.property.name === 'isAxiosError',
673
442
  )
674
443
 
675
444
  if (hasAxiosCheck && (hasFatalCheck || hasInstanceofErrorCheck)) {
@@ -695,8 +464,6 @@ const anzuPlugin = {
695
464
  * @param {boolean|'error'|'warn'|'off'} [options.noTsExtension='error'] - Severity for no-ts-extension rule.
696
465
  * @param {boolean|'error'|'warn'|'off'} [options.noFatalErrorAxiosCheck='error']
697
466
  * - Severity for no-fatal-error-axios-check rule.
698
- * @param {boolean|'error'|'warn'|'off'} [options.preferApiFetchItems='error']
699
- * - Severity for prefer-api-fetch-items rule.
700
467
  * @param {boolean|'error'|'warn'|'off'|Object} [options.deprecatedImports='error'] - Severity or config object.
701
468
  * @param {string[]} [options.deprecatedImports.exclude] - Import names to remove from the default list.
702
469
  * @param {string[]} [options.deprecatedImports.include] - Additional import names to add to the default list.
@@ -712,8 +479,6 @@ export function recommended(options = {}) {
712
479
  const {
713
480
  noTsExtension = 'error',
714
481
  noFatalErrorAxiosCheck = 'error',
715
- preferApiCommand = 'error',
716
- preferApiFetchItems = 'error',
717
482
  urlParamsMatchTemplate = 'error',
718
483
  deprecatedImports = 'error',
719
484
  } = options
@@ -732,18 +497,6 @@ export function recommended(options = {}) {
732
497
  rules['anzu/no-fatal-error-axios-check'] = fatalSeverity
733
498
  }
734
499
 
735
- // prefer-api-command
736
- const preferApiCommandSeverity = normalizeSeverity(preferApiCommand)
737
- if (preferApiCommandSeverity) {
738
- rules['anzu/prefer-api-command'] = preferApiCommandSeverity
739
- }
740
-
741
- // prefer-api-fetch-items
742
- const preferApiFetchItemsSeverity = normalizeSeverity(preferApiFetchItems)
743
- if (preferApiFetchItemsSeverity) {
744
- rules['anzu/prefer-api-fetch-items'] = preferApiFetchItemsSeverity
745
- }
746
-
747
500
  // url-params-match-template
748
501
  const urlParamsSeverity = normalizeSeverity(urlParamsMatchTemplate)
749
502
  if (urlParamsSeverity) {