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.
Files changed (36) hide show
  1. package/README.md +592 -124
  2. package/package.json +5 -7
  3. package/src/data/i18n.js +8 -0
  4. package/src/data/permissions.js +1 -8
  5. package/src/data/regex.js +2 -1
  6. package/src/index.js +0 -4
  7. package/src/rules/css-spacing.js +367 -0
  8. package/src/rules/manifest-key-order.js +0 -1
  9. package/src/rules/manifest-spacing.js +0 -1
  10. package/src/rules/no-duplicate-manifest-keys.js +0 -1
  11. package/src/rules/no-global-keyframes.js +84 -0
  12. package/src/rules/no-host-page-selectors.js +106 -0
  13. package/src/rules/no-invalid-locales.js +89 -0
  14. package/src/rules/no-invalid-permissions.js +3 -5
  15. package/src/rules/no-missing-default-locale.js +39 -14
  16. package/src/rules/no-missing-i18n-keys.js +3 -5
  17. package/src/rules/no-remote-urls.js +102 -0
  18. package/src/rules/no-scrollbar-styling.js +73 -0
  19. package/src/rules/no-sensitive-permissions.js +1 -1
  20. package/src/rules/no-unrestricted-host-permissions.js +31 -16
  21. package/src/rules/no-unused-i18n-keys.js +0 -1
  22. package/src/rules/no-unused-permissions.js +13 -4
  23. package/src/rules/require-license-file.js +0 -1
  24. package/src/rules/require-mv3.js +1 -1
  25. package/src/rules/require-short-name-when-useful.js +0 -1
  26. package/src/rules/valid-license-file.js +2 -4
  27. package/src/rules/valid-manifest-icons.js +31 -3
  28. package/src/rules/valid-mv3-schema.js +1 -1
  29. package/src/rules/valid-url-fields.js +42 -37
  30. package/src/rules/valid-version-field.js +1 -1
  31. package/src/utils/array.js +1 -0
  32. package/src/utils/extension.js +13 -0
  33. package/src/utils/i18n.js +3 -0
  34. package/src/utils/init.js +13 -7
  35. package/src/utils/license.js +3 -2
  36. package/src/data/deprecations.js +0 -10
@@ -7,7 +7,6 @@ export default {
7
7
  type: 'problem',
8
8
  docs: {
9
9
  description: 'Requires a license file in the extension',
10
- recommended: false,
11
10
  manifest: true
12
11
  },
13
12
  schema: [],
@@ -7,7 +7,7 @@ export default {
7
7
  docs: {
8
8
  description: 'Require manifest_version 3',
9
9
  category: 'Possible Errors',
10
- recommended: false,
10
+ recommended: true,
11
11
  manifest: true
12
12
  },
13
13
  schema: [],
@@ -12,7 +12,6 @@ export default {
12
12
  docs: {
13
13
  description: 'Requires a `short_name` when `name` exceeds 12 chars',
14
14
  category: 'Stylistic Issues',
15
- recommended: false,
16
15
  manifest: true
17
16
  },
18
17
  schema: [{
@@ -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 = toArray(opts.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: false,
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
- if (!regex.filename.manifest.test(extension.file.getName(context))) return {}
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
@@ -9,7 +9,7 @@ export default {
9
9
  docs: {
10
10
  description: 'Validate manifest against the MV3 schema',
11
11
  category: 'Possible Errors',
12
- recommended: false,
12
+ recommended: true,
13
13
  manifest: true
14
14
  },
15
15
  schema: [],
@@ -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
- if (!regex.filename.manifest.test(extension.file.getName(context))) return {}
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
- for (const { path, value } of collectStrings(manifest)) {
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
  }
@@ -13,7 +13,7 @@ export default {
13
13
  docs: {
14
14
  description: 'Validate the manifest version field format',
15
15
  category: 'Possible Errors',
16
- recommended: false,
16
+ recommended: true,
17
17
  manifest: true
18
18
  },
19
19
  schema: [{
@@ -0,0 +1 @@
1
+ export default { to(val) { return Array.isArray(val) ? val : val == null ? [] : [val] }}
@@ -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,
@@ -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
  }
@@ -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
- }