eslint-plugin-chrome-extensions 2.4.0 → 2.4.2

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 (35) hide show
  1. package/README.md +33 -38
  2. package/package.json +5 -3
  3. package/src/data/license.js +2 -1
  4. package/src/data/manifest.js +17 -0
  5. package/src/index.js +2 -1
  6. package/src/rules/manifest-key-order.js +2 -2
  7. package/src/rules/manifest-spacing.js +2 -2
  8. package/src/rules/no-content-style-leak.js +2 -2
  9. package/src/rules/no-deprecated-apis.js +1 -1
  10. package/src/rules/no-duplicate-manifest-keys.js +2 -2
  11. package/src/rules/no-insecure-messaging.js +1 -1
  12. package/src/rules/no-invalid-permissions.js +5 -5
  13. package/src/rules/no-missing-default-locale.js +1 -1
  14. package/src/rules/no-missing-i18n-keys.js +2 -1
  15. package/src/rules/no-missing-permissions.js +2 -1
  16. package/src/rules/no-missing-web-accessible-resources.js +3 -2
  17. package/src/rules/no-remote-code.js +1 -1
  18. package/src/rules/no-unrestricted-host-permissions.js +2 -2
  19. package/src/rules/no-unused-i18n-keys.js +1 -1
  20. package/src/rules/no-unused-permissions.js +5 -5
  21. package/src/rules/require-license-file.js +8 -6
  22. package/src/rules/require-mv3.js +2 -2
  23. package/src/rules/require-short-name-when-useful.js +2 -2
  24. package/src/rules/valid-filenames.js +1 -0
  25. package/src/rules/valid-manifest-icons.js +2 -2
  26. package/src/rules/valid-mv3-schema.js +5 -5
  27. package/src/rules/valid-service-worker.js +11 -8
  28. package/src/rules/valid-url-fields.js +4 -20
  29. package/src/rules/valid-version-field.js +2 -2
  30. package/src/utils/bundle.js +10 -0
  31. package/src/utils/extension.js +45 -0
  32. package/src/utils/node.js +22 -0
  33. package/src/utils/pkg.js +19 -0
  34. package/src/utils/rules.js +5 -0
  35. package/src/utils.js +0 -95
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <a target="_blank" href="https://www.npmjs.com/package/eslint-plugin-chrome-extensions">
6
6
  <img alt="[Latest Build]" src="https://img.shields.io/npm/v/eslint-plugin-chrome-extensions?logo=icinga&style=for-the-badge&label=Latest%20Build&color=32fcee"></a>
7
- <a href="#license">
7
+ <a href="https://codeberg.org/adamlui/eslint-plugin-chrome-extensions/src/branch/main/docs/LICENSE.md">
8
8
  <img alt="[License: MIT]" src="https://img.shields.io/badge/License-MIT-f1f20a.svg?logo=internetarchive&logoColor=white&labelColor=464646&style=for-the-badge"></a>
9
9
  <a target="_blank" href="https://eslint.org/">
10
10
  <img alt="[Requires ESLint >=9.6.0]" src="https://img.shields.io/badge/Requires-ESLint_%3E%3D9.6.0-4B32C3?style=for-the-badge&logo=eslint"></a>
@@ -40,7 +40,7 @@ import chromeExtensions from 'eslint-plugin-chrome-extensions'
40
40
  export default [
41
41
  // other configs
42
42
  {
43
- files: ['**/*extension/**/*.{cjs,js,jsx,mjs,ts,tsx}'],
43
+ files: ['**/extension/**/*.{cjs,js,jsx,mjs,ts,tsx}'],
44
44
  plugins: { 'chrome-extensions': chromeExtensions },
45
45
  rules: {
46
46
  ...chromeExtensions.configs.recommended.rules,
@@ -54,12 +54,12 @@ export default [
54
54
  'chrome-extensions/manifest-spacing': ['warn', { spaces: 4 }] // selectively tweak rule
55
55
  }
56
56
  },
57
- { files: ['**/*LICENSE*'], ...chromeExtensions.configs.license },
58
- { files: ['**/*extension/**/*.css'], ...chromeExtensions.configs.css },
59
- { files: ['**/*extension/**/*.html'], ...chromeExtensions.configs.html },
60
- { files: ['**/*extension/**/*.json'], ...chromeExtensions.configs.json },
61
- { files: ['**/*extension/**/*.json5'], ...chromeExtensions.configs.json5 },
62
- { files: ['**/*extension/**/*.jsonc'], ...chromeExtensions.configs.jsonc }
57
+ { files: ['**/*LICENSE*'], ...chromeExtensions.configs.license },
58
+ { files: ['**/extension/**/*.css'], ...chromeExtensions.configs.css },
59
+ { files: ['**/extension/**/*.html'], ...chromeExtensions.configs.html },
60
+ { files: ['**/extension/**/*.json'], ...chromeExtensions.configs.json },
61
+ { files: ['**/extension/**/*.json5'], ...chromeExtensions.configs.json5 },
62
+ { files: ['**/extension/**/*.jsonc'], ...chromeExtensions.configs.jsonc }
63
63
  ]
64
64
  ```
65
65
 
@@ -386,9 +386,8 @@ If you don't want deprecation validation at all. For individual deprecated APIs
386
386
  - "name": "Foo",
387
387
  - "name": "Bar",
388
388
  "name": "My Extension",
389
- - "version": "1.0.0",
390
- - "version": "2.0.0"
391
389
  "version": "1.0.0"
390
+ - "version": "2.0.0"
392
391
  }
393
392
  ```
394
393
 
@@ -488,7 +487,7 @@ If you intentionally target HTTP endpoints.
488
487
 
489
488
  <details>
490
489
 
491
- <summary>Requires sender/origin validation + filtered <code>tabs.query()</code> + explicit <code>window.postMessage()</code> target.</summary>
490
+ <summary>Requires sender/origin validation + filtered <code>chrome.tabs.query()</code> + explicit <code>window.postMessage()</code> target.</summary>
492
491
 
493
492
  <h4 id="no-insecure-messaging-options">Options</h4>
494
493
 
@@ -728,9 +727,9 @@ If your `_locales/` is externally populated at build time.
728
727
  <img height=18 alt="[HTML]" src="https://img.shields.io/badge/HTML-a8d8ea?style=flat-square" align="middle"></a>
729
728
  <a title="Works in 'json/json' config block" href="#usage">
730
729
  <img height=18 alt="[JSON]" src="https://img.shields.io/badge/JSON-a8e6cf?style=flat-square" align="middle"></a>
731
- <a title="Works in 'json5' config block" href="#usage">
730
+ <a title="Works in 'json/json5' config block" href="#usage">
732
731
  <img height=18 alt="[JSON5]" src="https://img.shields.io/badge/JSON5-c7e6b8?style=flat-square" align="middle"></a>
733
- <a title="Works in 'jsonc' config block" href="#usage">
732
+ <a title="Works in 'json/jsonc' config block" href="#usage">
734
733
  <img height=18 alt="[JSONC]" src="https://img.shields.io/badge/JSONC-e6e6a8?style=flat-square" align="middle"></a>
735
734
  <a title="Accepts array w/ options" href="#usage">
736
735
  <img height=18 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
@@ -760,7 +759,7 @@ If your `_locales/` is externally populated at build time.
760
759
  }
