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.
- package/README.md +33 -38
- package/package.json +5 -3
- package/src/data/license.js +2 -1
- package/src/data/manifest.js +17 -0
- package/src/index.js +2 -1
- package/src/rules/manifest-key-order.js +2 -2
- package/src/rules/manifest-spacing.js +2 -2
- package/src/rules/no-content-style-leak.js +2 -2
- package/src/rules/no-deprecated-apis.js +1 -1
- package/src/rules/no-duplicate-manifest-keys.js +2 -2
- package/src/rules/no-insecure-messaging.js +1 -1
- package/src/rules/no-invalid-permissions.js +5 -5
- package/src/rules/no-missing-default-locale.js +1 -1
- package/src/rules/no-missing-i18n-keys.js +2 -1
- package/src/rules/no-missing-permissions.js +2 -1
- package/src/rules/no-missing-web-accessible-resources.js +3 -2
- package/src/rules/no-remote-code.js +1 -1
- package/src/rules/no-unrestricted-host-permissions.js +2 -2
- package/src/rules/no-unused-i18n-keys.js +1 -1
- package/src/rules/no-unused-permissions.js +5 -5
- package/src/rules/require-license-file.js +8 -6
- package/src/rules/require-mv3.js +2 -2
- package/src/rules/require-short-name-when-useful.js +2 -2
- package/src/rules/valid-filenames.js +1 -0
- package/src/rules/valid-manifest-icons.js +2 -2
- package/src/rules/valid-mv3-schema.js +5 -5
- package/src/rules/valid-service-worker.js +11 -8
- package/src/rules/valid-url-fields.js +4 -20
- package/src/rules/valid-version-field.js +2 -2
- package/src/utils/bundle.js +10 -0
- package/src/utils/extension.js +45 -0
- package/src/utils/node.js +22 -0
- package/src/utils/pkg.js +19 -0
- package/src/utils/rules.js +5 -0
- 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="
|
|
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: ['
|
|
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*'],
|
|
58
|
-
{ files: ['
|
|
59
|
-
{ files: ['
|
|
60
|
-
{ files: ['
|
|
61
|
-
{ files: ['
|
|
62
|
-
{ files: ['
|
|
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"> <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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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": "
|
|
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.
|
|
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",
|
package/src/data/license.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/src/data/manifest.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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(
|
|
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 {
|
|
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(
|
|
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 {
|
|
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 || !
|
|
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,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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(
|
|
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 {
|
|
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(
|
|
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
|
|
60
|
-
for (const
|
|
61
|
-
const valueNode =
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
39
|
+
if (!file.isContentScript(context, manifest)) return {}
|
|
39
40
|
const regexes = collectWARpatterns(manifest).map(patternToRegex)
|
|
40
41
|
return {
|
|
41
42
|
CallExpression(node) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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(
|
|
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 {
|
|
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(
|
|
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
|
|
68
|
-
for (const
|
|
69
|
-
const valueNode =
|
|
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
|
|
17
|
+
+ '(at the package root or in a subdir).'
|
|
17
18
|
}
|
|
18
19
|
},
|
|
19
20
|
create(context) {
|
|
20
|
-
const { filename } = context
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
}
|
package/src/rules/require-mv3.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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(
|
|
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 {
|
|
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(
|
|
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,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
|
|
4
|
-
import { findExtensionRoot,
|
|
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(
|
|
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 {
|
|
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(
|
|
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
|
|
68
|
-
const value = typeof
|
|
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:
|
|
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
|
-
|
|
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 || !
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
scope
|
|
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 {
|
|
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
|
|
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(
|
|
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 {
|
|
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(
|
|
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
|
+
}
|
package/src/utils/pkg.js
ADDED
|
@@ -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
|
+
}
|
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
|
-
}
|