eslint-plugin-chrome-extensions 2.4.1 → 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 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>
@@ -54,7 +54,7 @@ export default [
54
54
  'chrome-extensions/manifest-spacing': ['warn', { spaces: 4 }] // selectively tweak rule
55
55
  }
56
56
  },
57
- { files: ['**/*LICENSE*'], ...chromeExtensions.configs.license },
57
+ { files: ['**/*LICENSE*'], ...chromeExtensions.configs.license },
58
58
  { files: ['**/extension/**/*.css'], ...chromeExtensions.configs.css },
59
59
  { files: ['**/extension/**/*.html'], ...chromeExtensions.configs.html },
60
60
  { files: ['**/extension/**/*.json'], ...chromeExtensions.configs.json },
@@ -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
 
@@ -1228,14 +1227,14 @@ 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 structure
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
1233
  /extension/manifest.json
1235
1234
  !
1236
1235
  ```
1237
1236
 
1238
- #### <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 structure
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
1239
1238
 
1240
1239
  ```diff
1241
1240
  /extension/manifest.json
@@ -1253,7 +1252,7 @@ If you intentionally omit a license, or it's injected at build time.
1253
1252
  #### See also
1254
1253
 
1255
1254
  - [`valid-license-file`](#valid-license-file)
1256
- <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>
1257
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>
1258
1257
 
1259
1258
  <div align="center">
@@ -1483,7 +1482,7 @@ If you don't use conventional filenames or paths, or they're generated at build
1483
1482
 
1484
1483
  <h3 id="valid-license-file">
1485
1484
  <code>valid-license-file</code>
1486
- <a title="Only works in 'license' config block" href="#usage">
1485
+ <a title="Only works in license config block" href="#usage">
1487
1486
  <img height=18 alt="[License only]" src="https://img.shields.io/badge/License_only-559bff?style=flat-square" align="middle"></a>
1488
1487
  <a title="Accepts array w/ options" href="#usage">
1489
1488
  <img height=18 alt="[Configurable]" src="https://img.shields.io/badge/Configurable-black?style=flat-square" align="middle"></a>
@@ -1506,13 +1505,9 @@ If you don't use conventional filenames or paths, or they're generated at build
1506
1505
  ```diff
1507
1506
  /* eslint chrome-extensions/valid-license-file: ['error', { requireSPDX: true, allowedLicenses: ['MIT'] }] */
1508
1507
 
1509
- // LICENSE.md
1510
-
1511
- MIT License
1512
-
1508
+ ! MIT License
1509
+ -----------
1513
1510
  Copyright © 2026 You
1514
-
1515
- !
1516
1511
  ```
1517
1512
 
1518
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
@@ -1520,15 +1515,17 @@ If you don't use conventional filenames or paths, or they're generated at build
1520
1515
  ```diff
1521
1516
  /* eslint chrome-extensions/valid-license-file: ['error', { requireSPDX: true, allowedLicenses: ['MIT'] }] */
1522
1517
 
1523
- // LICENSE.md
1524
-
1525
1518
  MIT License
1526
-
1519
+ -----------
1527
1520
  Copyright © 2026 You
1528
-
1521
+ +
1529
1522
  + SPDX-License-Identifier: MIT
1530
1523
  ```
1531
1524
 
1525
+ #### Benefits
1526
+
1527
+ Makes the license machine-readable by compliance scanners + SBOM generators + CI gates.
1528
+
1532
1529
  #### When not to use it
1533
1530
 
1534
1531
  If your license is injected at build time, or you use a license w/ no SPDX identifier.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-chrome-extensions",
3
- "version": "2.4.1",
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
  {
@@ -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
 
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,7 +49,7 @@ 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
@@ -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)
@@ -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,
@@ -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) {
@@ -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'
@@ -39,9 +41,9 @@ export default {
39
41
  || node.parent?.type == 'Property' && node.parent.key === node && !node.parent.computed
40
42
  || !domGlobals.has(node.name)
41
43
  ) return
42
- for (let scope = context.sourceCode.getScope(node) ;
43
- scope && scope.type != 'global' ;
44
- scope = scope.upper
44
+ for (let scope = context.sourceCode.getScope(node)
45
+ ;scope && scope.type != 'global'
46
+ ;scope = scope.upper
45
47
  ) if (scope.set.has(node.name)) return // shadowed by a local binding
46
48
  context.report({ node, messageId: 'domInSW', data: { global: node.name }})
47
49
  }
@@ -1,5 +1,5 @@
1
1
  import { matchPaths, urlPaths } from '../data/manifest.js'
2
- import { getFilename } from '../utils.js'
2
+ import { file } from '../utils/extension.js'
3
3
 
4
4
  const re = {
5
5
  absoluteURL: /^[a-z][a-z0-9+.-]*:\/\/[^\s/]+/i, manifestFilename: /manifest\.json$/,
@@ -67,7 +67,7 @@ export default {
67
67
  },
68
68
 
69
69
  create(context) {
70
- if (!re.manifestFilename.test(getFilename(context))) return {}
70
+ if (!re.manifestFilename.test(file.getName(context))) return {}
71
71
  const opts = context.options[0] || {},
72
72
  checkURLfields = opts.checkURLfields ?? true,
73
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
- }