761
760
 
762
761
  // _locales/en/messages.json
763
- { "unrelated": { "message": "…" }}
762
+ { "unrelated": { "message": "sup" }}
764
763
  ```
765
764
 
766
765
  #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/green.svg" align="bottom"> Correct code
@@ -776,7 +775,7 @@ If your `_locales/` is externally populated at build time.
776
775
  }
777
776
 
778
777
  // _locales/en/messages.json
779
- - { "unrelated": { "message": "…" }}
778
+ - { "unrelated": { "message": "sup" }}
780
779
  + {
781
780
  + "greeting": { "message": "Hello" },
782
781
  + "extName": { "message": "My Extension" }
@@ -1135,8 +1134,8 @@ If your i18n keys are referenced dynamically.
1135
1134
  <a title="Works in 'css/css' config block" href="#usage"><img height=14 alt="[CSS]" src="https://img.shields.io/badge/CSS-dcd6f7?style=flat-square" align="middle"></a>
1136
1135
  <a title="Works in 'html/html' config block" href="#usage"><img height=14 alt="[HTML]" src="https://img.shields.io/badge/HTML-a8d8ea?style=flat-square" align="middle"></a>
1137
1136
  <a title="Works in 'json/json' config block" href="#usage"><img height=14 alt="[JSON]" src="https://img.shields.io/badge/JSON-a8e6cf?style=flat-square" align="middle"></a>
1138
- <a title="Works in 'json5' config block" href="#usage"><img height=14 alt="[JSON5]" src="https://img.shields.io/badge/JSON5-c7e6b8?style=flat-square" align="middle"></a>
1139
- <a title="Works in 'jsonc' config block" href="#usage"><img height=14 alt="[JSONC]" src="https://img.shields.io/badge/JSONC-e6e6a8?style=flat-square" align="middle"></a>
1137
+ <a title="Works in 'json/json5' config block" href="#usage"><img height=14 alt="[JSON5]" src="https://img.shields.io/badge/JSON5-c7e6b8?style=flat-square" align="middle"></a>
1138
+ <a title="Works in 'json/jsonc' config block" href="#usage"><img height=14 alt="[JSONC]" src="https://img.shields.io/badge/JSONC-e6e6a8?style=flat-square" align="middle"></a>
1140
1139
  <a title="Accepts array w/ options" href="#no-missing-i18n-keys-options"><img height=14 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
1141
1140
  - <img height=12 src="https://www.google.com/s2/favicons?domain=developer.chrome.com&sz=64">&ensp;<a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/api/i18n">Chrome for Developers — `i18n`</a>
1142
1141
 
@@ -1228,20 +1227,18 @@ If you access the granted API through an alias or computed property rather than
1228
1227
 
1229
1228
  <summary>Requires a license file in the extension.</summary>
1230
1229
 
1231
- #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/gray.svg" align="bottom"> Incorrect code
1230
+ #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/gray.svg" align="bottom"> Incorrect directory structure
1232
1231
 
1233
1232
  ```diff
1234
- // files
1235
1233
  /extension/manifest.json
1236
1234
  !
1237
1235
  ```
1238
1236
 
1239
- #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/green.svg" align="bottom"> Correct code
1237
+ #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/green.svg" align="bottom"> Correct directory structure
1240
1238
 
1241
1239
  ```diff
1242
- // files
1243
1240
  /extension/manifest.json
1244
- + /extension/LICENSE
1241
+ + /extension/LICENSE.md
1245
1242
  ```
1246
1243
 
1247
1244
  #### When not to use it
@@ -1255,7 +1252,7 @@ If you intentionally omit a license, or it's injected at build time.
1255
1252
  #### See also
1256
1253
 
1257
1254
  - [`valid-license-file`](#valid-license-file)
1258
- <a title="Only works in 'license' config block" href="#usage"><img height=14 alt="[License only]" src="https://img.shields.io/badge/License_only-559bff?style=flat-square" align="middle"></a>
1255
+ <a title="Only works in license config block" href="#usage"><img height=14 alt="[License only]" src="https://img.shields.io/badge/License_only-559bff?style=flat-square" align="middle"></a>
1259
1256
  <a title="Accepts array w/ options" href="#valid-license-file-options"><img height=14 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
1260
1257
 
1261
1258
  <div align="center">
@@ -1403,9 +1400,9 @@ If you primarily target a browser that doesn't support `short_name`.
1403
1400
  <img height=18 alt="[HTML]" src="https://img.shields.io/badge/HTML-a8d8ea?style=flat-square" align="middle"></a>
1404
1401
  <a title="Works in 'json/json' config block" href="#usage">
1405
1402
  <img height=18 alt="[JSON]" src="https://img.shields.io/badge/JSON-a8e6cf?style=flat-square" align="middle"></a>
1406
- <a title="Works in 'json5' config block" href="#usage">
1403
+ <a title="Works in 'json/json5' config block" href="#usage">
1407
1404
  <img height=18 alt="[JSON5]" src="https://img.shields.io/badge/JSON5-c7e6b8?style=flat-square" align="middle"></a>
1408
- <a title="Works in 'jsonc' config block" href="#usage">
1405
+ <a title="Works in 'json/jsonc' config block" href="#usage">
1409
1406
  <img height=18 alt="[JSONC]" src="https://img.shields.io/badge/JSONC-e6e6a8?style=flat-square" align="middle"></a>
1410
1407
  <a title="Accepts array w/ options" href="#usage">
1411
1408
  <img height=18 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
@@ -1485,7 +1482,7 @@ If you don't use conventional filenames or paths, or they're generated at build
1485
1482
 
1486
1483
  <h3 id="valid-license-file">
1487
1484
  <code>valid-license-file</code>
1488
- <a title="Only works in 'license' config block" href="#usage">
1485
+ <a title="Only works in license config block" href="#usage">
1489
1486
  <img height=18 alt="[License only]" src="https://img.shields.io/badge/License_only-559bff?style=flat-square" align="middle"></a>
1490
1487
  <a title="Accepts array w/ options" href="#usage">
1491
1488
  <img height=18 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
@@ -1508,13 +1505,9 @@ If you don't use conventional filenames or paths, or they're generated at build
1508
1505
  ```diff
