@diia-inhouse/oxc-config 3.2.0 → 4.0.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/oxfmt.config.d.ts +1 -1
- package/dist/oxfmt.config.js +1 -1
- package/dist/oxlint/config.d.ts.map +1 -1
- package/dist/oxlint/config.js +208 -120
- package/dist/oxlint/plugins/packageCheck.mjs +595 -60
- package/oxlint/plugins/packageCheck.mjs +595 -60
- package/package.json +35 -31
|
@@ -1,84 +1,619 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
2
|
-
import { resolve } from 'node:path'
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join, resolve } from 'node:path'
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
const FIX_FLAGS = new Set(['--fix', '--fix-suggestions', '--fix-dangerously'])
|
|
5
|
+
const DEPENDENCY_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
|
|
6
|
+
const PUBLISH_FIELDS = ['license', 'files', 'publishConfig', 'repository']
|
|
7
|
+
const LEGACY_CONFIG_FIELDS = ['prettier', 'eslintConfig', 'eslintIgnore']
|
|
8
|
+
const LEGACY_PACKAGES = [
|
|
9
|
+
'eslint',
|
|
10
|
+
'typescript-eslint',
|
|
11
|
+
'jsonc-eslint-parser',
|
|
12
|
+
'eslint-config-prettier',
|
|
13
|
+
'eslint-plugin-prettier',
|
|
14
|
+
'@diia-inhouse/eslint-config',
|
|
15
|
+
'prettier',
|
|
16
|
+
'prettier-plugin-sort-imports',
|
|
17
|
+
'@ianvs/prettier-plugin-sort-imports',
|
|
18
|
+
'tsc-watch',
|
|
19
|
+
'ts-node',
|
|
20
|
+
'ts-node-dev',
|
|
21
|
+
'nodemon',
|
|
22
|
+
]
|
|
23
|
+
const LEGACY_SCRIPT_NAMES = new Set(['eslint', 'prettier'])
|
|
24
|
+
const LEGACY_TOOL_PATTERN = /\b(eslint|prettier)\b/
|
|
25
|
+
const SERVICE_NAME_PATTERN = /^service-|-service$/
|
|
26
|
+
const VERSION_RANGE_PATTERN = /^[\^~]/
|
|
27
|
+
const EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
|
|
28
|
+
const INSTALLED_FIELDS = ['dependencies', 'devDependencies', 'optionalDependencies']
|
|
29
|
+
const EMPTY_FIELDS = [
|
|
30
|
+
'keywords',
|
|
31
|
+
'description',
|
|
32
|
+
'author',
|
|
33
|
+
'scripts',
|
|
34
|
+
'dependencies',
|
|
35
|
+
'devDependencies',
|
|
36
|
+
'peerDependencies',
|
|
37
|
+
'optionalDependencies',
|
|
38
|
+
'overrides',
|
|
39
|
+
]
|
|
40
|
+
const DEV_TOOL_PATTERNS = [
|
|
41
|
+
/^@types\//,
|
|
42
|
+
/^vitest(-|$)/,
|
|
43
|
+
/^@vitest\//,
|
|
44
|
+
/^jest(-|$)/,
|
|
45
|
+
/^@jest\//,
|
|
46
|
+
/^oxlint(-|$)/,
|
|
47
|
+
/^oxfmt$/,
|
|
48
|
+
/^eslint(-|$)/,
|
|
49
|
+
/^@eslint\//,
|
|
50
|
+
/^prettier(-|$)/,
|
|
51
|
+
/^typescript$/,
|
|
52
|
+
/^madge$/,
|
|
53
|
+
/^lockfile-lint$/,
|
|
54
|
+
/^semantic-release(-|$)/,
|
|
55
|
+
/^@semantic-release\//,
|
|
56
|
+
/^rimraf$/,
|
|
57
|
+
/^tsc-alias$/,
|
|
58
|
+
/^tsc-watch$/,
|
|
59
|
+
/^@diia-inhouse\/(test|oxc-config|configs|scaffold|eslint-config|eslint-plugin)$/,
|
|
60
|
+
]
|
|
61
|
+
const DEFAULT_NODE_MAJOR = 24
|
|
62
|
+
const DEFAULT_AUTHOR = 'diia-team'
|
|
63
|
+
const DEV_SCRIPT = 'diia-dev'
|
|
64
|
+
const COMMITLINT_CONFIG = '@diia-inhouse/configs/dist/commitlint'
|
|
65
|
+
const MADGE_TS_CONFIG = './tsconfig.json'
|
|
66
|
+
|
|
67
|
+
const isFixRun = () => process.argv.some((arg) => FIX_FLAGS.has(arg))
|
|
68
|
+
|
|
69
|
+
const manifests = new Map()
|
|
70
|
+
const manifestByDir = new Map()
|
|
71
|
+
|
|
72
|
+
function findManifest(file) {
|
|
73
|
+
const seen = []
|
|
74
|
+
let dir = dirname(resolve(file))
|
|
75
|
+
let found = null
|
|
76
|
+
|
|
77
|
+
for (;;) {
|
|
78
|
+
const known = manifestByDir.get(dir)
|
|
79
|
+
|
|
80
|
+
if (known !== undefined) {
|
|
81
|
+
found = known
|
|
82
|
+
break
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
seen.push(dir)
|
|
86
|
+
|
|
87
|
+
const candidate = join(dir, 'package.json')
|
|
88
|
+
const parent = dirname(dir)
|
|
89
|
+
|
|
90
|
+
if (existsSync(candidate)) {
|
|
91
|
+
found = candidate
|
|
92
|
+
break
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (parent === dir) {
|
|
96
|
+
break
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
dir = parent
|
|
100
|
+
}
|
|
6
101
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return pkg
|
|
102
|
+
for (const d of seen) {
|
|
103
|
+
manifestByDir.set(d, found)
|
|
10
104
|
}
|
|
11
105
|
|
|
12
|
-
|
|
106
|
+
return found
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function loadManifest(path) {
|
|
110
|
+
let text
|
|
13
111
|
|
|
14
112
|
try {
|
|
15
|
-
|
|
113
|
+
text = readFileSync(path, 'utf8')
|
|
16
114
|
} catch {
|
|
17
|
-
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const cached = manifests.get(path)
|
|
119
|
+
|
|
120
|
+
if (cached?.text === text) {
|
|
121
|
+
return cached
|
|
18
122
|
}
|
|
19
123
|
|
|
20
|
-
|
|
124
|
+
try {
|
|
125
|
+
const entry = { path, text, data: JSON.parse(text), reported: new Set() }
|
|
126
|
+
|
|
127
|
+
manifests.set(path, entry)
|
|
128
|
+
|
|
129
|
+
return entry
|
|
130
|
+
} catch {
|
|
131
|
+
return null
|
|
132
|
+
}
|
|
21
133
|
}
|
|
22
134
|
|
|
23
|
-
|
|
135
|
+
function writeManifest(entry, data) {
|
|
136
|
+
const indent = /^\{\r?\n([ \t]+)/.exec(entry.text)?.[1] ?? ' '
|
|
137
|
+
const text = `${JSON.stringify(data, null, indent)}${entry.text.endsWith('\n') ? '\n' : ''}`
|
|
24
138
|
|
|
25
|
-
|
|
139
|
+
writeFileSync(entry.path, text)
|
|
140
|
+
entry.text = text
|
|
141
|
+
entry.data = data
|
|
142
|
+
}
|
|
26
143
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
144
|
+
function loadLock(entry) {
|
|
145
|
+
if (entry.lock === undefined) {
|
|
146
|
+
try {
|
|
147
|
+
entry.lock = JSON.parse(readFileSync(join(dirname(entry.path), 'package-lock.json'), 'utf8'))
|
|
148
|
+
} catch {
|
|
149
|
+
entry.lock = null
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return entry.lock
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const installedVersion = (entry, name) => loadLock(entry)?.packages?.[`node_modules/${name}`]?.version
|
|
157
|
+
|
|
158
|
+
const parseVersion = (text) => {
|
|
159
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(text)
|
|
160
|
+
|
|
161
|
+
return match ? match.slice(1, 4).map(Number) : null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const compareVersions = (a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2]
|
|
165
|
+
|
|
166
|
+
const lowestOf = (comparator) => {
|
|
167
|
+
const bare = comparator.replace(/^[\^~]|^>=|^=/, '')
|
|
168
|
+
|
|
169
|
+
if (comparator === '' || comparator === '*' || /^[<x]/i.test(comparator)) {
|
|
170
|
+
return [0, 0, 0]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const match = /^(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?(?:[-+][0-9A-Za-z.+-]*)?$/.exec(bare)
|
|
174
|
+
|
|
175
|
+
return match
|
|
176
|
+
? [match[1], match[2], match[3]].map((part) => (part === undefined || part === 'x' || part === '*' ? 0 : Number(part)))
|
|
177
|
+
: null
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const minVersion = (range) => {
|
|
181
|
+
const alternatives = String(range)
|
|
182
|
+
.split('||')
|
|
183
|
+
.map((alternative) =>
|
|
184
|
+
alternative
|
|
185
|
+
.trim()
|
|
186
|
+
.replace(/\s+-\s+.*$/, '')
|
|
187
|
+
.split(/\s+/)
|
|
188
|
+
.filter(Boolean),
|
|
189
|
+
)
|
|
190
|
+
const mins = alternatives.map((comparators) => {
|
|
191
|
+
const lows = comparators.map(lowestOf)
|
|
192
|
+
|
|
193
|
+
return lows.some((low) => low === null)
|
|
194
|
+
? null
|
|
195
|
+
: lows.reduce((highest, low) => (compareVersions(low, highest) > 0 ? low : highest), [0, 0, 0])
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
return mins.some((min) => min === null) ? null : mins.reduce((lowest, min) => (compareVersions(min, lowest) < 0 ? min : lowest))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const formatVersion = (version) => version.join('.')
|
|
202
|
+
|
|
203
|
+
const allDependencies = (data) => Object.assign({}, ...DEPENDENCY_FIELDS.map((field) => data[field] ?? {}))
|
|
204
|
+
const dependsOn = (data, name) => name in allDependencies(data)
|
|
205
|
+
const isUnscoped = ({ name }) => typeof name === 'string' && !name.startsWith('@')
|
|
206
|
+
const isPrivate = (data) => data.private === true || isUnscoped(data)
|
|
207
|
+
const isDevTool = (name) => DEV_TOOL_PATTERNS.some((pattern) => pattern.test(name))
|
|
208
|
+
const isEmpty = (value) =>
|
|
209
|
+
value === '' ||
|
|
210
|
+
(Array.isArray(value) && value.length === 0) ||
|
|
211
|
+
(typeof value === 'object' && value !== null && !Array.isArray(value) && Object.keys(value).length === 0)
|
|
212
|
+
|
|
213
|
+
export const manifestRules = {
|
|
214
|
+
'no-service-in-package-name': {
|
|
215
|
+
messages: {
|
|
216
|
+
suffix: 'package.json "name" is "{{ name }}": the repository is called <name>-service, the package is called <name>. Not auto-fixed, because the name is the service identity at runtime.',
|
|
217
|
+
},
|
|
218
|
+
check: ({ name }) => (typeof name === 'string' && SERVICE_NAME_PATTERN.test(name) ? [{ messageId: 'suffix', data: { name } }] : []),
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
'pinned-dependencies': {
|
|
222
|
+
messages: {
|
|
223
|
+
unpinned: 'Dependency "{{ name }}" has unpinned version "{{ version }}". Pin to an exact version (remove ^ or ~).',
|
|
224
|
+
inexact:
|
|
225
|
+
'Dependency "{{ name }}" is "{{ version }}", not an exact version from the registry. Not auto-fixed: run `npm i {{ name }}@<version>`.',
|
|
226
|
+
},
|
|
227
|
+
check: (data) => {
|
|
228
|
+
const violations = []
|
|
229
|
+
|
|
230
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
231
|
+
for (const [name, version] of Object.entries(data[field] ?? {})) {
|
|
232
|
+
if (typeof version !== 'string') {
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (VERSION_RANGE_PATTERN.test(version)) {
|
|
237
|
+
violations.push({ messageId: 'unpinned', data: { name, version } })
|
|
238
|
+
} else if (INSTALLED_FIELDS.includes(field) && !EXACT_VERSION_PATTERN.test(version)) {
|
|
239
|
+
violations.push({ messageId: 'inexact', data: { name, version } })
|
|
240
|
+
}
|
|
46
241
|
}
|
|
47
|
-
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return violations
|
|
48
245
|
},
|
|
246
|
+
fix: (data, _options, entry) => {
|
|
247
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
248
|
+
for (const [name, version] of Object.entries(data[field] ?? {})) {
|
|
249
|
+
const installed = typeof version === 'string' && VERSION_RANGE_PATTERN.test(version) && installedVersion(entry, name)
|
|
49
250
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
create(context) {
|
|
58
|
-
return {
|
|
59
|
-
Program(node) {
|
|
60
|
-
const data = loadPackageJson()
|
|
61
|
-
|
|
62
|
-
if (!data) {
|
|
63
|
-
return
|
|
64
|
-
}
|
|
251
|
+
if (installed) {
|
|
252
|
+
data[field][name] = installed
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
65
256
|
|
|
66
|
-
|
|
67
|
-
|
|
257
|
+
return data
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
'unique-dependencies': {
|
|
262
|
+
messages: {
|
|
263
|
+
duplicate:
|
|
264
|
+
'Dependency "{{ name }}" is listed in both {{ first }} and {{ second }}. Not auto-fixed, because package-lock.json must change with it: run `npm uninstall -D {{ name }}` if it is needed at runtime, `npm uninstall {{ name }}` if not.',
|
|
265
|
+
},
|
|
266
|
+
check: (data) => {
|
|
267
|
+
const violations = []
|
|
268
|
+
const seen = new Map()
|
|
269
|
+
|
|
270
|
+
for (const field of INSTALLED_FIELDS) {
|
|
271
|
+
for (const name of Object.keys(data[field] ?? {})) {
|
|
272
|
+
if (seen.has(name)) {
|
|
273
|
+
violations.push({ messageId: 'duplicate', data: { name, first: seen.get(name), second: field } })
|
|
274
|
+
} else {
|
|
275
|
+
seen.set(name, field)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
68
279
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
280
|
+
return violations
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
'no-dev-tooling-in-dependencies': {
|
|
285
|
+
messages: {
|
|
286
|
+
tool: 'Dependency "{{ name }}" is a development tool listed as a runtime dependency. Not auto-fixed, because package-lock.json must change with it: run `npm uninstall {{ name }} && npm i -D {{ name }}`.',
|
|
287
|
+
},
|
|
288
|
+
check: (data) =>
|
|
289
|
+
isPrivate(data)
|
|
290
|
+
? Object.keys(data.dependencies ?? {})
|
|
291
|
+
.filter(isDevTool)
|
|
292
|
+
.map((name) => ({ messageId: 'tool', data: { name } }))
|
|
293
|
+
: [],
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
'private-package': {
|
|
297
|
+
messages: {
|
|
298
|
+
missing: 'An unscoped package ("{{ name }}") can never be published to the registry; say so with "private": true.',
|
|
299
|
+
},
|
|
300
|
+
check: (data) => (isUnscoped(data) && data.private !== true ? [{ messageId: 'missing', data: { name: data.name } }] : []),
|
|
301
|
+
fix: (data) => ({ ...data, private: true }),
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
'no-publish-fields': {
|
|
305
|
+
messages: {
|
|
306
|
+
field: 'package.json "{{ field }}" describes publishing, and this package is private. Remove it.',
|
|
307
|
+
},
|
|
308
|
+
check: (data) =>
|
|
309
|
+
isPrivate(data)
|
|
310
|
+
? PUBLISH_FIELDS.filter((field) => field in data).map((field) => ({ messageId: 'field', data: { field } }))
|
|
311
|
+
: [],
|
|
312
|
+
fix: (data) => {
|
|
313
|
+
for (const field of PUBLISH_FIELDS) {
|
|
314
|
+
delete data[field]
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return data
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
'no-empty-fields': {
|
|
322
|
+
messages: {
|
|
323
|
+
empty: 'package.json "{{ field }}" is empty. Remove it.',
|
|
324
|
+
},
|
|
325
|
+
check: (data) =>
|
|
326
|
+
EMPTY_FIELDS.filter((field) => field in data && isEmpty(data[field])).map((field) => ({ messageId: 'empty', data: { field } })),
|
|
327
|
+
fix: (data) => {
|
|
328
|
+
for (const field of EMPTY_FIELDS) {
|
|
329
|
+
if (field in data && isEmpty(data[field])) {
|
|
330
|
+
delete data[field]
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return data
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
'engines-node': {
|
|
339
|
+
schema: [{ type: 'object', properties: { nodeMajor: { type: 'integer' } }, additionalProperties: false }],
|
|
340
|
+
messages: {
|
|
341
|
+
engines: 'package.json "engines.node" must be "{{ expected }}", the Node.js major CI builds on (found {{ found }}).',
|
|
342
|
+
},
|
|
343
|
+
check: (data, { nodeMajor = DEFAULT_NODE_MAJOR }) => {
|
|
344
|
+
const expected = `>=${nodeMajor}`
|
|
72
345
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
346
|
+
return data.engines?.node === expected
|
|
347
|
+
? []
|
|
348
|
+
: [{ messageId: 'engines', data: { expected, found: data.engines?.node ?? 'nothing' } }]
|
|
349
|
+
},
|
|
350
|
+
fix: (data, { nodeMajor = DEFAULT_NODE_MAJOR }) => ({ ...data, engines: { ...data.engines, node: `>=${nodeMajor}` } }),
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
author: {
|
|
354
|
+
schema: [{ type: 'object', properties: { author: { type: 'string' } }, additionalProperties: false }],
|
|
355
|
+
messages: {
|
|
356
|
+
author: 'package.json "author" must be "{{ expected }}" (found {{ found }}).',
|
|
357
|
+
},
|
|
358
|
+
check: (data, { author = DEFAULT_AUTHOR }) =>
|
|
359
|
+
data.author === author ? [] : [{ messageId: 'author', data: { expected: author, found: data.author ?? 'nothing' } }],
|
|
360
|
+
fix: (data, { author = DEFAULT_AUTHOR }) => ({ ...data, author }),
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
description: {
|
|
364
|
+
messages: {
|
|
365
|
+
missing: 'package.json "description" is empty. Say in one line what this package does.',
|
|
366
|
+
},
|
|
367
|
+
check: ({ description }) => (typeof description === 'string' && description.trim() !== '' ? [] : [{ messageId: 'missing' }]),
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
'no-legacy-tooling': {
|
|
371
|
+
messages: {
|
|
372
|
+
installed:
|
|
373
|
+
'The oxc toolchain replaced {{ names }}. Not auto-fixed, because package-lock.json must change with it: run `npm uninstall {{ command }}`.',
|
|
374
|
+
},
|
|
375
|
+
check: (data) => {
|
|
376
|
+
const installed = LEGACY_PACKAGES.filter((name) => dependsOn(data, name))
|
|
377
|
+
|
|
378
|
+
return installed.length > 0
|
|
379
|
+
? [{ messageId: 'installed', data: { names: installed.join(', '), command: installed.join(' ') } }]
|
|
380
|
+
: []
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
'no-legacy-config-fields': {
|
|
385
|
+
messages: {
|
|
386
|
+
field: 'package.json "{{ field }}" configures a tool the oxc toolchain replaced. Remove it.',
|
|
387
|
+
},
|
|
388
|
+
check: (data) => LEGACY_CONFIG_FIELDS.filter((field) => field in data).map((field) => ({ messageId: 'field', data: { field } })),
|
|
389
|
+
fix: (data) => {
|
|
390
|
+
for (const field of LEGACY_CONFIG_FIELDS) {
|
|
391
|
+
delete data[field]
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return data
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
'no-legacy-scripts': {
|
|
399
|
+
messages: {
|
|
400
|
+
script: 'Script "{{ name }}" runs {{ tool }}, which the oxc toolchain replaced.',
|
|
401
|
+
},
|
|
402
|
+
check: ({ scripts = {} }) => {
|
|
403
|
+
const violations = []
|
|
404
|
+
|
|
405
|
+
for (const [name, body] of Object.entries(scripts)) {
|
|
406
|
+
const tool = LEGACY_SCRIPT_NAMES.has(name) ? name : LEGACY_TOOL_PATTERN.exec(String(body))?.[1]
|
|
407
|
+
|
|
408
|
+
if (tool) {
|
|
409
|
+
violations.push({ messageId: 'script', data: { name, tool } })
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return violations
|
|
414
|
+
},
|
|
415
|
+
fix: (data) => {
|
|
416
|
+
for (const [name, body] of Object.entries(data.scripts ?? {})) {
|
|
417
|
+
if (LEGACY_SCRIPT_NAMES.has(name)) {
|
|
418
|
+
delete data.scripts[name]
|
|
419
|
+
} else {
|
|
420
|
+
data.scripts[name] = String(body)
|
|
421
|
+
.replace(/\bprettier --write\b/g, 'oxfmt')
|
|
422
|
+
.replace(/\bprettier --check\b/g, 'oxfmt --check')
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return data
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
'dev-script': {
|
|
431
|
+
messages: {
|
|
432
|
+
dev: 'Script "dev" must run "{{ expected }}" (found "{{ found }}"); it watches the entry file with the tsx bundled in @diia-inhouse/oxc-config.',
|
|
433
|
+
},
|
|
434
|
+
check: (data) => {
|
|
435
|
+
const dev = data.scripts?.dev
|
|
436
|
+
|
|
437
|
+
return dependsOn(data, '@diia-inhouse/diia-app') && typeof dev === 'string' && !/^diia-dev(\s|$)/.test(dev)
|
|
438
|
+
? [{ messageId: 'dev', data: { expected: DEV_SCRIPT, found: dev } }]
|
|
439
|
+
: []
|
|
440
|
+
},
|
|
441
|
+
fix: (data) => ({ ...data, scripts: { ...data.scripts, dev: DEV_SCRIPT } }),
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
'peers-installed-locally': {
|
|
445
|
+
messages: {
|
|
446
|
+
missing:
|
|
447
|
+
'Peer dependency "{{ name }}" is not installed for this package\'s own tests. Not auto-fixed, because package-lock.json must change with it: run `npm i -D {{ name }}`.',
|
|
448
|
+
},
|
|
449
|
+
check: (data) =>
|
|
450
|
+
isPrivate(data)
|
|
451
|
+
? []
|
|
452
|
+
: Object.keys(data.peerDependencies ?? {})
|
|
453
|
+
.filter((name) => !(name in (data.devDependencies ?? {})) && !(name in (data.dependencies ?? {})))
|
|
454
|
+
.map((name) => ({ messageId: 'missing', data: { name } })),
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
'exports-package-json': {
|
|
458
|
+
messages: {
|
|
459
|
+
missing:
|
|
460
|
+
'package.json "exports" does not expose "./package.json", so nothing can read this package\'s manifest through the package.',
|
|
461
|
+
},
|
|
462
|
+
check: (data) => {
|
|
463
|
+
const { exports } = data
|
|
464
|
+
|
|
465
|
+
if (
|
|
466
|
+
isPrivate(data) ||
|
|
467
|
+
exports === undefined ||
|
|
468
|
+
(typeof exports === 'object' && exports !== null && './package.json' in exports)
|
|
469
|
+
) {
|
|
470
|
+
return []
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return [{ messageId: 'missing' }]
|
|
474
|
+
},
|
|
475
|
+
fix: (data) => {
|
|
476
|
+
const { exports } = data
|
|
477
|
+
const isSubpathMap =
|
|
478
|
+
typeof exports === 'object' &&
|
|
479
|
+
exports !== null &&
|
|
480
|
+
!Array.isArray(exports) &&
|
|
481
|
+
Object.keys(exports).every((key) => key.startsWith('.'))
|
|
482
|
+
|
|
483
|
+
return isSubpathMap ? { ...data, exports: { ...exports, './package.json': './package.json' } } : data
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
|
|
487
|
+
'stale-overrides': {
|
|
488
|
+
messages: {
|
|
489
|
+
dead: 'Override "{{ name }}" pins a package nothing in the dependency tree uses. Remove it: `npm pkg delete overrides.{{ name }} && npm install`.',
|
|
490
|
+
redundant:
|
|
491
|
+
'Override "{{ name }}" = {{ pin }} raises nothing: every dependent already asks for {{ pin }} or newer. Remove it: `npm pkg delete overrides.{{ name }} && npm install`.',
|
|
492
|
+
holdingBack:
|
|
493
|
+
'Override "{{ name }}" = {{ pin }} holds {{ dependents }} below the {{ wanted }} they ask for. Raise it: `npm pkg set overrides.{{ name }}={{ wanted }} && npm install`.',
|
|
494
|
+
},
|
|
495
|
+
check: (data, _options, entry) => {
|
|
496
|
+
const packages = loadLock(entry)?.packages
|
|
497
|
+
|
|
498
|
+
if (!packages) {
|
|
499
|
+
return []
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const violations = []
|
|
503
|
+
|
|
504
|
+
for (const [name, pin] of Object.entries(data.overrides ?? {})) {
|
|
505
|
+
if (typeof pin !== 'string' || !EXACT_VERSION_PATTERN.test(pin)) {
|
|
506
|
+
continue
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const pinned = parseVersion(pin)
|
|
510
|
+
const dependents = []
|
|
511
|
+
|
|
512
|
+
for (const [location, manifest] of Object.entries(packages)) {
|
|
513
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
514
|
+
const range = manifest[field]?.[name]
|
|
515
|
+
|
|
516
|
+
if (range !== undefined) {
|
|
517
|
+
dependents.push({
|
|
518
|
+
from: location.replace(/^node_modules\//, '') || 'the package itself',
|
|
519
|
+
min: minVersion(range),
|
|
520
|
+
})
|
|
78
521
|
}
|
|
79
|
-
}
|
|
522
|
+
}
|
|
80
523
|
}
|
|
81
|
-
|
|
524
|
+
|
|
525
|
+
if (dependents.length === 0) {
|
|
526
|
+
violations.push({ messageId: 'dead', data: { name } })
|
|
527
|
+
continue
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const known = dependents.filter((dependent) => dependent.min !== null)
|
|
531
|
+
const above = known.filter((dependent) => compareVersions(dependent.min, pinned) > 0)
|
|
532
|
+
|
|
533
|
+
if (above.length > 0) {
|
|
534
|
+
const wanted = above.reduce(
|
|
535
|
+
(highest, dependent) => (compareVersions(dependent.min, highest) > 0 ? dependent.min : highest),
|
|
536
|
+
pinned,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
violations.push({
|
|
540
|
+
messageId: 'holdingBack',
|
|
541
|
+
data: { name, pin, wanted: formatVersion(wanted), dependents: above.map((dependent) => dependent.from).join(', ') },
|
|
542
|
+
})
|
|
543
|
+
} else if (known.length === dependents.length && known.every((dependent) => compareVersions(dependent.min, pinned) >= 0)) {
|
|
544
|
+
violations.push({ messageId: 'redundant', data: { name, pin } })
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return violations
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
|
|
552
|
+
'shared-configs': {
|
|
553
|
+
messages: {
|
|
554
|
+
commitlint: 'package.json "commitlint.extends" must be "{{ expected }}" so commit messages are checked by the shared rules.',
|
|
555
|
+
madge: 'package.json "madge.tsConfig" must be "{{ expected }}" so find-circulars resolves the same paths as the build.',
|
|
556
|
+
},
|
|
557
|
+
check: (data) => {
|
|
558
|
+
const violations = []
|
|
559
|
+
|
|
560
|
+
if (dependsOn(data, '@diia-inhouse/configs') && data.commitlint?.extends !== COMMITLINT_CONFIG) {
|
|
561
|
+
violations.push({ messageId: 'commitlint', data: { expected: COMMITLINT_CONFIG } })
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (dependsOn(data, 'madge') && data.madge?.tsConfig !== MADGE_TS_CONFIG) {
|
|
565
|
+
violations.push({ messageId: 'madge', data: { expected: MADGE_TS_CONFIG } })
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return violations
|
|
569
|
+
},
|
|
570
|
+
fix: (data) => {
|
|
571
|
+
if (dependsOn(data, '@diia-inhouse/configs')) {
|
|
572
|
+
data.commitlint = { ...data.commitlint, extends: COMMITLINT_CONFIG }
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (dependsOn(data, 'madge')) {
|
|
576
|
+
data.madge = { ...data.madge, tsConfig: MADGE_TS_CONFIG }
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return data
|
|
82
580
|
},
|
|
83
581
|
},
|
|
84
582
|
}
|
|
583
|
+
|
|
584
|
+
function defineRule(name, { messages, schema = [], check, fix }) {
|
|
585
|
+
return {
|
|
586
|
+
meta: { type: 'problem', messages, schema },
|
|
587
|
+
create(context) {
|
|
588
|
+
return {
|
|
589
|
+
Program(node) {
|
|
590
|
+
const path = findManifest(context.filename)
|
|
591
|
+
const entry = path && loadManifest(path)
|
|
592
|
+
|
|
593
|
+
if (!entry || entry.reported.has(name)) {
|
|
594
|
+
return
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
entry.reported.add(name)
|
|
598
|
+
|
|
599
|
+
const options = context.options?.[0] ?? {}
|
|
600
|
+
let violations = check(entry.data, options, entry)
|
|
601
|
+
|
|
602
|
+
if (violations.length > 0 && fix && isFixRun()) {
|
|
603
|
+
writeManifest(entry, fix(structuredClone(entry.data), options, entry))
|
|
604
|
+
violations = check(entry.data, options, entry)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
for (const { messageId, data } of violations) {
|
|
608
|
+
context.report({ node, messageId, data })
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export default {
|
|
617
|
+
meta: { name: '@diia-inhouse/oxlint-plugin-package' },
|
|
618
|
+
rules: Object.fromEntries(Object.entries(manifestRules).map(([name, rule]) => [name, defineRule(name, rule)])),
|
|
619
|
+
}
|