eslint-plugin-chrome-extensions 2.5.0 → 2.7.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 +592 -124
- package/package.json +5 -7
- package/src/data/i18n.js +8 -0
- package/src/data/permissions.js +1 -8
- package/src/data/regex.js +2 -1
- package/src/index.js +0 -4
- package/src/rules/css-spacing.js +367 -0
- package/src/rules/manifest-key-order.js +0 -1
- package/src/rules/manifest-spacing.js +0 -1
- package/src/rules/no-duplicate-manifest-keys.js +0 -1
- package/src/rules/no-global-keyframes.js +84 -0
- package/src/rules/no-host-page-selectors.js +106 -0
- package/src/rules/no-invalid-locales.js +89 -0
- package/src/rules/no-invalid-permissions.js +3 -5
- package/src/rules/no-missing-default-locale.js +39 -14
- package/src/rules/no-missing-i18n-keys.js +3 -5
- package/src/rules/no-remote-urls.js +102 -0
- package/src/rules/no-scrollbar-styling.js +73 -0
- package/src/rules/no-sensitive-permissions.js +1 -1
- package/src/rules/no-unrestricted-host-permissions.js +31 -16
- package/src/rules/no-unused-i18n-keys.js +0 -1
- package/src/rules/no-unused-permissions.js +13 -4
- package/src/rules/require-license-file.js +0 -1
- package/src/rules/require-mv3.js +1 -1
- package/src/rules/require-short-name-when-useful.js +0 -1
- package/src/rules/valid-license-file.js +2 -4
- package/src/rules/valid-manifest-icons.js +31 -3
- package/src/rules/valid-mv3-schema.js +1 -1
- package/src/rules/valid-url-fields.js +42 -37
- package/src/rules/valid-version-field.js +1 -1
- package/src/utils/array.js +1 -0
- package/src/utils/extension.js +13 -0
- package/src/utils/i18n.js +3 -0
- package/src/utils/init.js +13 -7
- package/src/utils/license.js +3 -2
- package/src/data/deprecations.js +0 -10
package/src/rules/require-mv3.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
+
import array from '../utils/array.js'
|
|
1
2
|
import license from '../utils/license.js'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
|
|
4
5
|
import regex from '../data/regex.js'
|
|
5
6
|
|
|
6
|
-
function toArray(value) { return value == null ? [] : Array.isArray(value) ? value : [value] }
|
|
7
|
-
|
|
8
7
|
function validateContent(context, node, { requireSPDX, allowedLicenses }) {
|
|
9
8
|
const text = context.sourceCode.text,
|
|
10
9
|
id = license.spdx.parse(text)
|
|
@@ -28,7 +27,6 @@ export default {
|
|
|
28
27
|
fixable: 'code',
|
|
29
28
|
docs: {
|
|
30
29
|
description: 'Validates SPDX identifiers in license files',
|
|
31
|
-
recommended: false,
|
|
32
30
|
license: true
|
|
33
31
|
},
|
|
34
32
|
defaultOptions: [{ requireSPDX: false, allowedLicenses: [] }],
|
|
@@ -56,7 +54,7 @@ export default {
|
|
|
56
54
|
create(context) {
|
|
57
55
|
const opts = context.options[0],
|
|
58
56
|
{ requireSPDX } = opts,
|
|
59
|
-
allowedLicenses =
|
|
57
|
+
allowedLicenses = array.to(opts.allowedLicenses),
|
|
60
58
|
{ filename } = context,
|
|
61
59
|
basename = path.basename(filename)
|
|
62
60
|
if (!regex.filename.license.test(basename) || regex.filename.script.test(basename))
|
|
@@ -7,13 +7,32 @@ import regex from '../data/regex.js'
|
|
|
7
7
|
const re = { svg: /\.svg$/i },
|
|
8
8
|
sizes = { recommended: ['16', '32', '48', '128'], required: '128' }
|
|
9
9
|
|
|
10
|
+
function validateIcons(context, icons, root, opts, loc) {
|
|
11
|
+
const { requireRecommended, checkFiles, checkSVG } = opts,
|
|
12
|
+
declaredSizes = new Set(Object.keys(icons))
|
|
13
|
+
if (requireRecommended) {
|
|
14
|
+
for (const size of sizes.recommended)
|
|
15
|
+
if (!declaredSizes.has(size))
|
|
16
|
+
context.report({ loc, messageId: 'missingSize', data: { size }})
|
|
17
|
+
} else if (!declaredSizes.has(sizes.required))
|
|
18
|
+
context.report({ loc, messageId: 'missingRequiredSize' })
|
|
19
|
+
for (const file of Object.values(icons)) {
|
|
20
|
+
if (typeof file != 'string') continue
|
|
21
|
+
if (checkSVG && re.svg.test(file))
|
|
22
|
+
context.report({ loc, messageId: 'svgNotSupported' })
|
|
23
|
+
else if (checkFiles && root // eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
24
|
+
&& !fs.existsSync(path.resolve(root, file))
|
|
25
|
+
) context.report({ loc, messageId: 'missingFile', data: { file }})
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
10
29
|
export default {
|
|
11
30
|
meta: {
|
|
12
31
|
type: 'problem',
|
|
13
32
|
docs: {
|
|
14
33
|
description: 'Validate manifest icons: sizes, existing files, no SVG',
|
|
15
34
|
category: 'Possible Errors',
|
|
16
|
-
recommended:
|
|
35
|
+
recommended: true,
|
|
17
36
|
manifest: true
|
|
18
37
|
},
|
|
19
38
|
defaultOptions: [{
|
|
@@ -48,12 +67,21 @@ export default {
|
|
|
48
67
|
},
|
|
49
68
|
|
|
50
69
|
create(context) {
|
|
51
|
-
|
|
70
|
+
const filename = extension.file.getName(context)
|
|
71
|
+
if (!regex.filename.manifest.test(filename) && !regex.filename.script.test(filename)) return {}
|
|
52
72
|
const opts = context.options[0] || {},
|
|
53
73
|
requireRecommended = opts.requireRecommendedSizes !== false,
|
|
54
74
|
checkFiles = opts.checkIconExists !== false,
|
|
55
75
|
checkSVG = opts.noSVG !== false,
|
|
56
|
-
root = checkFiles ? extension.findRoot(context) : null
|
|
76
|
+
root = checkFiles ? extension.findRoot(context) : null,
|
|
77
|
+
resolved = { requireRecommended, checkFiles, checkSVG }
|
|
78
|
+
if (!regex.filename.manifest.test(filename)) {
|
|
79
|
+
const manifest = extension.manifest.read(context) ; if (!manifest) return {}
|
|
80
|
+
const icons = manifest.icons
|
|
81
|
+
if (!icons || typeof icons != 'object' || Array.isArray(icons)) return {}
|
|
82
|
+
validateIcons(context, icons, root, resolved, { line: 1, column: 1 })
|
|
83
|
+
return {}
|
|
84
|
+
}
|
|
57
85
|
return {
|
|
58
86
|
'*'(node) {
|
|
59
87
|
const keyNode = node.name || node.key ; if (!keyNode) return
|
|
@@ -22,6 +22,39 @@ function collectStrings(obj, path = [], out = []) {
|
|
|
22
22
|
|
|
23
23
|
function isURL(value) { return re.absoluteURL.test(value.replace(re.template, 'x')) }
|
|
24
24
|
|
|
25
|
+
function validateManifest(context, manifest, opts) {
|
|
26
|
+
const { checkURLfields, checkMatchPatterns, allowedSchemes, allowAllURLs } = opts,
|
|
27
|
+
loc = { line: 1, column: 1 }
|
|
28
|
+
for (const { path, value } of collectStrings(manifest)) {
|
|
29
|
+
if (checkURLfields && manifestData.paths.url.has(path)) {
|
|
30
|
+
if (!isURL(value))
|
|
31
|
+
context.report({ loc, messageId: 'invalidURL', data: { field: path, value }})
|
|
32
|
+
} else if (checkMatchPatterns && manifestData.paths.match.has(path)) {
|
|
33
|
+
if (value == '<all_urls>') {
|
|
34
|
+
if (!allowAllURLs)
|
|
35
|
+
context.report({ loc, messageId: 'allURLs', data: { field: path }})
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
const match = value.match(re.match)
|
|
39
|
+
if (!match) {
|
|
40
|
+
context.report({ loc, messageId: 'invalidMatchPattern', data: { field: path, value }})
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
const [, scheme, host] = match
|
|
44
|
+
if (scheme != '*' && !allowedSchemes.includes(scheme))
|
|
45
|
+
context.report({ loc, messageId: 'invalidScheme', data: { field: path, value }})
|
|
46
|
+
else if (scheme == 'file' && host != '')
|
|
47
|
+
context.report({ loc, messageId: 'invalidFileHost', data: { field: path, value }})
|
|
48
|
+
else if (path == 'externally_connectable.matches' && host.includes('*'))
|
|
49
|
+
context.report({ loc, messageId: 'invalidExternalHost', data: { field: path, value }})
|
|
50
|
+
else if (host.includes('*') && host != '*' && !host.startsWith('*.'))
|
|
51
|
+
context.report({ loc, messageId: 'invalidHost', data: { field: path, value }})
|
|
52
|
+
else if (path == 'web_accessible_resources.matches' && !value.endsWith('/*'))
|
|
53
|
+
context.report({ loc, messageId: 'invalidWARPath', data: { field: path, value }})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
25
58
|
export default {
|
|
26
59
|
meta: {
|
|
27
60
|
type: 'problem',
|
|
@@ -70,53 +103,25 @@ export default {
|
|
|
70
103
|
},
|
|
71
104
|
|
|
72
105
|
create(context) {
|
|
73
|
-
|
|
106
|
+
const filename = extension.file.getName(context)
|
|
107
|
+
if (!regex.filename.manifest.test(filename) && !regex.filename.script.test(filename)) return {}
|
|
74
108
|
const opts = context.options[0] || {},
|
|
75
109
|
checkURLfields = opts.checkURLfields ?? true,
|
|
76
110
|
checkMatchPatterns = opts.checkMatchPatterns ?? true,
|
|
77
111
|
allowedSchemes = opts.allowedSchemes ?? ['https', 'file'],
|
|
78
112
|
allowAllURLs = opts.allowAllURLs ?? false
|
|
79
113
|
if (!checkURLfields && !checkMatchPatterns) return {}
|
|
114
|
+
const resolved = { checkURLfields, checkMatchPatterns, allowedSchemes, allowAllURLs }
|
|
115
|
+
if (!regex.filename.manifest.test(filename)) {
|
|
116
|
+
const manifest = extension.manifest.read(context) ; if (!manifest) return {}
|
|
117
|
+
validateManifest(context, manifest, resolved)
|
|
118
|
+
return {}
|
|
119
|
+
}
|
|
80
120
|
return {
|
|
81
121
|
'Document:exit'() {
|
|
82
122
|
let manifest
|
|
83
123
|
try { manifest = JSON.parse(context.sourceCode.text) } catch { return }
|
|
84
|
-
|
|
85
|
-
if (checkURLfields && manifestData.paths.url.has(path)) {
|
|
86
|
-
if (!isURL(value))
|
|
87
|
-
context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidURL',
|
|
88
|
-
data: { field: path, value }})
|
|
89
|
-
} else if (checkMatchPatterns && manifestData.paths.match.has(path)) {
|
|
90
|
-
if (value == '<all_urls>') {
|
|
91
|
-
if (!allowAllURLs)
|
|
92
|
-
context.report({ loc: { line: 1, column: 1 }, messageId: 'allURLs',
|
|
93
|
-
data: { field: path }})
|
|
94
|
-
continue
|
|
95
|
-
}
|
|
96
|
-
const match = value.match(re.match)
|
|
97
|
-
if (!match) {
|
|
98
|
-
context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidMatchPattern',
|
|
99
|
-
data: { field: path, value }})
|
|
100
|
-
continue
|
|
101
|
-
}
|
|
102
|
-
const [, scheme, host] = match
|
|
103
|
-
if (scheme != '*' && !allowedSchemes.includes(scheme))
|
|
104
|
-
context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidScheme',
|
|
105
|
-
data: { field: path, value }})
|
|
106
|
-
else if (scheme == 'file' && host != '')
|
|
107
|
-
context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidFileHost',
|
|
108
|
-
data: { field: path, value }})
|
|
109
|
-
else if (path == 'externally_connectable.matches' && host.includes('*'))
|
|
110
|
-
context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidExternalHost',
|
|
111
|
-
data: { field: path, value }})
|
|
112
|
-
else if (host.includes('*') && host != '*' && !host.startsWith('*.'))
|
|
113
|
-
context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidHost',
|
|
114
|
-
data: { field: path, value }})
|
|
115
|
-
else if (path == 'web_accessible_resources.matches' && !value.endsWith('/*'))
|
|
116
|
-
context.report({ loc: { line: 1, column: 1 }, messageId: 'invalidWARPath',
|
|
117
|
-
data: { field: path, value }})
|
|
118
|
-
}
|
|
119
|
-
}
|
|
124
|
+
validateManifest(context, manifest, resolved)
|
|
120
125
|
}
|
|
121
126
|
}
|
|
122
127
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default { to(val) { return Array.isArray(val) ? val : val == null ? [] : [val] }}
|
package/src/utils/extension.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
import fs from 'node:fs'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
|
|
6
|
+
import permissions from '../data/permissions.js'
|
|
7
|
+
import regex from '../data/regex.js'
|
|
8
|
+
|
|
6
9
|
const manifestCache = new Map()
|
|
7
10
|
|
|
8
11
|
const utils = {
|
|
@@ -36,6 +39,16 @@ const utils = {
|
|
|
36
39
|
},
|
|
37
40
|
|
|
38
41
|
manifest: {
|
|
42
|
+
permission: {
|
|
43
|
+
valid: permissions.valid,
|
|
44
|
+
sensitive: permissions.sensitive,
|
|
45
|
+
isValid(name, target = 'mv3') {
|
|
46
|
+
if (permissions.valid.has(name)) return true
|
|
47
|
+
if (target == 'mv3') return false
|
|
48
|
+
return regex.url.test(name) || name.startsWith('<') || name == '*://*/*'
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
|
|
39
52
|
read(context) {
|
|
40
53
|
const root = utils.findRoot(context) ; if (!root) return null
|
|
41
54
|
if (manifestCache.has(root)) return manifestCache.get(root)
|
package/src/utils/i18n.js
CHANGED
|
@@ -43,6 +43,9 @@ export default {
|
|
|
43
43
|
|
|
44
44
|
isPredefined(key) { return key.startsWith('@@') },
|
|
45
45
|
|
|
46
|
+
localeIsValid(code, allowed = new Set()) {
|
|
47
|
+
return typeof code == 'string' && (allowed.has(code) || i18n.locales.supported.has(code)) },
|
|
48
|
+
|
|
46
49
|
readDefaultLocaleMessages(manifest, root) {
|
|
47
50
|
const locale = manifest?.default_locale
|
|
48
51
|
if (typeof locale != 'string') return null
|
package/src/utils/init.js
CHANGED
|
@@ -4,19 +4,25 @@ import json from '@eslint/json'
|
|
|
4
4
|
import rulesUtils from './rules.js'
|
|
5
5
|
import text from './text.js'
|
|
6
6
|
|
|
7
|
+
import cssSpacing from '../rules/css-spacing.js'
|
|
7
8
|
import manifestKeyOrder from '../rules/manifest-key-order.js'
|
|
8
9
|
import manifestSpacing from '../rules/manifest-spacing.js'
|
|
9
10
|
import noContentStyleLeak from '../rules/no-content-style-leak.js'
|
|
10
11
|
import noDeprecatedAPIs from '../rules/no-deprecated-apis.js'
|
|
11
12
|
import noDuplicateManifestKeys from '../rules/no-duplicate-manifest-keys.js'
|
|
13
|
+
import noGlobalKeyframes from '../rules/no-global-keyframes.js'
|
|
14
|
+
import noHostPageSelectors from '../rules/no-host-page-selectors.js'
|
|
12
15
|
import noInsecureFetch from '../rules/no-insecure-fetch.js'
|
|
13
16
|
import noInsecureMessaging from '../rules/no-insecure-messaging.js'
|
|
17
|
+
import noInvalidLocales from '../rules/no-invalid-locales.js'
|
|
14
18
|
import noInvalidPermissions from '../rules/no-invalid-permissions.js'
|
|
15
19
|
import noMissingDefaultLocale from '../rules/no-missing-default-locale.js'
|
|
16
20
|
import noMissingI18nKeys from '../rules/no-missing-i18n-keys.js'
|
|
17
21
|
import noMissingPermissions from '../rules/no-missing-permissions.js'
|
|
18
22
|
import noMissingWebAccessibleResources from '../rules/no-missing-web-accessible-resources.js'
|
|
19
23
|
import noRemoteCode from '../rules/no-remote-code.js'
|
|
24
|
+
import noRemoteURLs from '../rules/no-remote-urls.js'
|
|
25
|
+
import noScrollbarStyling from '../rules/no-scrollbar-styling.js'
|
|
20
26
|
import noSensitivePermissions from '../rules/no-sensitive-permissions.js'
|
|
21
27
|
import noUnrestrictedHostPermissions from '../rules/no-unrestricted-host-permissions.js'
|
|
22
28
|
import noUnusedI18nKeys from '../rules/no-unused-i18n-keys.js'
|
|
@@ -35,12 +41,6 @@ import validVersionField from '../rules/valid-version-field.js'
|
|
|
35
41
|
const namespace = 'chrome-extensions'
|
|
36
42
|
|
|
37
43
|
export default {
|
|
38
|
-
bundleDeprecation(plugin, [ruleId, bundleNames], pluginName) { /* eslint-disable security/detect-object-injection */
|
|
39
|
-
for (const bundleName of bundleNames)
|
|
40
|
-
if (plugin.configs[bundleName])
|
|
41
|
-
plugin.configs[bundleName].rules[`${pluginName}/${ruleId}`] = 'error'
|
|
42
|
-
}, /* eslint-enable security/detect-object-injection */
|
|
43
|
-
|
|
44
44
|
configs(plugin) {
|
|
45
45
|
const plugins = { [namespace]: plugin },
|
|
46
46
|
rules = plugin.rules,
|
|
@@ -67,23 +67,29 @@ export default {
|
|
|
67
67
|
}
|
|
68
68
|
},
|
|
69
69
|
|
|
70
|
-
plugin() { return { rules: {}, configs: {}}},
|
|
70
|
+
plugin() { return { rules: {}, configs: {} }},
|
|
71
71
|
|
|
72
72
|
rules(plugin) {
|
|
73
73
|
plugin.rules = {
|
|
74
|
+
'css-spacing': cssSpacing,
|
|
74
75
|
'manifest-key-order': manifestKeyOrder,
|
|
75
76
|
'manifest-spacing': manifestSpacing,
|
|
76
77
|
'no-content-style-leak': noContentStyleLeak,
|
|
77
78
|
'no-deprecated-apis': noDeprecatedAPIs,
|
|
78
79
|
'no-duplicate-manifest-keys': noDuplicateManifestKeys,
|
|
80
|
+
'no-global-keyframes': noGlobalKeyframes,
|
|
81
|
+
'no-host-page-selectors': noHostPageSelectors,
|
|
79
82
|
'no-insecure-fetch': noInsecureFetch,
|
|
80
83
|
'no-insecure-messaging': noInsecureMessaging,
|
|
84
|
+
'no-invalid-locales': noInvalidLocales,
|
|
81
85
|
'no-invalid-permissions': noInvalidPermissions,
|
|
82
86
|
'no-missing-default-locale': noMissingDefaultLocale,
|
|
83
87
|
'no-missing-i18n-keys': noMissingI18nKeys,
|
|
84
88
|
'no-missing-permissions': noMissingPermissions,
|
|
85
89
|
'no-missing-web-accessible-resources': noMissingWebAccessibleResources,
|
|
86
90
|
'no-remote-code': noRemoteCode,
|
|
91
|
+
'no-remote-urls': noRemoteURLs,
|
|
92
|
+
'no-scrollbar-styling': noScrollbarStyling,
|
|
87
93
|
'no-sensitive-permissions': noSensitivePermissions,
|
|
88
94
|
'no-unrestricted-host-permissions': noUnrestrictedHostPermissions,
|
|
89
95
|
'no-unused-i18n-keys': noUnusedI18nKeys,
|
package/src/utils/license.js
CHANGED
|
@@ -14,8 +14,7 @@ const re = { spdxTag: /SPDX-License-Identifier:\s*([\w.-]+)/i },
|
|
|
14
14
|
.map(([id, { name }]) => ({ id, name }))
|
|
15
15
|
.sort((a, b) => b.name.length - a.name.length)
|
|
16
16
|
|
|
17
|
-
function tryReaddir(dir) {
|
|
18
|
-
try { return fs.readdirSync(dir, { withFileTypes: true }) } catch { return null }}
|
|
17
|
+
function tryReaddir(dir) { try { return fs.readdirSync(dir, { withFileTypes: true }) } catch { return null }}
|
|
19
18
|
|
|
20
19
|
export default {
|
|
21
20
|
find(root) {
|
|
@@ -38,6 +37,7 @@ export default {
|
|
|
38
37
|
trailer = `${text.endsWith(nl) ? '' : nl}${nl}SPDX-License-Identifier: ${id}${nl}`
|
|
39
38
|
return fixer.insertTextAfterRange([0, text.length], trailer)
|
|
40
39
|
},
|
|
40
|
+
|
|
41
41
|
find(text) {
|
|
42
42
|
const normalizedTxt = text.replace(/\s+/g, ' ')
|
|
43
43
|
let best = null
|
|
@@ -49,6 +49,7 @@ export default {
|
|
|
49
49
|
}
|
|
50
50
|
return best?.id ?? null
|
|
51
51
|
},
|
|
52
|
+
|
|
52
53
|
isValid(id) { return spdxIDs.has(id) },
|
|
53
54
|
parse(text) { return text.match(re.spdxTag)?.[1] ?? null }
|
|
54
55
|
}
|
package/src/data/deprecations.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
// Delete a line to end support
|
|
2
|
-
|
|
3
|
-
export default {
|
|
4
|
-
bundles: {
|
|
5
|
-
'no-invalid-permissions': ['recommended'], // 2026-10-5 announced future removal
|
|
6
|
-
'require-mv3': ['recommended'], // 2026-10-5 announced future removal
|
|
7
|
-
'valid-mv3-schema': ['recommended'], // 2026-10-5 announced future removal
|
|
8
|
-
'valid-version-field': ['recommended'] // 2026-10-5 announced future removal
|
|
9
|
-
}
|
|
10
|
-
}
|