eslint-plugin-chrome-extensions
v2.8.0
Published
ESLint rules for Chrome extensions
Downloads
1,732
Maintainers
Readme
eslint-plugin-chrome-extensions
ESLint rules for Chrome extensions.
Installation
From your project root:
npm i -D eslint-plugin-chrome-extensionsESLint
Add bundled configs to your eslint.config.*js for each file type you need:
...
import chromeExtensions from 'eslint-plugin-chrome-extensions'
export default [
// other configs
{
files: ['**/extension/**/*.{cjs,js,jsx,mjs,ts,tsx}'],
plugins: { 'chrome-extensions': chromeExtensions },
rules: {
...chromeExtensions.configs.recommended.rules,
'chrome-extensions/no-deprecated-apis': 'off', // selectively disable rule
'chrome-extensions/valid-version-field': ['error', { versionType: 'date' }] // selectively tweak rule
}
},
{ files: ['**/*LICENSE*'], ...chromeExtensions.configs.license },
{ files: ['**/manifest.json'], ...chromeExtensions.configs.manifest },
{ files: ['**/extension/**/*.css'], ...chromeExtensions.configs.css },
{ files: ['**/extension/**/*.html'], ...chromeExtensions.configs.html },
{ files: ['**/extension/**/*.json'], ...chromeExtensions.configs.json },
{ files: ['**/extension/**/*.json5'], ...chromeExtensions.configs.json5 },
{ files: ['**/extension/**/*.jsonc'], ...chromeExtensions.configs.jsonc }
]Oxlint
Add a JS bundle to your oxlint.config.ts (non-JS parsing unsupported by Oxlint):
...
import chromeExtensions from 'eslint-plugin-chrome-extensions'
export default defineConfig({
// main config
overrides: [
// other overrides
{
files: ['**/extension/**/*.{cjs,js,jsx,mjs,ts,tsx}'],
jsPlugins: ['eslint-plugin-chrome-extensions'],
rules: {
...chromeExtensions.configs.recommended.rules,
'chrome-extensions/no-deprecated-apis': 'off', // selectively disable rule
'chrome-extensions/no-insecure-messaging': ['error', { disallowWildcardTarget: false }] // selectively tweak rule
}
}
]
})Note: Rule config is either a severity string ('off', 'warn', 'error') or an array [severity, options] when the rule accepts options.
Available JS bundles: recommended, strict (everything), problem (deterministic bugs only), security
Available filetype bundles (ESLint only): manifest, license, css, html, json, json5, jsonc
spaces(default2) – number of spaces per indent levelcheckBraces(defaulttrue) – enforce 1 space after{and before}in single-line rulescheckBrackets(defaulttrue) – enforce 0 spaces after[and before]in attribute selectorscheckColons(defaulttrue) – enforce 0 spaces before and 1 space after:in declarations, and 1 space between value tokenscheckCombinators(defaulttrue) – enforce 1 space on each side of>,+,~in selectorscheckCommas(defaulttrue) – enforce 0 spaces before and 1 space after,in selector listscheckParentheses(defaulttrue) – enforce 0 spaces after(and before)in functions and pseudo-classescheckSemicolons(defaulttrue) – enforce 1 space after;in single-line declaration blocks
Incorrect code
! .panel { background : blue }
! .panel+em { font-style: italic }
! :not( .bar ) { color: red }Correct code
- .panel { background : blue }
+ .panel { background: blue }
- .panel+em { font-style: italic }
+ .panel + em { font-style: italic }
- :not( .bar ) { color: red }
+ :not(.bar) { color: red }When not to use it
If you don't care about visual consistency in your CSS, or you already use Stylelint to format CSS.
Introduced in
v2.7.0 (October 17, 2026)
See also
manifest-spacing- MDN — CSS syntax
[Back to index ↑][epce-rules]
order(default[]) – manifest keys in custom order (e.g.['name', 'manifest_version']) to selectively override built-in order with
Incorrect code
/* eslint chrome-extensions/manifest-key-order: ['error', { order: ['name', 'manifest_version'] }] */
{
! "manifest_version": 3,
"name": "My Extension",
"version": "1.0.0"
}Correct code
/* eslint chrome-extensions/manifest-key-order: ['error', { order: ['name', 'manifest_version'] }] */
{
- "manifest_version": 3,
"name": "My Extension",
+ "manifest_version": 3,
"version": "1.0.0"
}When not to use it
If you don't care about the order of manifest keys.
Introduced in
v2.0.0 (October 4, 2026)
See also
[Back to index ↑][epce-rules]
spaces(default2) – number of spaces per indent levelcheckColons(defaulttrue) – enforce 0 spaces before and 1 space after:in key/value pairscheckCommas(defaulttrue) – enforce 0 spaces before and 1 space after,in inline objectscheckBraces(defaulttrue) – enforce 1 space after{and before}in inline objectscheckBrackets(defaulttrue) – enforce 0 spaces after[and before]in inline arrays
Incorrect code
/* eslint chrome-extensions/manifest-spacing: ['error', { spaces: 2 }] */
{
! "manifest_version": 3,
! "version" : "1.0.0",
! "permissions": [ "tabs","cookies" ],
! "icons": {"16": "icon16.png"},
"name": "My Extension"
}Correct code
/* eslint chrome-extensions/manifest-spacing: ['error', { spaces: 2 }] */
{
- "manifest_version": 3,
+ "manifest_version": 3,
- "version" : "1.0.0",
+ "version": "1.0.0",
- "permissions": [ "tabs","cookies" ],
+ "permissions": ["tabs", "cookies"],
- "icons": {"16": "icon16.png"},
+ "icons": { "16": "icon16.png" },
"name": "My Extension"
}When not to use it
If you don't care about visual consistency in your manifest.
Introduced in
v2.2.0 (October 12, 2026)
See also
[Back to index ↑][epce-rules]
Incorrect code
! document.head.append(document.createElement('style'))Correct code
+ const host = document.createElement('div'),
+ root = host.attachShadow({ mode: 'open' })
+ document.body.append(host)
- document.head.append(document.createElement('style'))
+ root.append(document.createElement('style'))When not to use it
If you intentionally interact w/ host page styles.
Introduced in
v1.0.0 (October 2, 2026)
See also
- Chrome for Developers — Content scripts
- MDN — Using shadow DOM
[Back to index ↑][epce-rules]
target(default'mv3') – Chrome extension manifest version whose deprecations to validate against ('mv3','mv2')allowedAPIs(default[]) – additional deprecated API namespaces to allow, as a single string or array
Incorrect code
/* eslint chrome-extensions/no-deprecated-apis: ['error', { target: 'mv3' }] */
! chrome.pageAction.show(tabId)
! chrome.extension.getBackgroundPage().doThing()
! chrome.tabs.executeScript(tabId, { code: 'doThing()' })Correct code
/* eslint chrome-extensions/no-deprecated-apis: ['error', { target: 'mv3' }] */
- chrome.pageAction.show(tabId)
+ chrome.action.enable(tabId)
- chrome.extension.getBackgroundPage().doThing()
+ await chrome.runtime.getContexts({ contextTypes: ['BACKGROUND'] })
- chrome.tabs.executeScript(tabId, { code: 'doThing()' })
+ chrome.scripting.executeScript({ target: { tabId }, func: doThing })When not to use it
If you don't want deprecation validation at all. For individual deprecated APIs you still need, prefer allowedAPIs over disabling the rule.
Introduced in
v1.0.0 (October 2, 2026)
See also
- Chrome for Developers — API reference
[Back to index ↑][epce-rules]
Incorrect code
{
"manifest_version": 3,
! "name": "Foo",
! "name": "Bar",
! "version": "1.0.0",
! "version": "2.0.0"
}Correct code
{
"manifest_version": 3,
- "name": "Foo",
- "name": "Bar",
"name": "My Extension",
"version": "1.0.0"
- "version": "2.0.0"
}When not to use it
If you rely on a Chromium fork resolving duplicate keys in a specific order.
Introduced in
v2.1.0 (October 5, 2026)
See also
[Back to index ↑][epce-rules]
flagCommonNames(defaulttrue) – flag well-known animation names (spin,fade-in,slide-in-right, etc.) that are most likely to collide w/ host pagesrequiredPrefix(defaultundefined) – flag names not starting w/ this prefix
Incorrect code
! @keyframes spin { from { transform: rotate(0) } to { transform: rotate(360deg) } }
! @keyframes fade-in { from { opacity: 0 } to { opacity: 1 } }Correct code
- @keyframes spin { from { transform: rotate(0) } to { transform: rotate(360deg) } }
+ @keyframes myext-spin { from { transform: rotate(0) } to { transform: rotate(360deg) } }
- @keyframes fade-in { from { opacity: 0 } to { opacity: 1 } }
+ @keyframes myext-fade-in { from { opacity: 0 } to { opacity: 1 } }When not to use it
If your CSS is scoped inside a Shadow DOM root.
Introduced in
v2.7.0 (October 17, 2026)
See also
no-host-page-selectors- MDN —
@keyframes - MDN — Using shadow DOM
[Back to index ↑][epce-rules]
allowedAttributes(default[]) – inline event handler attributes to permit (e.g.['onafterprint']), as a single string or array
Incorrect code
! <button onclick="alert('hi')">Click</button>
! <body onload="init()">
! <img src="logo.png" onerror="this.hidden = true">Correct code
- <button onclick="alert('hi')">Click</button>
+ <button id="myButton">Click</button>
- <body onload="init()">
+ <body>
- <img src="logo.png" onerror="this.hidden = true">
+ <img src="logo.png" id="logo">
// popup.js
+ document.getElementById('myButton').addEventListener('click', () => alert('hi'))
+ document.addEventListener('DOMContentLoaded', init)
+ document.getElementById('logo').addEventListener('error', event => event.target.hidden = true)When not to use it
If your HTML is a sandbox page (where the CSP is intentionally relaxed). For individual handlers you still need, prefer allowedAttributes over disabling the rule.
Introduced in
v2.8.0 (October 20, 2026)
See also
no-inline-scriptsno-remote-code- Chrome for Developers —
content_security_policy - MDN — CSP
script-src - MDN —
addEventListener()
[Back to index ↑][epce-rules]
allowedSrcPrefixes(default['chrome-extension://']) – additionalsrcprefixes to permit, as a single string or array
Incorrect code
! <script>alert('hi')</script>
! <script src="https://cdn.example.com/lib.js"></script>
! <script src="//cdn.example.com/lib.js"></script>
! <script src="data:text/javascript;base64,YWxlcnQoMSk="></script>Correct code
- <script>alert('hi')</script>
+ <script src="popup.js"></script>
- <script src="https://cdn.example.com/lib.js"></script>
+ <script src="./vendor/lib.js"></script>
- <script src="//cdn.example.com/lib.js"></script>
+ <script type="module" src="popup.js"></script>
- <script src="data:text/javascript;base64,YWxlcnQoMSk="></script>
+ <script src="chrome-extension://abc123/popup.js"></script>When not to use it
If your HTML is a sandbox page (where the CSP is intentionally relaxed).
Introduced in
v2.8.0 (October 20, 2026)
See also
no-remote-code- Chrome for Developers —
content_security_policy - MDN — CSP
script-src - MDN —
<script>element
[Back to index ↑][epce-rules]
allowedSelectors(default[]) – selectors to explicitly allow (e.g.['body.dark']), as a single string or arraydisallowUniversal(defaulttrue) – flag*disallowStructural(defaulttrue) – flaghtml,body,:rootdisallowBareElements(defaulttrue) – flag elem types (e.g.div,a) that lackclass/id/attribute
Incorrect code
! * { box-sizing: border-box }
! body { margin: 0 }
! html { font-family: sans-serif }
! button:hover { opacity: 0.8 }Correct code
- * { box-sizing: border-box }
+ :host { box-sizing: border-box }
- body { margin: 0 }
+ :host { margin: 0 }
- html { font-family: sans-serif }
+ :host { font-family: sans-serif }
- button:hover { opacity: 0.8 }
+ .myext-button:hover { opacity: 0.8 }When not to use it
If you intentionally style the host page. For individual selectors you still need, prefer allowedSelectors over disabling the rule.
Introduced in
v2.7.0 (October 17, 2026)
See also
no-content-style-leak- MDN —
:host
[Back to index ↑][epce-rules]
Incorrect code
! fetch('http://api.example.com/data')
! xhr.open('GET', 'http://cdn.example.com/lib.js')Correct code
- fetch('http://api.example.com/data')
+ fetch('https://api.example.com/data')
- xhr.open('GET', 'http://cdn.example.com/lib.js')
+ xhr.open('GET', 'https://cdn.example.com/lib.js')When not to use it
If you intentionally target HTTP endpoints.
Introduced in
v2.1.0 (October 5, 2026)
See also
- RFC 9110 — HTTP Semantics
- Chrome for Developers — Cross-origin network requests
- MDN —
XMLHttpRequest.open() - MDN —
fetch()
[Back to index ↑][epce-rules]
disallowWildcardTarget(defaulttrue) – flag postMessage() calls that use '*' as targetOrigin
Incorrect code
chrome.runtime.onMessageExternal.addListener((msg, sender, respond) => {
! handle(msg)
})
window.addEventListener('message', event => {
! handle(event.data)
})
! chrome.tabs.query({})
.then(tabs => tabs.forEach(tab =>
chrome.tabs.sendMessage(tab.id, { action: 'ping' })))
! window.postMessage(payload, '*')Correct code
chrome.runtime.onMessageExternal.addListener((msg, sender, respond) => {
+ if (sender.id != 'allowed-extension-id') return
handle(msg)
})
window.addEventListener('message', event => {
+ if (event.origin != 'https://allowed-origin.com') return
handle(event.data)
})
- chrome.tabs.query({})
+ chrome.tabs.query({ url: 'https://allowed-origin.com/*' })
.then(tabs => tabs.forEach(tab =>
chrome.tabs.sendMessage(tab.id, { action: 'ping' })))
- window.postMessage(payload, '*')
+ window.postMessage(payload, 'https://allowed-origin.com')When not to use it
If you trust every external caller and open tab, or you intentionally broadcast to any origin.
Introduced in
v1.0.0 (October 2, 2026)
See also
- Chrome for Developers —
runtime.onMessageExternal - Chrome for Developers —
tabs.query() - MDN —
window.postMessage()security concerns
[Back to index ↑][epce-rules]
allowedLocales(default[]) – additional locale codes to allow, as a single string or array
Incorrect code
// manifest.json
{
"manifest_version": 3,
! "default_locale": "pt-BR"
}
! // _locales/pt-BR/messages.jsonCorrect code
// manifest.json
{
"manifest_version": 3,
- "default_locale": "pt-BR"
+ "default_locale": "pt_BR"
}
- // _locales/pt-BR/messages.json
+ // _locales/pt_BR/messages.jsonWhen not to use it
If your _locales/ is externally populated at build time. If you target a Chromium fork w/ additional supported locales, prefer allowedLocales over disabling the rule.
Introduced in
v2.6.0 (October 16, 2026)
See also
- Chrome for Developers —
i18n no-missing-default-locale
[Back to index ↑][epce-rules]
target(default'mv3') – Chrome manifest version whose permissions to validate against ('mv3','mv2')allowedPermissions(default[]) – additional permission names to allow beyond the built-in list, as a single string or array
Incorrect code
/* eslint chrome-extensions/no-invalid-permissions: ['error', { target: 'mv3' }] */
{
! "permissions": ["browserAction", "tabs"],
! "optional_permissions": ["history", "pageAction"]
}Correct code
/* eslint chrome-extensions/no-invalid-permissions: ['error', { target: 'mv3' }] */
{
- "permissions": ["browserAction", "tabs"],
+ "permissions": ["tabs"],
- "optional_permissions": ["history", "pageAction"]
+ "optional_permissions": ["history"]
}When not to use it
If you don't want permission validation at all. For custom permissions from a Chromium fork, prefer allowedPermissions over disabling the rule.
Introduced in
v1.0.0 (October 2, 2026)
See also
[Back to index ↑][epce-rules]
ignoreKeys(default[]) – keys to skip when checking localescheckAllLocales(defaultfalse) – also flag keys missing from any locale
Incorrect code
// manifest.json
{
"manifest_version": 3,
"default_locale": "en",
"name": "__MSG_extName__"
}
// _locales/en/messages.json
{
"extName": { "message": "My Extension" }
}
// _locales/de/messages.json
{
"extName": { "message": "Meine Erweiterung" },
! "welcomeMsg": { "message": "Willkommen" }
}Correct code
// manifest.json
{
"manifest_version": 3,
"default_locale": "en",
"name": "__MSG_extName__"
}
// _locales/en/messages.json
{
"extName": { "message": "My Extension" },
+ "welcomeMsg": { "message": "Welcome" }
}
// _locales/de/messages.json
{
"extName": { "message": "Meine Erweiterung" },
"welcomeMsg": { "message": "Willkommen" }
}When not to use it
If your _locales/ is externally populated at build time.
Introduced in
v2.4.0 (October 13, 2026)
See also
- Chrome for Developers —
default_locale - <a target="_blank" href="https://developer.chrome.com/docs/extensions/referen
