From c127fb6beaccc0a51463d7cca94018234a31482d Mon Sep 17 00:00:00 2001 From: Oliver Blanthorn Date: Sat, 11 Jul 2026 15:02:07 +0200 Subject: [PATCH] Turn unsupported-apis into a rule factory This way we can mark specific platforms as compatible --- .eslintrc.js | 11 ++- custom-eslint-rules/lib/unsupported-apis.js | 90 +++++++++++++++++++ custom-eslint-rules/readme.md | 24 +++++ .../unsupported-apis-chrome.js | 1 + .../unsupported-apis-firefox.js | 1 + custom-eslint-rules/unsupported-apis.js | 63 ------------- 6 files changed, 126 insertions(+), 64 deletions(-) create mode 100644 custom-eslint-rules/lib/unsupported-apis.js create mode 100644 custom-eslint-rules/readme.md create mode 100644 custom-eslint-rules/unsupported-apis-chrome.js create mode 100644 custom-eslint-rules/unsupported-apis-firefox.js delete mode 100644 custom-eslint-rules/unsupported-apis.js diff --git a/.eslintrc.js b/.eslintrc.js index fc1e490b..352e7829 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,5 @@ +const browserTargets = require("./browser-targets.json") + /* 👋 Hi! This file was autogenerated by tslint-to-eslint-config. https://github.com/typescript-eslint/tslint-to-eslint-config @@ -34,7 +36,14 @@ module.exports = { "sonarjs" ], "rules": { - "unsupported-apis": "warn", + "unsupported-apis-chrome": [ + "warn", + { "minimumVersion": browserTargets.chrome.minimumVersion } + ], + "unsupported-apis-firefox": [ + "warn", + { "minimumVersion": browserTargets.firefox.minimumVersion } + ], "sonarjs/cognitive-complexity": "off", //"error", "sonarjs/no-duplicate-string": "off", "sonarjs/no-unused-collection": "off", //"error", // There seems to be a bug with this rule - exported collections are assumed unused diff --git a/custom-eslint-rules/lib/unsupported-apis.js b/custom-eslint-rules/lib/unsupported-apis.js new file mode 100644 index 00000000..ddce0880 --- /dev/null +++ b/custom-eslint-rules/lib/unsupported-apis.js @@ -0,0 +1,90 @@ +const bcd = require("@mdn/browser-compat-data") +const api = bcd.webextensions.api + +function propertyNameOrValue(n) { + return n.property.type == "Literal" ? n.property.value : n.property.name +} + +function isVersionNewer(version, minimumVersion) { + const versionParts = String(version).split(".").map(Number) + const minimumParts = String(minimumVersion).split(".").map(Number) + if (versionParts.some(Number.isNaN) || minimumParts.some(Number.isNaN)) + return false + const length = Math.max(versionParts.length, minimumParts.length) + for (let i = 0; i < length; i++) { + const difference = (versionParts[i] || 0) - (minimumParts[i] || 0) + if (difference !== 0) return difference > 0 + } + return false +} + +function detectBrowserUsage(context, node, browser, minimumVersion) { + let localApi = api + const fullName = [] + while ( + node.type == "MemberExpression" && + propertyNameOrValue(node) in localApi + ) { + const n = node + node = node.parent + const name = propertyNameOrValue(n) + fullName.push(name) + localApi = localApi[name] + if (!localApi.__compat) { + continue + } + const support = localApi.__compat.support + if (support[browser].version_added === false) { + context.report({ + node: n, + messageId: "unsupportedApis", + data: { + name: browser, + api: fullName.join("."), + }, + }) + } else { + const version = support[browser].version_added + if ( + minimumVersion !== undefined && + isVersionNewer(version, minimumVersion) + ) { + context.report({ + node: n, + messageId: "apiTooRecent", + data: { + api: fullName.join("."), + name: browser, + version: minimumVersion, + }, + }) + } + } + } +} + +module.exports = browser => ({ + meta: { + schema: [ + { + type: "object", + properties: { minimumVersion: { type: "string" } }, + additionalProperties: false, + }, + ], + messages: { + unsupportedApis: "{{ api }} unsupported on '{{ name }}'", + apiTooRecent: + "{{ api }} is not supported on {{ name }} {{ version }}", + }, + }, + create(context) { + const { minimumVersion } = context.options[0] || {} + const detect = node => + detectBrowserUsage(context, node, browser, minimumVersion) + return { + 'MemberExpression[object.name="browser"]': detect, + 'MemberExpression[object.name="browserBg"]': detect, + } + }, +}) diff --git a/custom-eslint-rules/readme.md b/custom-eslint-rules/readme.md new file mode 100644 index 00000000..51ec89f9 --- /dev/null +++ b/custom-eslint-rules/readme.md @@ -0,0 +1,24 @@ +# adding a new target + +make a file here like + +```js +// unsupported-apis-mosaic.js +module.exports = require("./lib/unsupported-apis")("mosaic") +``` + + +add a line to `../browser-targets.json` + +```json +"mosaic": { "minimumVersion": "1.0.0" } +``` + +add a few lines to `../.eslintrc.js` + +```js +"unsupported-apis-mosaic": [ + "warn", + { "minimumVersion": browserTargets.mosaic.minimumVersion } +], +``` diff --git a/custom-eslint-rules/unsupported-apis-chrome.js b/custom-eslint-rules/unsupported-apis-chrome.js new file mode 100644 index 00000000..265254fc --- /dev/null +++ b/custom-eslint-rules/unsupported-apis-chrome.js @@ -0,0 +1 @@ +module.exports = require("./lib/unsupported-apis")("chrome") diff --git a/custom-eslint-rules/unsupported-apis-firefox.js b/custom-eslint-rules/unsupported-apis-firefox.js new file mode 100644 index 00000000..4850e21f --- /dev/null +++ b/custom-eslint-rules/unsupported-apis-firefox.js @@ -0,0 +1 @@ +module.exports = require("./lib/unsupported-apis")("firefox") diff --git a/custom-eslint-rules/unsupported-apis.js b/custom-eslint-rules/unsupported-apis.js deleted file mode 100644 index a795fad4..00000000 --- a/custom-eslint-rules/unsupported-apis.js +++ /dev/null @@ -1,63 +0,0 @@ -const bcd = require('@mdn/browser-compat-data'); -const api = bcd.webextensions.api; -const supported_browsers = ["firefox", "chrome"]; -const minimalSupportedFirefoxVersion = 114; - -function propertyNameOrValue(n) { - return (n.property.type == "Literal" ? n.property.value : n.property.name) -} - -function detectBrowserUsage(context, node) { - let localApi = api; - let fullName = []; - while (node.type == "MemberExpression" && propertyNameOrValue(node) in localApi) { - const n = node; - node = node.parent; - let name = propertyNameOrValue(n); - fullName.push(name); - localApi = localApi[name]; - if (!localApi.__compat) { - continue; - } - let support = localApi.__compat.support; - for (let browser of supported_browsers) { - if (support[browser].version_added === false) { - context.report({ - node: n, - messageId: "unsupportedApis", - data: { - name: browser, - api: fullName.join("."), - } - }); - } else { - const version = Number(support[browser].version_added); - if (!isNaN(version) && version > minimalSupportedFirefoxVersion) { - context.report({ - node: n, - messageId: "apiTooRecent", - data: { - api: fullName.join("."), - version: minimalSupportedFirefoxVersion - } - }); - } - } - } - } -} - -module.exports = { - meta: { - messages: { - unsupportedApis: "{{ api }} unsupported on '{{ name }}'", - apiTooRecent: "{{ api }} is not supported on firefox {{ version }}", - } - }, - create(context) { - return { - 'MemberExpression[object.name="browser"]': (n) => detectBrowserUsage(context, n), - 'MemberExpression[object.name="browserBg"]': (n) => detectBrowserUsage(context, n), - }; - } -};