1509
1506
  /* eslint chrome-extensions/valid-license-file: ['error', { requireSPDX: true, allowedLicenses: ['MIT'] }] */
1510
1507
 
1511
- // LICENSE.md
1512
-
1513
- MIT License
1514
-
1508
+ ! MIT License
1509
+ -----------
1515
1510
  Copyright © 2026 You
1516
-
1517
- !
1518
1511
  ```
1519
1512
 
1520
1513
  #### <img width=13 height=13 src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/99f14247b2/assets/images/icons/square/with-symbol/green.svg" align="bottom"> Correct code
@@ -1522,15 +1515,17 @@ If you don't use conventional filenames or paths, or they're generated at build
1522
1515
  ```diff
1523
1516
  /* eslint chrome-extensions/valid-license-file: ['error', { requireSPDX: true, allowedLicenses: ['MIT'] }] */
1524
1517
 
1525
- // LICENSE.md
1526
-
1527
1518
  MIT License
1528
-
1519
+ -----------
1529
1520
  Copyright © 2026 You
1530
-
1521
+ +
1531
1522
  + SPDX-License-Identifier: MIT
1532
1523
  ```
1533
1524
 
1525
+ #### Benefits
1526
+
1527
+ Makes the license machine-readable by compliance scanners + SBOM generators + CI gates.
1528
+
1534
1529
  #### When not to use it
1535
1530
 
1536
1531
  If your license is injected at build time, or you use a license w/ no SPDX identifier.
@@ -1776,7 +1771,7 @@ If your background context isn't a service worker.
1776
1771
  <h4 id="valid-url-fields-options">Options</h4>
1777
1772
 
1778
1773
  - `checkURLfields` (default `true`) – validate URL fields (<a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/manifest/chrome-settings-override">`chrome_settings_overrides`</a>, <a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/manifest/homepage-url">`homepage_url`</a>, <a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/manifest/oauth2">`oauth2.scopes`</a>, <a target="_blank" href="https://developer.chrome.com/docs/apps/autoupdate#update_url">`update_url`</a>)
1779
- - `checkMatchPatterns` (default `true`) – validate match patterns (in <a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/manifest/content-scripts">`content_scripts`</a>, <a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/manifest/externally-connectable">`externally_connectable`</a>, <a target="_blank" href="https://developer.chrome.com/docs/extensions/develop/concepts/declare-permissions#host-permissions">`host_permissions`</a>, <a target="_blank" href="https://developer.chrome.com/docs/extensions/develop/concepts/declare-permissions">`optional_host_permissions`</a>, <a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/manifest/web-accessible-resources">`web_accessible_resources`</a>)
1774
+ - `checkMatchPatterns` (default `true`) – validate <a target="_blank" href="https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns">match patterns</a> (in <a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/manifest/content-scripts">`content_scripts`</a>, <a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/manifest/externally-connectable">`externally_connectable`</a>, <a target="_blank" href="https://developer.chrome.com/docs/extensions/develop/concepts/declare-permissions#host-permissions">`host_permissions`</a>, <a target="_blank" href="https://developer.chrome.com/docs/extensions/develop/concepts/declare-permissions">`optional_host_permissions`</a>, <a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/manifest/web-accessible-resources">`web_accessible_resources`</a>)
1780
1775
  - `allowedSchemes` (default `['https', 'file']`) – permitted URL schemes for <a target="_blank" href="https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns">match patterns</a>
1781
1776
  - `allowAllURLs` (default `false`) – allow unrestricted patterns (<a target="_blank" href="https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns">`<all_urls>`</a>, <a target="_blank" href="https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns">`https://*/*`</a>, <a target="_blank" href="https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns">`*://*/*`</a>)
1782
1777
 
@@ -1810,7 +1805,7 @@ If your background context isn't a service worker.
1810
1805
 
1811
1806
  #### When not to use it
1812
1807
 
1813
- If your URLs or match patterns are generated at build time.
1808
+ If your URLs or patterns are generated at build time.
1814
1809
 
1815
1810
  #### Introduced in
1816
1811
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-chrome-extensions",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
4
4
  "description": "ESLint rules for Chrome extensions",
