eslint-plugin-chrome-extensions 1.0.1 → 2.1.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/README.md +580 -80
- package/package.json +6 -5
- package/src/data/deprecations.js +10 -0
- package/src/data/manifest.js +24 -5
- package/src/index.js +41 -22
- package/src/rules/manifest-key-order.js +76 -0
- package/src/rules/no-content-style-leak.js +1 -1
- package/src/rules/no-duplicate-manifest-keys.js +36 -0
- package/src/rules/no-insecure-fetch.js +52 -0
- package/src/rules/no-insecure-messaging.js +44 -7
- package/src/rules/no-invalid-permissions.js +26 -5
- package/src/rules/no-missing-web-accessible-resources.js +51 -0
- package/src/rules/no-remote-code.js +11 -9
- package/src/rules/no-unrestricted-host-permissions.js +54 -0
- package/src/rules/no-unused-permissions.js +64 -19
- package/src/rules/require-mv3.js +21 -3
- package/src/rules/valid-manifest-icons.js +85 -0
- package/src/rules/valid-mv3-schema.js +54 -5
- package/src/rules/valid-service-worker.js +1 -1
- package/src/rules/valid-version-field.js +95 -0
- package/src/utils.js +24 -3
|
@@ -1,15 +1,40 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { apiPermissionMap, findPermission } from '../data/apis.js'
|
|
4
|
+
import { findExtensionRoot, getFilename, readManifest } from '../utils.js'
|
|
3
5
|
|
|
4
|
-
const
|
|
6
|
+
const re = { manifestFilename: /manifest\.json$/ }
|
|
7
|
+
|
|
8
|
+
function collectUsedAPIs(manifest, root) {
|
|
9
|
+
const files = new Set()
|
|
10
|
+
if (manifest.background?.service_worker) files.add(manifest.background.service_worker)
|
|
11
|
+
for (const script of manifest.background?.scripts || []) files.add(script)
|
|
12
|
+
for (const contentScript of manifest.content_scripts || [])
|
|
13
|
+
for (const file of contentScript.js || []) files.add(file)
|
|
14
|
+
if (manifest.action?.default_popup) files.add(manifest.action.default_popup)
|
|
15
|
+
if (manifest.options_ui?.page) files.add(manifest.options_ui.page)
|
|
16
|
+
if (manifest.devtools_page) files.add(manifest.devtools_page)
|
|
17
|
+
const re = { /* eslint-disable-next-line security/detect-unsafe-regex */
|
|
18
|
+
chromeAPI: /\bchrome\.\w+(?:\.\w+)*/g }
|
|
19
|
+
const used = new Set()
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
try {
|
|
22
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
23
|
+
const src = fs.readFileSync(path.resolve(root, file), 'utf8')
|
|
24
|
+
for (const match of src.matchAll(re.chromeAPI)) used.add(match[0])
|
|
25
|
+
} catch {} // eslint-disable-line no-empty
|
|
26
|
+
}
|
|
27
|
+
return used
|
|
28
|
+
}
|
|
5
29
|
|
|
6
30
|
export default {
|
|
7
31
|
meta: {
|
|
8
|
-
type: '
|
|
32
|
+
type: 'problem',
|
|
9
33
|
docs: {
|
|
10
|
-
description: 'Flag manifest permissions whose APIs are never used
|
|
11
|
-
category: '
|
|
12
|
-
recommended:
|
|
34
|
+
description: 'Flag manifest permissions whose APIs are never used',
|
|
35
|
+
category: 'Possible Errors',
|
|
36
|
+
recommended: false,
|
|
37
|
+
manifest: true
|
|
13
38
|
},
|
|
14
39
|
schema: [],
|
|
15
40
|
messages: {
|
|
@@ -18,20 +43,40 @@ export default {
|
|
|
18
43
|
},
|
|
19
44
|
|
|
20
45
|
create(context) {
|
|
46
|
+
if (!re.manifestFilename.test(getFilename(context))) return {}
|
|
21
47
|
const manifest = readManifest(context) ; if (!manifest) return {}
|
|
48
|
+
const root = findExtensionRoot(context) ; if (!root) return {}
|
|
49
|
+
const usedAPIs = collectUsedAPIs(manifest, root)
|
|
50
|
+
const permissionNodes = new Map()
|
|
51
|
+
function isUsed(permission) {
|
|
52
|
+
let known = false
|
|
53
|
+
for (const [, perm] of apiPermissionMap)
|
|
54
|
+
if (perm == permission) { known = true ; break }
|
|
55
|
+
if (!known) return true // custom permission → not our business
|
|
56
|
+
for (const api of usedAPIs)
|
|
57
|
+
if (findPermission(api) == permission) return true
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
22
60
|
return {
|
|
23
|
-
'
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
61
|
+
'*'(node) {
|
|
62
|
+
const keyNode = node.name || node.key ; if (!keyNode) return
|
|
63
|
+
const keyName = keyNode.value ?? keyNode.name
|
|
64
|
+
if (keyName != 'permissions' && keyName != 'optional_permissions') return
|
|
65
|
+
const arrNode = node.value ; if (arrNode?.type != 'Array') return
|
|
66
|
+
const elements = arrNode.elements || arrNode.items || []
|
|
67
|
+
for (const el of elements) {
|
|
68
|
+
const valueNode = el.value || el
|
|
69
|
+
const perm = valueNode?.value
|
|
70
|
+
if (typeof perm != 'string') continue
|
|
71
|
+
if (!permissionNodes.has(perm)) permissionNodes.set(perm, [])
|
|
72
|
+
permissionNodes.get(perm).push(valueNode)
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
'Document:exit'() {
|
|
76
|
+
for (const [permission, nodes] of permissionNodes) {
|
|
77
|
+
if (isUsed(permission)) continue
|
|
78
|
+
for (const n of nodes)
|
|
79
|
+
context.report({ node: n, messageId: 'unusedPermission', data: { permission }})
|
|
35
80
|
}
|
|
36
81
|
}
|
|
37
82
|
}
|
package/src/rules/require-mv3.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { readManifest } from '../utils.js'
|
|
1
|
+
import { getFilename, readManifest } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
const re = { manifestFilename: /manifest\.json$/ }
|
|
2
4
|
|
|
3
5
|
export default {
|
|
4
6
|
meta: {
|
|
@@ -6,15 +8,31 @@ export default {
|
|
|
6
8
|
docs: {
|
|
7
9
|
description: 'Require manifest_version 3',
|
|
8
10
|
category: 'Possible Errors',
|
|
9
|
-
recommended:
|
|
11
|
+
recommended: false,
|
|
12
|
+
manifest: true
|
|
10
13
|
},
|
|
11
14
|
schema: [],
|
|
12
15
|
messages: {
|
|
13
|
-
notMV3: 'manifest.json must declare `"manifest_version": 3
|
|
16
|
+
notMV3: 'manifest.json must declare `"manifest_version": 3`.'
|
|
14
17
|
}
|
|
15
18
|
},
|
|
16
19
|
|
|
17
20
|
create(context) {
|
|
21
|
+
if (re.manifestFilename.test(getFilename(context))) { // lint it directly
|
|
22
|
+
let foundVersion = null, versionNode = null
|
|
23
|
+
return {
|
|
24
|
+
'*'(node) {
|
|
25
|
+
const keyNode = node.name || node.key ; if (!keyNode) return
|
|
26
|
+
if ((keyNode.value ?? keyNode.name) != 'manifest_version') return
|
|
27
|
+
foundVersion = node.value?.value
|
|
28
|
+
versionNode = node.value || keyNode
|
|
29
|
+
},
|
|
30
|
+
'*:exit'(node) {
|
|
31
|
+
if (node.type != 'Document' || foundVersion == 3) return
|
|
32
|
+
context.report({ node: versionNode || node, messageId: 'notMV3' })
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
18
36
|
const manifest = readManifest(context) ; if (!manifest || manifest.manifest_version == 3) return {}
|
|
19
37
|
return { Program(node) { context.report({ node, messageId: 'notMV3' }) }}
|
|
20
38
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { findExtensionRoot, getFilename } from '../utils.js'
|
|
5
|
+
|
|
6
|
+
const re = { manifestFilename: /manifest\.json$/, svg: /\.svg$/i },
|
|
7
|
+
sizes = { recommended: ['16', '32', '48', '128'], required: '128' }
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'problem',
|
|
12
|
+
docs: {
|
|
13
|
+
description: 'Validate manifest icons: sizes, existing files, no SVG',
|
|
14
|
+
category: 'Possible Errors',
|
|
15
|
+
recommended: false,
|
|
16
|
+
manifest: true
|
|
17
|
+
},
|
|
18
|
+
defaultOptions: [{
|
|
19
|
+
requireRecommendedSizes: true,
|
|
20
|
+
checkIconExists: true,
|
|
21
|
+
noSVG: true
|
|
22
|
+
}],
|
|
23
|
+
schema: [{
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
requireRecommendedSizes: {
|
|
27
|
+
type: 'boolean',
|
|
28
|
+
description: 'Require all recommended sizes (16, 32, 48, 128)'
|
|
29
|
+
},
|
|
30
|
+
checkIconExists: {
|
|
31
|
+
type: 'boolean',
|
|
32
|
+
description: 'Flag icon paths that do not resolve to a file'
|
|
33
|
+
},
|
|
34
|
+
noSVG: {
|
|
35
|
+
type: 'boolean',
|
|
36
|
+
description: 'Flag SVG icon paths (unsupported by Chrome)'
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
additionalProperties: false
|
|
40
|
+
}],
|
|
41
|
+
messages: {
|
|
42
|
+
missingSize: 'Recommended icon size "{{size}}" is missing (16, 32, 48, 128).',
|
|
43
|
+
missingRequiredSize: 'Icons must include a 128x128 icon.',
|
|
44
|
+
missingFile: 'Icon file "{{file}}" does not exist in the extension root.',
|
|
45
|
+
svgNotSupported: 'SVG icons are not supported by Chrome. Use PNG instead.'
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
create(context) {
|
|
50
|
+
if (!re.manifestFilename.test(getFilename(context))) return {}
|
|
51
|
+
const opts = context.options[0] || {},
|
|
52
|
+
requireRecommended = opts.requireRecommendedSizes !== false,
|
|
53
|
+
checkFiles = opts.checkIconExists !== false,
|
|
54
|
+
checkSVG = opts.noSVG !== false
|
|
55
|
+
const root = checkFiles ? findExtensionRoot(context) : null
|
|
56
|
+
return {
|
|
57
|
+
'*'(node) {
|
|
58
|
+
const keyNode = node.name || node.key ; if (!keyNode) return
|
|
59
|
+
const keyName = keyNode.value ?? keyNode.name ; if (keyName != 'icons') return
|
|
60
|
+
const iconsObj = node.value ; if (iconsObj?.type != 'Object') return
|
|
61
|
+
const members = iconsObj.members || iconsObj.properties || []
|
|
62
|
+
const declaredSizes = new Set()
|
|
63
|
+
for (const member of members) {
|
|
64
|
+
const sizeNode = member.name || member.key,
|
|
65
|
+
size = sizeNode?.value ?? sizeNode?.name,
|
|
66
|
+
fileNode = member.value,
|
|
67
|
+
file = fileNode?.value
|
|
68
|
+
if (typeof size == 'string') declaredSizes.add(size)
|
|
69
|
+
if (typeof file != 'string') continue
|
|
70
|
+
if (checkSVG && re.svg.test(file))
|
|
71
|
+
context.report({ node: fileNode, messageId: 'svgNotSupported' })
|
|
72
|
+
else if (checkFiles && root // eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
73
|
+
&& !fs.existsSync(path.resolve(root, file))
|
|
74
|
+
) context.report({ node: fileNode, messageId: 'missingFile', data: { file }})
|
|
75
|
+
}
|
|
76
|
+
if (requireRecommended) {
|
|
77
|
+
for (const size of sizes.recommended)
|
|
78
|
+
if (!declaredSizes.has(size))
|
|
79
|
+
context.report({ node: iconsObj, messageId: 'missingSize', data: { size }})
|
|
80
|
+
} else if (!declaredSizes.has(sizes.required))
|
|
81
|
+
context.report({ node: iconsObj, messageId: 'missingRequiredSize' })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { mv2OnlyKeys,
|
|
2
|
-
|
|
1
|
+
import { mv2OnlyKeys, forbiddenCSPdirectives } from '../data/manifest.js'
|
|
2
|
+
|
|
3
|
+
import { getFilename, readManifest } from '../utils.js'
|
|
4
|
+
|
|
5
|
+
const re = { manifestFilename: /manifest\.json$/ }
|
|
3
6
|
|
|
4
7
|
export default {
|
|
5
8
|
meta: {
|
|
@@ -7,17 +10,63 @@ export default {
|
|
|
7
10
|
docs: {
|
|
8
11
|
description: 'Validate manifest against the MV3 schema',
|
|
9
12
|
category: 'Possible Errors',
|
|
10
|
-
recommended:
|
|
13
|
+
recommended: false,
|
|
14
|
+
manifest: true
|
|
11
15
|
},
|
|
12
16
|
schema: [],
|
|
13
17
|
messages: {
|
|
14
|
-
mv2Key: '`{{key}}` is not allowed in MV3
|
|
18
|
+
mv2Key: '`{{key}}` is not allowed in MV3. {{reason}}.',
|
|
15
19
|
unsafeCSP: 'MV3 CSP must not include `{{directive}}`.',
|
|
16
20
|
wrongCSPType: '`content_security_policy` must be an object in MV3, not a string.'
|
|
17
21
|
}
|
|
18
22
|
},
|
|
19
23
|
|
|
20
24
|
create(context) {
|
|
25
|
+
if (re.manifestFilename.test(getFilename(context))) { // lint it directly
|
|
26
|
+
const ancestors = [], pendingReports = []
|
|
27
|
+
let isMV3 = false
|
|
28
|
+
const keyOf = n => {
|
|
29
|
+
const k = n?.name || n?.key
|
|
30
|
+
return k?.value ?? k?.name
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
'*'(node) {
|
|
34
|
+
const keyName = keyOf(node)
|
|
35
|
+
if (keyName) {
|
|
36
|
+
const isTopLevel = ancestors.length == 2
|
|
37
|
+
&& ancestors[0].type == 'Document'
|
|
38
|
+
&& ancestors[1].type == 'Object'
|
|
39
|
+
const inBackground = ancestors.some(a => keyOf(a) == 'background')
|
|
40
|
+
const inCSP = ancestors.some(a => keyOf(a) == 'content_security_policy')
|
|
41
|
+
if (keyName == 'manifest_version' && node.value?.value == 3) isMV3 = true
|
|
42
|
+
if (isTopLevel && mv2OnlyKeys.has(keyName))
|
|
43
|
+
pendingReports.push({ node, messageId: 'mv2Key',
|
|
44
|
+
data: { key: keyName, reason: 'MV2-only key' }})
|
|
45
|
+
if (inBackground && keyName == 'scripts')
|
|
46
|
+
pendingReports.push({ node, messageId: 'mv2Key',
|
|
47
|
+
data: { key: 'background.scripts', reason: 'use background.service_worker' }})
|
|
48
|
+
if (inBackground && keyName == 'persistent')
|
|
49
|
+
pendingReports.push({ node, messageId: 'mv2Key',
|
|
50
|
+
data: { key: 'background.persistent', reason: 'MV3 service workers are never persistent' }})
|
|
51
|
+
if (isTopLevel && keyName == 'content_security_policy' && node.value?.type == 'String')
|
|
52
|
+
pendingReports.push({ node: node.value, messageId: 'wrongCSPType' })
|
|
53
|
+
if (inCSP && keyName == 'extension_pages' && typeof node.value?.value == 'string')
|
|
54
|
+
for (const directive of forbiddenCSPdirectives)
|
|
55
|
+
if (node.value.value.includes(directive))
|
|
56
|
+
pendingReports.push({ node: node.value, messageId: 'unsafeCSP',
|
|
57
|
+
data: { directive }})
|
|
58
|
+
}
|
|
59
|
+
ancestors.push(node)
|
|
60
|
+
},
|
|
61
|
+
'*:exit'(node) {
|
|
62
|
+
ancestors.pop()
|
|
63
|
+
if (node.type == 'Document') {
|
|
64
|
+
if (!isMV3) return
|
|
65
|
+
for (const r of pendingReports) context.report(r)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
21
70
|
const manifest = readManifest(context) ; if (!manifest || manifest.manifest_version != 3) return {}
|
|
22
71
|
return {
|
|
23
72
|
Program(node) {
|
|
@@ -35,7 +84,7 @@ export default {
|
|
|
35
84
|
context.report({ node, messageId: 'wrongCSPType' })
|
|
36
85
|
const cspPages = csp?.extension_pages
|
|
37
86
|
if (typeof cspPages == 'string')
|
|
38
|
-
for (const directive of
|
|
87
|
+
for (const directive of forbiddenCSPdirectives)
|
|
39
88
|
if (cspPages.includes(directive))
|
|
40
89
|
context.report({ node, messageId: 'unsafeCSP', data: { directive }})
|
|
41
90
|
}
|
|
@@ -12,7 +12,7 @@ export default {
|
|
|
12
12
|
schema: [],
|
|
13
13
|
messages: {
|
|
14
14
|
listenerNotTopLevel: '`chrome.*.addListener()` must be registered at module top level so it attaches on service worker wake-up.',
|
|
15
|
-
keepaliveTimer: 'Sub-30s `{{fn}}` will not keep the MV3 service worker alive
|
|
15
|
+
keepaliveTimer: 'Sub-30s `{{fn}}` will not keep the MV3 service worker alive. Use `chrome.alarms` instead.',
|
|
16
16
|
domInSW: '`{{global}}` is not available in a service worker.'
|
|
17
17
|
}
|
|
18
18
|
},
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { getFilename, readManifest } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
const re = { /* eslint-disable security/detect-unsafe-regex */
|
|
4
|
+
chromeParts: /^\d+(?:\.\d+){0,3}$/,
|
|
5
|
+
date: /^\d{4}\.(?:0?[1-9]|1[0-2])\.(?:0?[1-9]|[12]\d|3[01])(?:\.\d+)?$/,
|
|
6
|
+
manifestFilename: /manifest\.json$/,
|
|
7
|
+
semver: /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/
|
|
8
|
+
} /* eslint-enable security/detect-unsafe-regex */
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'problem',
|
|
13
|
+
docs: {
|
|
14
|
+
description: 'Validate the manifest version field format',
|
|
15
|
+
category: 'Possible Errors',
|
|
16
|
+
recommended: false,
|
|
17
|
+
manifest: true
|
|
18
|
+
},
|
|
19
|
+
schema: [{
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
versionType: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
enum: ['chrome', 'semver', 'date'],
|
|
25
|
+
description: 'Version format to enforce (default: chrome)'
|
|
26
|
+
},
|
|
27
|
+
allowFutureDate: {
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
description: 'Allow version dates later than the current date (default: false)'
|
|
30
|
+
},
|
|
31
|
+
timezone: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'IANA timezone for determining the current date (when omitted, only year and month are checked)'
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
additionalProperties: false
|
|
37
|
+
}],
|
|
38
|
+
defaultOptions: [{}],
|
|
39
|
+
messages: {
|
|
40
|
+
invalidVersion: 'Invalid manifest `version`: must be a valid {{versionType}} version (expected format: {{example}}).',
|
|
41
|
+
futureVersion: 'Invalid manifest `version`: date cannot be in the future.'
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
create(context) {
|
|
46
|
+
const options = context.options[0] || {},
|
|
47
|
+
versionType = options.versionType || 'chrome',
|
|
48
|
+
allowFutureDate = options.allowFutureDate ?? false,
|
|
49
|
+
timezone = options.timezone,
|
|
50
|
+
example = versionType == 'semver' ? '1.2.3'
|
|
51
|
+
: versionType == 'date' ? 'YYYY.M.D or YYYY.M.D.N'
|
|
52
|
+
: '1.2.3 or 1.2.3.4'
|
|
53
|
+
const now = timezone
|
|
54
|
+
? Object.fromEntries(new Intl.DateTimeFormat('en', {
|
|
55
|
+
timeZone: timezone, year: 'numeric', month: 'numeric', day: 'numeric'
|
|
56
|
+
}).formatToParts().filter(({ type }) => ['year', 'month', 'day'].includes(type))
|
|
57
|
+
.map(({ type, value }) => [type, Number(value)]))
|
|
58
|
+
: { year: new Date().getUTCFullYear(), month: new Date().getUTCMonth() +1 }
|
|
59
|
+
function validate(node, version) {
|
|
60
|
+
if (typeof version != 'string') return
|
|
61
|
+
const parts = version.split('.')
|
|
62
|
+
if (!re.chromeParts.test(version) || parts.some(part => Number(part) > 65535))
|
|
63
|
+
return context.report({ node, messageId: 'invalidVersion', data: { versionType, example }})
|
|
64
|
+
if (versionType == 'semver') {
|
|
65
|
+
if (!re.semver.test(version))
|
|
66
|
+
context.report({ node, messageId: 'invalidVersion', data: { versionType, example }})
|
|
67
|
+
} else if (versionType == 'date') {
|
|
68
|
+
if (!re.date.test(version))
|
|
69
|
+
context.report({ node, messageId: 'invalidVersion', data: { versionType, example }})
|
|
70
|
+
else {
|
|
71
|
+
const [year, month, day] = parts.map(Number)
|
|
72
|
+
if (!allowFutureDate && (
|
|
73
|
+
year > now.year
|
|
74
|
+
|| year == now.year && month > now.month
|
|
75
|
+
|| timezone && year == now.year && month == now.month && day > now.day
|
|
76
|
+
)) context.report({ node, messageId: 'futureVersion' })
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (re.manifestFilename.test(getFilename(context))) // lint it directly
|
|
81
|
+
return {
|
|
82
|
+
'*'(node) {
|
|
83
|
+
const keyNode = node.name || node.key ; if (!keyNode) return
|
|
84
|
+
const keyName = keyNode.value ?? keyNode.name ; if (keyName != 'version') return
|
|
85
|
+
const valueNode = node.value ; if (valueNode?.type != 'String') return
|
|
86
|
+
validate(valueNode, valueNode.value)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
Program(node) {
|
|
91
|
+
validate(node, readManifest(context)?.version)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/utils.js
CHANGED
|
@@ -4,13 +4,34 @@ import path from 'node:path'
|
|
|
4
4
|
const re = { chromeAPI: /\bchrome(?:\.\w+)+/g }, // eslint-disable-line security/detect-unsafe-regex
|
|
5
5
|
manifestCache = new Map()
|
|
6
6
|
|
|
7
|
+
export function bundleRules(rules, predicate, pluginName, removals = []) {
|
|
8
|
+
return Object.fromEntries(Object.entries(rules)
|
|
9
|
+
.filter(([ruleId, rule]) => predicate(rule) || removals.includes(ruleId))
|
|
10
|
+
.map(([ruleId]) => [`${pluginName}/${ruleId}`, 'error']))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function deprecatedFrom(bundles, bundleName) {
|
|
14
|
+
const ruleIds = []
|
|
15
|
+
for (const [ruleId, [bundle]] of Object.entries(bundles))
|
|
16
|
+
if (bundle === bundleName) ruleIds.push(ruleId)
|
|
17
|
+
return ruleIds
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function initBundleDeprecation(plugin, [ruleId, [bundleName, note]], pluginName) {
|
|
21
|
+
if (!process.env.DEBUG_MODE) return
|
|
22
|
+
if (plugin.rules[ruleId]) // eslint-disable-line security/detect-object-injection
|
|
23
|
+
console.warn(`[${pluginName}] '${ruleId}' will be removed from '${
|
|
24
|
+
bundleName}' in a future major release${note ? ` (${note})` : ''}`)
|
|
25
|
+
}
|
|
26
|
+
|
|
7
27
|
export function findExtensionRoot(context) {
|
|
8
28
|
let dir = path.dirname(getFilename(context))
|
|
9
|
-
|
|
10
|
-
while (dir != root) {
|
|
29
|
+
while (dir) {
|
|
11
30
|
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
12
31
|
if (fs.existsSync(path.join(dir, 'manifest.json'))) return dir
|
|
13
|
-
|
|
32
|
+
const parent = path.dirname(dir)
|
|
33
|
+
if (parent == dir) break // reached filesystem root or a stuck relative "."
|
|
34
|
+
dir = parent
|
|
14
35
|
}
|
|
15
36
|
return null
|
|
16
37
|
}
|