@anzusystems/common-admin 1.47.0-beta.dev-1790143991 → 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/dist/AFormRemoteAutocomplete.vue_vue_type_script_setup_true_lang-DAnD88NB.mjs +10084 -0
- package/dist/AFormRemoteAutocomplete.vue_vue_type_script_setup_true_lang-DAnD88NB.mjs.map +1 -0
- package/dist/common-admin.css +1 -2
- package/dist/common-admin.d.ts +852 -2540
- package/dist/common-admin.js +23908 -44939
- package/dist/common-admin.js.map +1 -1
- package/dist/index-2jGSo7Ju.mjs +22165 -0
- package/dist/index-2jGSo7Ju.mjs.map +1 -0
- package/dist/labs.d.ts +122 -3311
- package/dist/labs.js +3518 -7244
- package/dist/labs.js.map +1 -1
- package/dist/webfontloader-CZqOgyx0.mjs +454 -0
- package/dist/webfontloader-CZqOgyx0.mjs.map +1 -0
- package/package.json +52 -58
- package/src/eslint/plugin.mjs +47 -294
- package/dist/AFormRemoteAutocomplete-BIW7Twfr.mjs +0 -10407
- package/dist/AFormRemoteAutocomplete-BIW7Twfr.mjs.map +0 -1
- package/dist/common-admin.css.d.ts +0 -6
- package/dist/esm-Dvz_E-iW.mjs +0 -18225
- package/dist/esm-Dvz_E-iW.mjs.map +0 -1
- package/dist/playground/cropper-landscape.png +0 -0
- package/dist/playground/cropper-portrait.png +0 -0
- package/dist/playground/cropper-small.png +0 -0
- package/dist/playground/cropper-wide.png +0 -0
- package/dist/webfontloader-BVmLxgC5.mjs +0 -485
- package/dist/webfontloader-BVmLxgC5.mjs.map +0 -1
- package/src/eslint/oxlint.mjs +0 -30
- package/src/eslint/plugin.d.mts +0 -20
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.
|
|
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": "
|
|
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.
|
|
43
|
-
"build:dev": "vite build --minify false --sourcemap true --config vite.config.lib.
|
|
44
|
-
"lib:build": "
|
|
45
|
-
"ci": "run-
|
|
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
|
|
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
|
|
51
|
-
"lint:eslint:fix": "eslint --fix
|
|
52
|
-
"lint:oxlint": "oxlint .
|
|
53
|
-
"lint:oxlint:fix": "oxlint . --fix
|
|
54
|
-
"lint:stylelint": "stylelint src/**/*.{scss,vue}
|
|
55
|
-
"lint:stylelint:fix": "stylelint src/**/*.{scss,vue} --fix
|
|
56
|
-
"format": "oxfmt
|
|
57
|
-
"format:check": "oxfmt
|
|
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:
|
|
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.
|
|
70
|
-
"@sentry/vue": "^10.
|
|
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.
|
|
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.
|
|
78
|
-
"@vitest/browser": "^
|
|
79
|
-
"@vitest/browser-playwright": "^
|
|
80
|
-
"@vitest/ui": "^
|
|
81
|
-
"@vue/compiler-dom": "3.5.
|
|
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.
|
|
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": "
|
|
90
|
-
"@vueuse/integrations": "
|
|
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.
|
|
96
|
-
"eslint-plugin-oxlint": "1.
|
|
97
|
-
"eslint-plugin-vue": "10.
|
|
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.
|
|
101
|
-
"oxlint": "1.
|
|
94
|
+
"oxfmt": "^0.65.0",
|
|
95
|
+
"oxlint": "1.80.0",
|
|
102
96
|
"pinia": "4.0.3",
|
|
103
|
-
"playwright": "^1.
|
|
104
|
-
"postcss": "^8.5.
|
|
97
|
+
"playwright": "^1.62.1",
|
|
98
|
+
"postcss": "^8.5.26",
|
|
105
99
|
"postcss-html": "^2.0.0",
|
|
106
|
-
"postcss-prefix-selector": "^2.2.
|
|
100
|
+
"postcss-prefix-selector": "^2.2.0",
|
|
107
101
|
"rusha": "^0.8.14",
|
|
108
|
-
"sass": "1.
|
|
102
|
+
"sass": "1.103.1",
|
|
109
103
|
"socket.io-client": "4.8.3",
|
|
110
104
|
"sortablejs": "^1.15.7",
|
|
111
|
-
"stylelint": "17.
|
|
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": "
|
|
117
|
-
"unplugin": "3.
|
|
118
|
-
"unplugin-dts": "^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": "
|
|
114
|
+
"vite": "7.3.6",
|
|
121
115
|
"vite-plugin-vuetify": "^2.1.3",
|
|
122
116
|
"vitepress": "1.6.4",
|
|
123
|
-
"vitest": "^
|
|
124
|
-
"vue": "3.5.
|
|
117
|
+
"vitest": "^4.1.11",
|
|
118
|
+
"vue": "3.5.42",
|
|
125
119
|
"vue-eslint-parser": "^10.4.1",
|
|
126
|
-
"vue-i18n": "11.4.
|
|
127
|
-
"vue-router": "5.3.
|
|
120
|
+
"vue-i18n": "11.4.10",
|
|
121
|
+
"vue-router": "5.3.0",
|
|
128
122
|
"vue-tsc": "3.3.11",
|
|
129
|
-
"vuetify": "4.
|
|
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": "^
|
|
137
|
-
"@vueuse/integrations": "^
|
|
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",
|
package/src/eslint/plugin.mjs
CHANGED
|
@@ -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'
|
|
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'
|
|
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
|
|
306
|
-
|
|
307
|
-
|
|
229
|
+
for (const rule of deprecationRules) {
|
|
230
|
+
const matchPath = rule.path || rule.module
|
|
231
|
+
if (!matchPath || source !== matchPath) continue
|
|
308
232
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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:
|
|
315
|
-
message: `'${
|
|
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 /
|
|
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 =
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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:
|
|
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 ` +
|
|
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) {
|