5
5
  "keywords": [
6
6
  "eslint",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "repository": {
19
19
  "type": "git",
20
- "url": "git+https://codeberg.org/adamlui/eslint-plugin-chrome-extensions.git"
20
+ "url": "https://codeberg.org/adamlui/eslint-plugin-chrome-extensions.git"
21
21
  },
22
22
  "funding": [
23
23
  {
@@ -66,12 +66,14 @@
66
66
  },
67
67
  "devDependencies": {
68
68
  "@e18e/eslint-plugin": "^0.8.1",
69
+ "@eslint/css": "^2.0.0",
69
70
  "@eslint/js": "^10.0.1",
70
71
  "@eslint/json": "^2.1.1",
71
72
  "@eslint/markdown": "^7.5.1",
73
+ "@html-eslint/eslint-plugin": "^0.66.1",
72
74
  "@stylistic/eslint-plugin": "^5.10.0",
73
75
  "eslint": "^10.12.0",
74
- "eslint-plugin-constructors": "^1.4.2",
76
+ "eslint-plugin-constructors": "^1.4.3",
75
77
  "eslint-plugin-eslint-plugin": "^7.6.2",
76
78
  "eslint-plugin-import-x": "^4.17.1",
77
79
  "eslint-plugin-n": "^18.4.1",
@@ -3,7 +3,8 @@ import path from 'node:path'
3
3
 
4
4
  export const re = {
5
5
  licenseFilename: // e.g. LICENSE.md, LICENSE-MIT, MIT-LICENSE.md, COPYING.LESSER, LICENSE-2.0.txt
6
- /^(?:[\w.-]*-)?(?:LICEN[SC]E|COPYING)(?:[-.][\w.-]+)?$/i, // eslint-disable-line security/detect-unsafe-regex
6
+ // eslint-disable-next-line security/detect-unsafe-regex
7
+ /^(?!.*\.(?:[cm]?js|[cm]?ts)x?$)(?:[\w.-]*-)?(?:LICEN[SC]E|COPYING)(?:[-.][\w.-]+)?$/i,
7
8
  spdxTag: /SPDX-License-Identifier:\s*([\w.+-]+)/i
8
9
  }
9
10
 
@@ -36,9 +36,26 @@ export const manifestKeyOrder = [
36
36
  'theme_experiment', 'tts_engine', 'update_url', 'version_name', 'web_accessible_resources'
37
37
  ]
38
38
 
39
+ export const matchPaths = new Set([
40
+ 'content_scripts.matches', 'externally_connectable.matches',
41
+ 'host_permissions', 'optional_host_permissions', 'web_accessible_resources.matches'
42
+ ])
43
+
39
44
  export const mv2OnlyKeys = new Set(['browser_action', 'page_action'])
40
45
 
41
46
  export const remoteCodePatterns = [
42
47
  'eval', 'new-function', 'string-timer', 'dynamic-import', 'remote-script-src', 'execute-script-url']
43
48
 
44
49
  export const serviceWorkerKeepaliveMinMS = 30000
50
+
51
+ export const urlPaths = new Set([
52
+ 'homepage_url', 'update_url', 'oauth2.scopes',
53
+ 'chrome_settings_overrides.homepage',
54
+ 'chrome_settings_overrides.search_provider.alternate_urls',
55
+ 'chrome_settings_overrides.search_provider.favicon_url',
56
+ 'chrome_settings_overrides.search_provider.image_url',
57
+ 'chrome_settings_overrides.search_provider.instant_url',
58
+ 'chrome_settings_overrides.search_provider.search_url',
59
+ 'chrome_settings_overrides.search_provider.suggest_url',
60
+ 'chrome_settings_overrides.startup_pages'
61
+ ])
package/src/index.js CHANGED
@@ -31,7 +31,8 @@ import validServiceWorker from './rules/valid-service-worker.js'
31
31
  import validURLfields from './rules/valid-url-fields.js'
32
32
  import validVersionField from './rules/valid-version-field.js'
33
33
 
34
- import { bundleRules, initBundleDeprecation } from './utils.js'
34
+ import { bundle as bundleRules } from './utils/rules.js'
35
+ import { initDeprecation as initBundleDeprecation } from './utils/bundle.js'
35
36
  import { textParser } from './data/text-parser.js'
36
37
 
37
38
  const plugin = {
@@ -1,5 +1,5 @@
1
1
  import { manifestKeyOrder } from '../data/manifest.js'
2
- import { getFilename } from '../utils.js'
2
+ import { file } from '../utils/extension.js'
3
3
 
4
4
  const re = { manifestFilename: /manifest\.json$/ }
5
5
 
@@ -43,7 +43,7 @@ export default {
43
43
  },
44
44
 
45
45
  create(context) {
46
- if (!re.manifestFilename.test(getFilename(context))) return {}
46
+ if (!re.manifestFilename.test(file.getName(context))) return {}
47
47
  const custom = context.options[0]?.order ?? [],
48
48
  master = custom.length ? manifestKeyOrder.filter(key => !custom.includes(key))
49
49
  : manifestKeyOrder
@@ -1,4 +1,4 @@
1
- import { getFilename } from '../utils.js'
1
+ import { file } from '../utils/extension.js'
2
2
 
3
3
  const re = { manifestFilename: /manifest\.json$/, leadingWhitespace: /^[ \t]+/ },
4
4
  brackets = { open: new Set(['LBrace', 'LBracket']), close: new Set(['RBrace', 'RBracket']) }
@@ -57,7 +57,7 @@ export default {
57
57
  },
58
58
 
59
59
  create(context) {
60
- if (!re.manifestFilename.test(getFilename(context))) return {}
60
+ if (!re.manifestFilename.test(file.getName(context))) return {}
61
61
  const opts = context.options[0] || {},
62
62
  spaces = opts.spaces ?? 2,
63
63
  checkColons = opts.checkColons ?? true,
@@ -1,4 +1,4 @@
1
- import { readManifest, isContentScriptFile } from '../utils.js'
1
+ import { file, readManifest } from '../utils/extension.js'
2
2
 
3
3
  const re = { appendMethod: /^append(?:Child)?$/ }
4
4
 
@@ -17,7 +17,7 @@ export default {
17
17
  },
18
18
 
19
19
  create(context) {
20
- const manifest = readManifest(context) ; if (!manifest || !isContentScriptFile(context, manifest)) return {}
20
+ const manifest = readManifest(context) ; if (!manifest || !file.isContentScript(context, manifest)) return {}
21
21
  const styleElems = new Set(), docAliases = new Set()
22
22
  function isCreateStyle(node) {
23
23
  return node?.type == 'CallExpression'
@@ -1,5 +1,5 @@
1
1
  import { findDeprecated } from '../data/apis.js'
2
- import { getMemberPath } from '../utils.js'
2
+ import { getMemberPath } from '../utils/node.js'
3
3
 
4
4
  export default {
5
5
  meta: {
@@ -1,4 +1,4 @@
1
- import { getFilename } from '../utils.js'
1
+ import { file } from '../utils/extension.js'
2
2
 
3
3
  const re = { manifestFilename: /manifest\.json$/ }
4
4
 
@@ -18,7 +18,7 @@ export default {
18
18
  },
19
19
 
20
20
  create(context) {
21
- if (!re.manifestFilename.test(getFilename(context))) return {}
21
+ if (!re.manifestFilename.test(file.getName(context))) return {}
22
22
  return {
23
23
  Object(node) {
24
24
  const members = node.members || node.properties || [],
@@ -1,4 +1,4 @@
1
- import { getMemberPath } from '../utils.js'
1
+ import { getMemberPath } from '../utils/node.js'
2
2
  import { isExternalMessagingListener } from '../data/apis.js'
3
3
 
4
4
  const re = { postmsgValidation: /\b\w+\.(?:origin|source)\b/, senderValidation: /sender\.(?:id|origin)/ }
@@ -1,4 +1,4 @@
1
- import { getFilename, readManifest } from '../utils.js'
1
+ import { file, readManifest } from '../utils/extension.js'
2
2
  import { isValidPermission } from '../data/permissions.js'
3
3
 
4
4
  const re = { manifestFilename: /manifest\.json$/ }
@@ -49,16 +49,16 @@ export default {
49
49
  if (allowed.has(permission) || isValidPermission(permission, target)) return
50
50
  context.report({ node, messageId: 'invalidPermission', data: { permission, target }})
51
51
  }
52
- if (re.manifestFilename.test(getFilename(context))) // lint it directly
52
+ if (re.manifestFilename.test(file.getName(context))) // lint it directly
53
53
  return {
54
54
  '*'(node) {
55
55
  const keyNode = node.name || node.key ; if (!keyNode) return
56
56
  const keyName = keyNode.value ?? keyNode.name
57
57
  if (keyName != 'permissions' && keyName != 'optional_permissions') return
58
58
  const arrNode = node.value ; if (arrNode?.type != 'Array') return
59
- const elements = arrNode.elements || arrNode.items || []
60
- for (const el of elements) {
61
- const valueNode = el.value || el
59
+ const elems = arrNode.elements || arrNode.items || []
60
+ for (const elem of elems) {
61
+ const valueNode = elem.value || elem
62
62
  checkPermission(valueNode, valueNode?.value)
63
63
  }
64
64
  }
@@ -4,7 +4,7 @@ import fs from 'node:fs'
4
4
  import path from 'node:path'
5
5
 
6
6
  import { findExtensionRoot, isPredefined } from '../data/i18n.js'
7
- import { readManifest } from '../utils.js'
7
+ import { readManifest } from '../utils/extension.js'
8
8
 
9
9
  const re = { manifestFile: /manifest\.json$/ }
10
10
 
@@ -1,4 +1,5 @@
1
- import { getMemberPath, readManifest } from '../utils.js'
1
+ import { getMemberPath } from '../utils/node.js'
2
+ import { readManifest } from '../utils/extension.js'
2
3
  import { findExtensionRoot, isPredefined, readDefaultLocaleMessages, re as i18nRe } from '../data/i18n.js'
3
4
 
4
5
  const re = { file: { js: /\.(cjs|js|jsx|mjs|ts|tsx)$/i, manifest: /manifest\.json$/, text: /\.(css|htm|html)$/i }}
@@ -1,5 +1,6 @@
1
1
  import { findPermission } from '../data/apis.js'
2
- import { getMemberPath, readManifest } from '../utils.js'
2
+ import { getMemberPath } from '../utils/node.js'
3
+ import { readManifest } from '../utils/extension.js'
3
4
 
4
5
  function queriesActiveTab(node) {
5
6
  const arg = node.arguments[0] ; if (arg?.type != 'ObjectExpression') return false
@@ -1,4 +1,5 @@
1
- import { getMemberPath, isContentScriptFile, readManifest } from '../utils.js'
1
+ import { getMemberPath } from '../utils/node.js'
2
+ import { file, readManifest } from '../utils/extension.js'
2
3
 
3
4
  const re = { leadingSlash: /^\//, regexSpecial: /[.+?^${}()|[\]\\]/g }
4
5
 
@@ -35,7 +36,7 @@ export default {
35
36
 
36
37
  create(context) {
37
38
  const manifest = readManifest(context) ; if (!manifest) return {}
38
- if (!isContentScriptFile(context, manifest)) return {}
39
+ if (!file.isContentScript(context, manifest)) return {}
39
40
  const regexes = collectWARpatterns(manifest).map(patternToRegex)
40
41
  return {
41
42
  CallExpression(node) {
@@ -1,5 +1,5 @@
1
1
  import { remoteCodePatterns } from '../data/manifest.js'
2
- import { getMemberPath } from '../utils.js'
2
+ import { getMemberPath } from '../utils/node.js'
3
3
 
4
4
  const re = { url: /^https?:\/\// }
5
5
 
@@ -1,4 +1,4 @@
1
- import { getFilename } from '../utils.js'
1
+ import { file } from '../utils/extension.js'
2
2
 
3
3
  const re = { broadHost: /^(?:<all_urls>|\*:\/\/\*\/\*|\w+:\/\/\*\/\*)$/, manifestFilename: /manifest\.json$/ },
4
4
  hostKeys = new Set(['host_permissions', 'optional_host_permissions', 'optional_permissions', 'permissions'])
@@ -31,7 +31,7 @@ export default {
31
31
  },
32
32
 
33
33
  create(context) {
34
- if (!re.manifestFilename.test(getFilename(context))) return {}
34
+ if (!re.manifestFilename.test(file.getName(context))) return {}
35
35
  const allowed = new Set(context.options[0]?.allowedPatterns || [])
36
36
  return {
37
37
  '*'(node) {
@@ -1,5 +1,5 @@
1
- import { readManifest } from '../utils.js'
2
1
  import { collectUsedKeys, defaultScanExtensions, findExtensionRoot, readDefaultLocaleMessages } from '../data/i18n.js'
2
+ import { readManifest } from '../utils/extension.js'
3
3
 
4
4
  const re = { manifestFile: /manifest\.json$/ }
5
5
 
@@ -2,7 +2,7 @@ import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
 
4
4
  import { apiPermissionMap, findPermission } from '../data/apis.js'
5
- import { findExtensionRoot, getFilename, readManifest } from '../utils.js'
5
+ import { file, findRoot as findExtensionRoot, readManifest } from '../utils/extension.js'
6
6
 
7
7
  const re = { manifestFilename: /manifest\.json$/ }
8
8
 
@@ -44,7 +44,7 @@ export default {
44
44
  },
45
45
 
46
46
  create(context) {
47
- if (!re.manifestFilename.test(getFilename(context))) return {}
47
+ if (!re.manifestFilename.test(file.getName(context))) return {}
48
48
  const manifest = readManifest(context) ; if (!manifest) return {}
49
49
  const root = findExtensionRoot(context) ; if (!root) return {}
50
50
  const usedAPIs = collectUsedAPIs(manifest, root)
@@ -64,9 +64,9 @@ export default {
64
64
  const keyName = keyNode.value ?? keyNode.name
65
65
  if (keyName != 'permissions' && keyName != 'optional_permissions') return
66
66
  const arrNode = node.value ; if (arrNode?.type != 'Array') return
67
- const elements = arrNode.elements || arrNode.items || []
68
- for (const el of elements) {
69
- const valueNode = el.value || el
67
+ const elems = arrNode.elements || arrNode.items || []
68
+ for (const elem of elems) {
69
+ const valueNode = elem.value || elem
70
70
  const perm = valueNode?.value
71
71
  if (typeof perm != 'string') continue
72
72
  if (!permissionNodes.has(perm)) permissionNodes.set(perm, [])
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path'
2
2
 
3
3
  import { findLicenseFile } from '../data/license.js'
4
+ import { findRoot as findPkgRoot } from '../utils/pkg.js'
4
5
 
5
6
  export default {
6
7
  meta: {
@@ -13,15 +14,16 @@ export default {
13
14
  schema: [],
14
15
  messages: {
15
16
  missingFile: 'No license file found. Add one of: LICENSE, LICENSE.md, LICENSE.txt, COPYING '
16
- + '(at extension root or in a subdir).'
17
+ + '(at the package root or in a subdir).'
17
18
  }
18
19
  },
19
20
  create(context) {
20
- const { filename } = context,
21
- check = node => {
22
- if (!findLicenseFile(path.dirname(filename)))
23
- context.report({ node, messageId: 'missingFile' })
24
- }
21
+ const { filename } = context
22
+ function check(node) {
23
+ const pkgRoot = findPkgRoot(path.dirname(filename))
24
+ if (!pkgRoot || !findLicenseFile(pkgRoot))
25
+ context.report({ node, messageId: 'missingFile' })
26
+ }
25
27
  return { Program: check, Document: check }
26
28
  }
27
29
  }
@@ -1,4 +1,4 @@
1
- import { getFilename, readManifest } from '../utils.js'
1
+ import { file, readManifest } from '../utils/extension.js'
2
2
 
3
3
  const re = { manifestFilename: /manifest\.json$/ }
4
4
 
@@ -18,7 +18,7 @@ export default {
18
18
  },
19
19
 
20
20
  create(context) {
21
- if (re.manifestFilename.test(getFilename(context))) { // lint it directly
21
+ if (re.manifestFilename.test(file.getName(context))) { // lint it directly
22
22
  let foundVersion = null, versionNode = null
23
23
  return {
24
24
  '*'(node) {
@@ -1,4 +1,4 @@
1
- import { getFilename } from '../utils.js'
1
+ import { file } from '../utils/extension.js'
2
2
 
3
3
  const defaults = { maxNameLength: 12 },
4
4
  re = { i18nPlaceholder: /^__MSG_.+__$/, manifestFilename: /manifest\.json$/ }
@@ -43,7 +43,7 @@ export default {
43
43
  },
44
44
 
45
45
  create(context) {
46
- if (!re.manifestFilename.test(getFilename(context))) return {}
46
+ if (!re.manifestFilename.test(file.getName(context))) return {}
47
47
  const opts = context.options[0] || {},
48
48
  maxNameLength = opts.maxNameLength ?? defaults.maxNameLength,
49
49
  always = opts.always ?? false,
@@ -62,6 +62,7 @@ export default {
62
62
  category: 'Possible Errors',
63
63
  recommended: true,
64
64
  manifest: true,
65
+ css: true,
65
66
  html: true,
66
67
  json: true
67
68
  },
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
 
4
- import { findExtensionRoot, getFilename } from '../utils.js'
4
+ import { findRoot as findExtensionRoot, file } from '../utils/extension.js'
5
5
 
6
6
  const re = { manifestFilename: /manifest\.json$/, svg: /\.svg$/i },
7
7
  sizes = { recommended: ['16', '32', '48', '128'], required: '128' }
@@ -47,7 +47,7 @@ export default {
47
47
  },
48
48
 
49
49
  create(context) {
50
- if (!re.manifestFilename.test(getFilename(context))) return {}
50
+ if (!re.manifestFilename.test(file.getName(context))) return {}
51
51
  const opts = context.options[0] || {},
52
52
  requireRecommended = opts.requireRecommendedSizes !== false,
53
53
  checkFiles = opts.checkIconExists !== false,
@@ -1,7 +1,7 @@
1
1
  import { mv2OnlyKeys, forbiddenCSPdirectives, ffMV3bgKeys, ffDataCollectionRequired, ffDataCollectionOptional }
2
2
  from '../data/manifest.js'
3
3
 
4
- import { getFilename, readManifest } from '../utils.js'
4
+ import { file, readManifest } from '../utils/extension.js'
5
5
 
6
6
  const re = { manifestFilename: /manifest\.json$/ }
7
7
 
@@ -24,7 +24,7 @@ export default {
24
24
  },
25
25
 
26
26
  create(context) {
27
- if (re.manifestFilename.test(getFilename(context))) { // lint it directly
27
+ if (re.manifestFilename.test(file.getName(context))) { // lint it directly
28
28
  const ancestors = [], pendingReports = [], deferredMV2 = []
29
29
  let isMV3 = false, isFF = false
30
30
  function keyOf(node) {
@@ -64,12 +64,12 @@ export default {
64
64
  data: { directive }})
65
65
  if (inDataCollection && (keyName == 'required' || keyName == 'optional')
66
66
  && Array.isArray(node.value?.elements)
67
- ) for (const el of node.value.elements) {
68
- const value = typeof el.value == 'string' ? el.value : el.value?.value
67
+ ) for (const elem of node.value.elements) {
68
+ const value = typeof elem.value == 'string' ? elem.value : elem.value?.value
69
69
  if (typeof value == 'string') {
70
70
  const set = keyName == 'required' ? ffDataCollectionRequired : ffDataCollectionOptional
71
71
  if (!set.has(value))
72
- pendingReports.push({ node: el, messageId: 'invalidDataCollectionValue',
72
+ pendingReports.push({ node: elem, messageId: 'invalidDataCollectionValue',
73
73
  data: { value, kind: keyName }})
74
74
  }
75
75
  }
@@ -1,5 +1,7 @@
1
1
  import { domGlobals, serviceWorkerKeepaliveMinMS } from '../data/manifest.js'
2
- import { isServiceWorkerFile, isInsideNestedFunction, readManifest } from '../utils.js'
2
+
3
+ import { isInsideNestedFunction } from '../utils/node.js'
4
+ import { file, readManifest } from '../utils/extension.js'
3
5
 
4
6
  export default {
5
7
  meta: {
@@ -19,7 +21,7 @@ export default {
19
21
  },
20
22
 
21
23
  create(context) {
22
- const manifest = readManifest(context) ; if (!manifest || !isServiceWorkerFile(context, manifest)) return {}
24
+ const manifest = readManifest(context) ; if (!manifest || !file.isServiceWorker(context, manifest)) return {}
23
25
  return {
24
26
  CallExpression(node) {
25
27
  if (node.callee.type == 'MemberExpression'
@@ -35,12 +37,13 @@ export default {
35
37
  ) context.report({ node, messageId: 'keepaliveTimer', data: { fn: node.callee.name }})
36
38
  },
37
39
  Identifier(node) {
38
- if (node.parent?.type == 'MemberExpression' && node.parent.property === node) return
39
- if (node.parent?.type == 'Property' && node.parent.key === node && !node.parent.computed) return
40
- if (!domGlobals.has(node.name)) return
41
- for (let scope = context.sourceCode.getScope(node) ;
42
- scope && scope.type != 'global' ;
43
- scope = scope.upper
40
+ if (node.parent?.type == 'MemberExpression' && node.parent.property === node
41
+ || node.parent?.type == 'Property' && node.parent.key === node && !node.parent.computed
42
+ || !domGlobals.has(node.name)
43
+ ) return
44
+ for (let scope = context.sourceCode.getScope(node)
45
+ ;scope && scope.type != 'global'
46
+ ;scope = scope.upper
44
47
  ) if (scope.set.has(node.name)) return // shadowed by a local binding
45
48
  context.report({ node, messageId: 'domInSW', data: { global: node.name }})
46
49
  }
@@ -1,32 +1,16 @@
1
- import { getFilename } from '../utils.js'
1
+ import { matchPaths, urlPaths } from '../data/manifest.js'
2
+ import { file } from '../utils/extension.js'
2
3
 
3
4
  const re = {
4
5
  absoluteURL: /^[a-z][a-z0-9+.-]*:\/\/[^\s/]+/i, manifestFilename: /manifest\.json$/,
5
6
  match: /^(\*|https?|file|ftp):\/\/([^/]*)(\/.*)$/, template: /\{[^}]+\}/g
6
7
  }
7
8
 
8
- const matchPaths = new Set([
9
- 'content_scripts.matches', 'externally_connectable.matches',
10
- 'host_permissions', 'optional_host_permissions', 'web_accessible_resources.matches'
11
- ])
12
-
13
- const urlPaths = new Set([
14
- 'homepage_url', 'update_url', 'oauth2.scopes',
15
- 'chrome_settings_overrides.homepage',
16
- 'chrome_settings_overrides.search_provider.alternate_urls',
17
- 'chrome_settings_overrides.search_provider.favicon_url',
18
- 'chrome_settings_overrides.search_provider.image_url',
19
- 'chrome_settings_overrides.search_provider.instant_url',
20
- 'chrome_settings_overrides.search_provider.search_url',
21
- 'chrome_settings_overrides.search_provider.suggest_url',
22
- 'chrome_settings_overrides.startup_pages'
23
- ])
24
-
25
9
  function collectStrings(obj, path = [], out = []) {
26
10
  if (obj == null) return out
27
11
  if (typeof obj == 'string') out.push({ path: path.join('.'), value: obj })
28
12
  else if (Array.isArray(obj))
29
- for (const el of obj) collectStrings(el, path, out)
13
+ for (const elem of obj) collectStrings(elem, path, out)
30
14
  else if (typeof obj == 'object')
31
15
  for (const key of Object.keys(obj)) // eslint-disable-next-line security/detect-object-injection
32
16
  collectStrings(obj[key], [...path, key], out)
@@ -83,7 +67,7 @@ export default {
83
67
  },
84
68
 
85
69
  create(context) {
86
- if (!re.manifestFilename.test(getFilename(context))) return {}
70
+ if (!re.manifestFilename.test(file.getName(context))) return {}
87
71
  const opts = context.options[0] || {},
88
72
  checkURLfields = opts.checkURLfields ?? true,
89
73
  checkMatchPatterns = opts.checkMatchPatterns ?? true,
@@ -1,4 +1,4 @@
1
- import { getFilename, readManifest } from '../utils.js'
1
+ import { file, readManifest } from '../utils/extension.js'
2
2
 
3
3
  const re = { /* eslint-disable security/detect-unsafe-regex */
4
4
  chromeParts: /^\d+(?:\.\d+){0,3}$/,
@@ -79,7 +79,7 @@ export default {
79
79
  }
80
80
  }
81
81
  }
82
- if (re.manifestFilename.test(getFilename(context))) // lint it directly
82
+ if (re.manifestFilename.test(file.getName(context))) // lint it directly
83
83
  return {
84
84
  '*'(node) {
85
85
  const keyNode = node.name || node.key ; if (!keyNode) return
@@ -0,0 +1,10 @@
1
+ export function initDeprecation(plugin, [ruleId, bundleNames], pluginName) {
2
+ for (const bundleName of bundleNames) {
3
+ /* eslint-disable security/detect-object-injection */
4
+ if (plugin.configs[bundleName])
5
+ plugin.configs[bundleName].rules[`${pluginName}/${ruleId}`] = 'error'
6
+ /* eslint-enable security/detect-object-injection */
7
+ if (process.env.DEBUG_MODE)
8
+ console.warn(`[${pluginName}] '${ruleId}' will be removed from '${bundleName}' in a future major release`)
9
+ }
10
+ }
@@ -0,0 +1,45 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ const manifestCache = new Map()
5
+
6
+ export function findRoot(context) {
7
+ let dir = path.dirname(file.getName(context))
8
+ while (dir) {
9
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
10
+ if (fs.existsSync(path.join(dir, 'manifest.json'))) return dir
11
+ const parent = path.dirname(dir)
12
+ if (parent == dir) break // reached fs root
13
+ dir = parent
14
+ }
15
+ return null
16
+ }
17
+
18
+ export const file = {
19
+ getName(context) { return context.filename || context.getFilename() },
20
+
21
+ isContentScript(context, manifest) {
22
+ const scripts = (manifest?.content_scripts || []).flatMap(cs => cs.js || []) ; if (!scripts.length) return false
23
+ const root = findRoot(context) ; if (!root) return false
24
+ const filename = path.resolve(file.getName(context))
25
+ return scripts.some(script => filename == path.resolve(root, script))
26
+ },
27
+
28
+ isServiceWorker(context, manifest) {
29
+ const worker = manifest?.background?.service_worker ; if (!worker) return false
30
+ const root = findRoot(context) ; if (!root) return false
31
+ return path.resolve(file.getName(context)) == path.resolve(root, worker)
32
+ }
33
+ }
34
+
35
+ export function readManifest(context) {
36
+ const root = findRoot(context)
37
+ if (!root) return null
38
+ if (manifestCache.has(root)) return manifestCache.get(root)
39
+ let parsed
40
+ try { // eslint-disable-next-line security/detect-non-literal-fs-filename
41
+ parsed = JSON.parse(fs.readFileSync(path.join(root, 'manifest.json'), 'utf8'))
42
+ } catch { parsed = null }
43
+ manifestCache.set(root, parsed)
44
+ return parsed
45
+ }
@@ -0,0 +1,22 @@
1
+ export function getMemberPath(node) {
2
+ const parts = []
3
+ let cur = node
4
+ while (cur?.type == 'MemberExpression') {
5
+ if (cur.computed || cur.property.type != 'Identifier') return null
6
+ parts.unshift(cur.property.name)
7
+ cur = cur.object
8
+ }
9
+ if (cur?.type != 'Identifier') return null
10
+ parts.unshift(cur.name)
11
+ return parts.join('.')
12
+ }
13
+
14
+ export function isInsideNestedFunction(node) {
15
+ let current = node.parent
16
+ while (current) {
17
+ if (['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression'].includes(current.type))
18
+ return true
19
+ current = current.parent
20
+ }
21
+ return false
22
+ }
@@ -0,0 +1,19 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ export function findRoot(dir) {
5
+ let current = path.resolve(dir)
6
+ while (true) {
7
+ if (current.split(path.sep).includes('node_modules')) {
8
+ const parent = path.dirname(current)
9
+ if (parent === current) return null
10
+ current = parent
11
+ continue
12
+ }
13
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
14
+ if (fs.existsSync(path.join(current, 'package.json'))) return current
15
+ const parent = path.dirname(current)
16
+ if (parent === current) return null
17
+ current = parent
18
+ }
19
+ }
@@ -0,0 +1,5 @@
1
+ export function bundle(rules, predicate, pluginName) {
2
+ return Object.fromEntries(Object.entries(rules)
3
+ .filter(([, rule]) => predicate(rule))
4
+ .map(([ruleId]) => [`${pluginName}/${ruleId}`, 'error']))
5
+ }
package/src/utils.js DELETED
@@ -1,95 +0,0 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
-
4
- const re = { chromeAPI: /\bchrome(?:\.\w+)+/g }, // eslint-disable-line security/detect-unsafe-regex
5
- manifestCache = new Map()
6
-
7
- export function bundleRules(rules, predicate, pluginName) {
8
- return Object.fromEntries(Object.entries(rules)
9
- .filter(([, rule]) => predicate(rule))
10
- .map(([ruleId]) => [`${pluginName}/${ruleId}`, 'error']))
11
- }
12
-
13
- export function findExtensionRoot(context) {
14
- let dir = path.dirname(getFilename(context))
15
- while (dir) {
16
- // eslint-disable-next-line security/detect-non-literal-fs-filename
17
- if (fs.existsSync(path.join(dir, 'manifest.json'))) return dir
18
- const parent = path.dirname(dir)
19
- if (parent == dir) break // reached filesystem root or a stuck relative "."
20
- dir = parent
21
- }
22
- return null
23
- }
24
-
25
- export function getChromeAPIcalls(sourceCode) {
26
- const calls = new Set()
27
- for (const match of sourceCode.getText().matchAll(re.chromeAPI))
28
- calls.add(match[0])
29
- return calls
30
- }
31
-
32
- export function getFilename(context) { return context.filename || context.getFilename() }
33
-
34
- export function getMemberPath(node) {
35
- const parts = []
36
- let cur = node
37
- while (cur?.type == 'MemberExpression') {
38
- if (cur.computed || cur.property.type != 'Identifier') return null
39
- parts.unshift(cur.property.name)
40
- cur = cur.object
41
- }
42
- if (cur?.type != 'Identifier') return null
43
- parts.unshift(cur.name)
44
- return parts.join('.')
45
- }
46
-
47
- export function initBundleDeprecation(plugin, [ruleId, bundleNames], pluginName) {
48
- for (const bundleName of bundleNames) {
49
- /* eslint-disable security/detect-object-injection */
50
- if (plugin.configs[bundleName])
51
- plugin.configs[bundleName].rules[`${pluginName}/${ruleId}`] = 'error'
52
- /* eslint-enable security/detect-object-injection */
53
- if (process.env.DEBUG_MODE)
54
- console.warn(`[${pluginName}] '${ruleId}' will be removed from '${bundleName}' in a future major release`)
55
- }
56
- }
57
-
58
- export function isContentScriptFile(context, manifest) {
59
- const scripts = (manifest?.content_scripts || []).flatMap(cs => cs.js || [])
60
- if (!scripts.length) return false
61
- const root = findExtensionRoot(context)
62
- if (!root) return false
63
- const filename = path.resolve(getFilename(context))
64
- return scripts.some(s => filename == path.resolve(root, s))
65
- }
66
-
67
- export function isInsideNestedFunction(node) {
68
- let cur = node.parent
69
- while (cur) {
70
- if (['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression'].includes(cur.type))
71
- return true
72
- cur = cur.parent
73
- }
74
- return false
75
- }
76
-
77
- export function isServiceWorkerFile(context, manifest) {
78
- const sw = manifest?.background?.service_worker
79
- if (!sw) return false
80
- const root = findExtensionRoot(context)
81
- if (!root) return false
82
- return path.resolve(getFilename(context)) == path.resolve(root, sw)
83
- }
84
-
85
- export function readManifest(context) {
86
- const root = findExtensionRoot(context)
87
- if (!root) return null
88
- if (manifestCache.has(root)) return manifestCache.get(root)
89
- let parsed
90
- try { // eslint-disable-next-line security/detect-non-literal-fs-filename
91
- parsed = JSON.parse(fs.readFileSync(path.join(root, 'manifest.json'), 'utf8'))
92
- } catch { parsed = null }
93
- manifestCache.set(root, parsed)
94
- return parsed
95
- }