browser-extension-compat-data
v0.2.2
Published
Validate WebExtensions manifest (and optionally permissions/APIs) against MDN browser-compat-data
Maintainers
Readme
Validate WebExtensions manifest (and optionally permissions/APIs) against MDN browser-compat-data
browser-extension-compat-data

MDN WebExtensions compatibility data (manifest fields, APIs, and permissions) in one lean package. Query whether a feature is supported in a browser/version (lookup), lint a whole extension across multiple targets (validate), or wire it into ESLint.
- Data: MDN browser-compat-data (webextensions)
- No runtime fetch. Ships a single compact JSON asset (~24 KiB gzipped), not the full ~15 MB MDN dataset.
Install
npm i browser-extension-compat-dataLookup: "is this supported?"
import {
isSupported,
getSupport,
getMinVersion,
getMdnUrl,
} from 'browser-extension-compat-data'
isSupported('api', 'runtime.sendMessage', 'safari') // => true
isSupported('manifest', 'side_panel', 'firefox') // => false
isSupported('api', 'scripting.executeScript', 'chrome', '88') // version-gated
getMinVersion('manifest', 'action', 'chrome') // => "88"
getMdnUrl('permissions', 'tabs') // MDN url (explicit or derived)
getSupport('api', 'runtime.sendMessage')
// { chrome: { supported: true, versionAdded: "26" }, safari: { supported: true, ... }, ... }First arg is the domain ('manifest' | 'api' | 'permissions'), second the dotted BCD key. An unknown browser throws (a typo can never read as a silent pass); the canonical set is in BROWSERS.
Validate a whole extension, across targets
import { analyzeExtension } from 'browser-extension-compat-data'
const report = await analyzeExtension('./my-extension', [
{ browser: 'chrome', version: '116' },
{ browser: 'firefox', version: '115' },
{ browser: 'safari', version: '16.4' },
])
report.ok // false if any target has findings
report.scannedFiles // sources resolved from the manifest (background, content scripts)
for (const { target, findings } of report.targets) {
// findings: { kind, key, reason, browser, message?, file?, loc?, mdnUrl?, support? }
}It reads manifest.json, checks every field/permission MDN knows about (including host_permissions and optional_permissions), applies Manifest V2/V3 structural rules, and scans referenced source for chrome.*/browser.* usage. That scan includes scripts inside HTML entry-points (popup, options, devtools, sidebar, new-tab overrides), both external <script src> and inline blocks.
Usage resolution follows destructuring, webextension-polyfill imports, and aliasing, with file:line:column on every finding.
reason is one of not-supported | removed | partial | flag | manifest-version | no-compat-data.
Lower-level validators
import {
getUnsupportedManifestFields,
getUnsupportedAPIsFromFile,
} from 'browser-extension-compat-data'
await getUnsupportedManifestFields('./manifest.json', {
browser: 'safari',
version: '17',
})
await getUnsupportedAPIsFromFile('./background.js', {
browser: 'firefox',
scanMode: 'accurate',
})scanMode: 'accurate' (default for analyzeExtension) parses the AST and is alias-aware; 'fast' is a quick regex heuristic.
CLI
npx browser-extension-compat-data ./my-extension --targets chrome116,firefox115,safari16.4manifest: ./my-extension/manifest.json
scanned 2 source file(s): bg.js, content.js
✓ chrome 116: no issues
✗ firefox 115: 3 issue(s)
[not supported] manifest: side_panel
[not supported] api: offscreen.createDocument (bg.js:3:0)
[no compat data] permission: offscreenExits non-zero when anything is reported. --scan all scans every script in the directory; --mode fast; --no-permissions; --json.
ESLint plugin
// eslint.config.js
import { eslintPlugin } from 'browser-extension-compat-data'
export default [
{
plugins: { 'webext-compat': eslintPlugin },
rules: {
'webext-compat/compat': [
'warn',
{ targets: ['firefox115', 'safari16.4'] },
],
},
},
]Flags chrome.*/browser.* calls unsupported by your targets, inline in the editor. It is alias-aware (destructuring, webextension-polyfill, aliasing), the same resolution the analyzer uses.
Typed keys
The generated key unions ship with the package, so consumers can opt into compile-time safety:
import type {
ApiKey,
ManifestKey,
PermissionKey,
} from 'browser-extension-compat-data'
const used = ['runtime.sendMessage', 'tabs.query'] satisfies ApiKey[] // typo => compile errorThe runtime functions accept any string (so dynamic keys and custom datasets still work); the unions are opt-in.
API surface
type Domain = 'manifest' | 'api' | 'permissions'
type Browser = 'chrome' | 'edge' | 'firefox' | 'firefox_android' | 'opera' | 'safari' | 'safari_ios'
// Lookup
isSupported(domain, key, browser, version?) // throws on unknown browser
getSupport(domain, key) / getBrowserSupport(domain, key, browser)
getMinVersion(domain, key, browser) / getMdnUrl(domain, key) / hasFeature(domain, key) / listKeys(domain)
// Validate
analyzeExtension(input, targets, options?) -> ExtensionReport
getUnsupportedManifestFields(path, options) / getUnsupportedAPIsFromFile(path, options)
evaluateManifest(manifestObject, browser, version?, checkPermissions?)
// Browsers / targets / helpers
BROWSERS, isKnownBrowser, assertBrowser, parseTarget, parseTargets
compareVersions, buildMdnUrl, runCli, eslintPlugin
// Data source (tests / custom datasets)
getIndex, setIndex, setIndexFromFile, resetIndexBrowser & bundler use
The default entry (.) reads the index from disk and pulls in fs/acorn, so
it is Node-only. For the web, CDNs, or an in-browser compiler, import the
fs-free subset from browser-extension-compat-data/browser. It excludes the
file scanners (getUnsupportedManifestFields, getUnsupportedAPIsFromFile,
analyzeExtension), the CLI and the ESLint plugin, and it starts from an empty
index — bring your own data with setIndex:
import {evaluateManifest, setIndex} from 'browser-extension-compat-data/browser'
// Inline the ~700 KB dataset (bundlers resolve the JSON), or fetch it at runtime.
import index from 'browser-extension-compat-data/index.json'
setIndex(index)
evaluateManifest(manifestObject, 'firefox') // -> UnsupportedItem[]Data & provenance
The package ships one precomputed file, src/generated/index.json, copied to dist/index.json at build time and read lazily (not inlined into the bundles). It flattens MDN's webextensions/{manifest,api,permissions} into key -> { u?: mdnUrl, s: { browser: { a, r?, p?, f? } } } (a version_added, r version_removed, p partial, f behind-a-flag). getIndex().v reports the upstream BCD version.
A daily GitHub Action re-syncs MDN BCD, rebuilds the index + src/generated/keys.ts, and commits only when the data changed.
# rebuild locally against a checkout of mdn/browser-compat-data
node scripts/build-index.mjs path/to/browser-compat-data/webextensions src/generated/index.jsonNode-only at runtime (reads the JSON asset via
fs).
Related projects
- browser-extension-manifest-fields
- browser-extension-capabilities
- extension-from-store
- chrome-extension-manifest-json-schema
- parse5-asset-patcher
License
MIT (c) Cezar Augusto.
