├── tests ├── fixture │ ├── file.ts │ ├── react.tsx │ └── tsconfig.json ├── test-helper.ts └── tscompat.ts ├── .gitignore ├── index.js ├── docs └── rules │ └── tscompat.md ├── jsr.json ├── tsconfig.json ├── .github ├── scripts │ └── update-jsr.mjs ├── workflows │ ├── dependabot-auto-merge.yml │ ├── ci.yml │ └── deploy.yml └── dependabot.yml ├── types.ts ├── eslint.config.js ├── README.md ├── package.json ├── LICENSE └── lib └── rules └── tscompat.ts /tests/fixture/file.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixture/react.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { tscompat } from "./lib/rules/tscompat.js"; 2 | 3 | export default { 4 | rules: { 5 | tscompat, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /docs/rules/tscompat.md: -------------------------------------------------------------------------------- 1 | # Lint for browser compatability (`tscompat/tscompat`) 2 | 3 | This rule enables linting your code for browser compatibility. 4 | -------------------------------------------------------------------------------- /tests/fixture/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "lib": ["DOM", "ESNEXT"] 5 | }, 6 | "include": ["file.ts", "react.tsx"] 7 | } 8 | -------------------------------------------------------------------------------- /jsr.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@koddsson/eslint-config-tscompat", 3 | "version": "0.0.0", 4 | "exports": "./index.js", 5 | "publish": { 6 | "exclude": ["tests/"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "nodenext", 5 | "lib": ["DOM", "ESNEXT"], 6 | "checkJs": true 7 | }, 8 | "include": ["tests", "lib", "*.js", "*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /.github/scripts/update-jsr.mjs: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from "node:fs/promises"; 2 | 3 | const TAG_NAME = process.argv[2]; 4 | console.log(process.argv); 5 | 6 | const existingJSRConfig = JSON.parse(await readFile("./jsr.json", "utf8")); 7 | existingJSRConfig.version = TAG_NAME; 8 | writeFile("./jsr.json", JSON.stringify(existingJSRConfig, null, 2), "utf8"); 9 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | export type BrowsersListBrowserName = 2 | | "ie" 3 | | "edge" 4 | | "firefox" 5 | | "chrome" 6 | | "safari" 7 | | "opera" 8 | | "ios_saf" 9 | | "op_mini" 10 | | "android" 11 | | "bb" 12 | | "op_mob" 13 | | "and_chr" 14 | | "and_ff" 15 | | "ie_mob" 16 | | "and_uc" 17 | | "samsung" 18 | | "and_qq" 19 | | "baidu" 20 | | "kaios" 21 | | "fx" 22 | | "ff" 23 | | "ios" 24 | | "explorer" 25 | | "blackberry" 26 | | "explorermobile" 27 | | "operamini" 28 | | "operamobile" 29 | | "chromeandroid" 30 | | "firefoxandroid" 31 | | "ucandroid" 32 | | "qqandroid"; 33 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: github.actor == 'dependabot[bot]' 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v2 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Enable auto-merge for Dependabot PRs 19 | run: gh pr merge --auto --merge "$PR_URL" 20 | env: 21 | PR_URL: ${{github.event.pull_request.html_url}} 22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | # Check for updates to GitHub Actions every week 17 | interval: "weekly" 18 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslint from "@eslint/js"; 2 | import tseslint from "typescript-eslint"; 3 | import eslintPlugin from "eslint-plugin-eslint-plugin"; 4 | 5 | export default tseslint.config( 6 | eslint.configs.recommended, 7 | ...tseslint.configs.recommendedTypeChecked, 8 | { 9 | languageOptions: { 10 | parserOptions: { 11 | project: true, 12 | tsconfigRootDir: import.meta.dirname, 13 | }, 14 | }, 15 | }, 16 | { 17 | ...eslintPlugin.configs["flat/all-type-checked"], 18 | }, 19 | { 20 | rules: { 21 | "@typescript-eslint/no-unsafe-assignment": "off", 22 | "@typescript-eslint/no-unsafe-member-access": "off", 23 | }, 24 | }, 25 | { 26 | ignores: ["eslint.config.js", ".github/"], 27 | }, 28 | ); 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Run tests 5 | 6 | on: 7 | push: 8 | branches: ["main"] 9 | pull_request: 10 | branches: ["main"] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v6 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v6 20 | with: 21 | node-version: 24.x 22 | cache: "npm" 23 | - run: npm ci 24 | - run: npm run build --if-present 25 | - run: npm run lint --if-present 26 | - run: npm test --if-present 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `@koddsson/eslint-plugin-tscompat` 2 | 3 | > A type-aware browser compatability ESLint rule 4 | 5 | ## Install 6 | 7 | Assuming you already have ESLint installed, run: 8 | 9 | ```sh 10 | npm install @koddsson/eslint-plugin-tscompat --save-dev 11 | ``` 12 | 13 | ## Usage 14 | 15 | Then extend the recommended eslint config: 16 | 17 | ```js 18 | import tscompat from "@koddsson/eslint-plugin-tscompat"; 19 | import parser from "@typescript-eslint/parser"; 20 | 21 | export default [ 22 | { 23 | plugins: { 24 | tscompat, 25 | }, 26 | rules: { 27 | "tscompat/tscompat": [ 28 | "error", 29 | { browserslist: [">0.3%", "last 2 versions", "not dead"] }, 30 | ], 31 | }, 32 | languageOptions: { 33 | parser, 34 | parserOptions: { 35 | projectService: true, 36 | tsconfigRootDir: import.meta.dirname, 37 | }, 38 | }, 39 | }, 40 | ]; 41 | ``` 42 | -------------------------------------------------------------------------------- /tests/test-helper.ts: -------------------------------------------------------------------------------- 1 | import { after, describe, it } from "node:test"; 2 | import { fileURLToPath } from "node:url"; 3 | import * as path from "node:path"; 4 | 5 | import { RuleTester } from "@typescript-eslint/rule-tester"; 6 | 7 | RuleTester.afterAll = after; 8 | // eslint-disable-next-line @typescript-eslint/no-misused-promises 9 | RuleTester.describe = describe; 10 | // eslint-disable-next-line @typescript-eslint/no-misused-promises 11 | RuleTester.it = it; 12 | // eslint-disable-next-line @typescript-eslint/no-misused-promises 13 | RuleTester.itOnly = it.only; 14 | 15 | const __filename = fileURLToPath(import.meta.url); 16 | const __dirname = path.dirname(__filename); 17 | 18 | const tsconfigRootDir = path.join(__dirname, "./fixture/"); 19 | 20 | export const ruleTester = new RuleTester({ 21 | languageOptions: { 22 | parserOptions: { 23 | tsconfigRootDir, 24 | project: "./tsconfig.json", 25 | }, 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@koddsson/eslint-plugin-tscompat", 3 | "version": "0.0.0", 4 | "description": "", 5 | "type": "module", 6 | "scripts": { 7 | "lint": "eslint lib/ tests/ index.js", 8 | "build": "esbuild index.js lib/**/* --outdir=dist --platform=node --format=esm", 9 | "test": "tsx --test tests/tscompat.ts " 10 | }, 11 | "exports": { 12 | ".": "./dist/index.js" 13 | }, 14 | "files": [ 15 | "docs/", 16 | "dist/" 17 | ], 18 | "keywords": [], 19 | "author": "", 20 | "license": "ISC", 21 | "dependencies": { 22 | "@mdn/browser-compat-data": "^7.0.0", 23 | "@typescript-eslint/type-utils": "^8.0.1", 24 | "@typescript-eslint/utils": "^8.0.0", 25 | "browserslist": "^4.23.0" 26 | }, 27 | "devDependencies": { 28 | "@eslint/js": "^9.0.0", 29 | "@types/eslint": "^9.6.0", 30 | "@types/node": "^24.0.1", 31 | "@typescript-eslint/parser": "^8.33.0", 32 | "@typescript-eslint/rule-tester": "^8.33.0", 33 | "esbuild": "^0.27.0", 34 | "eslint-plugin-eslint-plugin": "^7.0.0", 35 | "tsx": "^4.7.2", 36 | "typescript-eslint": "^8.0.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Kristján Oddsson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release (npm) 2 | on: 3 | release: 4 | types: [published] 5 | 6 | # Allows you to run this workflow manually from the Actions tab 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v6 15 | - name: Setup Node 16 | uses: actions/setup-node@v6 17 | with: 18 | node-version: 24.x 19 | - name: Install Dependencies 20 | run: npm ci 21 | - name: Lint 22 | run: npm run lint 23 | - name: Test 24 | run: npm test 25 | publish-npm: 26 | needs: build 27 | runs-on: ubuntu-latest 28 | permissions: 29 | id-token: write 30 | steps: 31 | - uses: actions/checkout@v6 32 | - uses: actions/setup-node@v6 33 | with: 34 | node-version: 24.x 35 | registry-url: 'https://registry.npmjs.org' 36 | cache: 'npm' 37 | - run: npm ci 38 | - run: npm version ${TAG_NAME} --git-tag-version=false 39 | env: 40 | TAG_NAME: ${{ github.ref_name }} 41 | - run: npm publish --provenance --access public --tag next 42 | if: "github.event.release.prerelease" 43 | env: 44 | NODE_AUTH_TOKEN: ${{ secrets.npm_token }} 45 | - run: npm publish --provenance --access public 46 | if: "!github.event.release.prerelease" 47 | env: 48 | NODE_AUTH_TOKEN: ${{ secrets.npm_token }} 49 | -------------------------------------------------------------------------------- /lib/rules/tscompat.ts: -------------------------------------------------------------------------------- 1 | import { ESLintUtils } from "@typescript-eslint/utils"; 2 | import { getTypeName } from "@typescript-eslint/type-utils"; 3 | import bcd from "@mdn/browser-compat-data" with { type: "json" }; 4 | import browserslist from "browserslist"; 5 | 6 | import typescript from "typescript"; 7 | const { SymbolFlags } = typescript; // Import CJS module in ESM 8 | 9 | // TODO: Normalize this all into a single browser name so we don't have to be converting all the 10 | // dang time. 11 | import type { 12 | BrowserName as MDNBrowserName, 13 | SimpleSupportStatement, 14 | } from "@mdn/browser-compat-data"; 15 | import type { BrowsersListBrowserName } from "../../types.js"; 16 | 17 | import type { ParserServicesWithTypeInformation } from "@typescript-eslint/utils"; 18 | import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/typescript-estree"; 19 | import type { Type, UnionOrIntersectionType, TypeChecker } from "typescript"; 20 | import type { SimpleSupportStatement } from "@mdn/browser-compat-data"; 21 | 22 | const createRule = ESLintUtils.RuleCreator( 23 | (name) => `https://example.com/rule/${name}`, 24 | ); 25 | 26 | function getConstrainedTypeAtLocation( 27 | services: ParserServicesWithTypeInformation, 28 | node: TSESTree.Node, 29 | ): Type | UnionOrIntersectionType { 30 | const nodeType = services.getTypeAtLocation(node); 31 | const constrained = services.program 32 | .getTypeChecker() 33 | .getBaseConstraintOfType(nodeType); 34 | 35 | return constrained ?? nodeType; 36 | } 37 | 38 | function getFailures({ 39 | support, 40 | browserTargets, 41 | }: { 42 | support: Record; 43 | browserTargets: Partial>; 44 | }): Array<{ name: MDNBrowserName; version: number; supportedSince: number }> { 45 | const supportFailures: Array<{ 46 | name: MDNBrowserName; 47 | version: number; 48 | supportedSince: number; 49 | }> = []; 50 | for (const [name, version] of Object.entries(browserTargets)) { 51 | const browserSupport = support[name as MDNBrowserName]; 52 | if (browserSupport === undefined) continue; 53 | 54 | // If MDN indicates that the feature is not supported at all, then fail. 55 | if (!browserSupport.version_added) { 56 | supportFailures.push({ 57 | name: name as MDNBrowserName, 58 | version, 59 | supportedSince: Infinity, 60 | }); 61 | continue; 62 | } 63 | 64 | // Otherwise, attempt to parse the version at which support was added. 65 | const supportedSince = browserSupport.version_added === true ? 0 : Number.parseFloat(browserSupport.version_added); 66 | if (!Number.isNaN(supportedSince) && version < supportedSince) { 67 | supportFailures.push({ 68 | name: name as MDNBrowserName, 69 | version, 70 | supportedSince, 71 | }); 72 | } 73 | } 74 | return supportFailures; 75 | } 76 | 77 | function browserslistToMdnNames( 78 | browserslistData: Partial>, 79 | ): Partial> { 80 | const nameMap: Partial> = { 81 | chrome: "chrome", 82 | and_chr: "chrome_android", 83 | edge: "edge", 84 | firefox: "firefox", 85 | and_ff: "firefox_android", 86 | android: "webview_android", 87 | ios_saf: "safari_ios", 88 | opera: "opera", 89 | safari: "safari", 90 | samsung: "samsunginternet_android", 91 | }; 92 | for (const [name, value] of Object.entries(browserslistData)) { 93 | // @ts-expect-error TODO 94 | const newName = nameMap[name]; 95 | if (newName === name) continue; 96 | // @ts-expect-error TODO 97 | // eslint-disable-next-line 98 | browserslistData[newName] = value; 99 | // @ts-expect-error TODO 100 | delete browserslistData[name]; 101 | } 102 | return browserslistData; 103 | } 104 | 105 | function findBrowserTargets( 106 | list: string[], 107 | ): Partial> { 108 | const browsers: Partial> = {}; 109 | for (const item of list) { 110 | const [name, version] = item.split(" ") as [ 111 | BrowsersListBrowserName, 112 | string, 113 | ]; 114 | const parsedVersion = Number.parseFloat(version); 115 | if (browsers[name] == null || browsers[name] > parsedVersion) { 116 | browsers[name] = parsedVersion; 117 | } 118 | } 119 | return browserslistToMdnNames(browsers); 120 | } 121 | 122 | function findSupport({ 123 | typeName, 124 | calleeName, 125 | }: { 126 | typeName: string | undefined; 127 | calleeName: string | undefined; 128 | }): Partial< 129 | Record 130 | > { 131 | // TODO: I hate this 132 | if (typeName === "typeof globalThis") { 133 | typeName = calleeName; 134 | calleeName = undefined; 135 | } 136 | 137 | /** @type {Partial> | undefined} */ 138 | let support = 139 | // TODO 140 | (typeName && 141 | calleeName && 142 | bcd.javascript.builtins[typeName]?.[calleeName]?.__compat?.support) || 143 | // when `window.Map()` 144 | (calleeName && bcd.javascript.builtins[calleeName]?.__compat?.support) || 145 | // when Map().size 146 | (typeName && bcd.javascript.builtins[typeName]?.__compat?.support) || 147 | // When `window.fetch()` 148 | (calleeName && bcd.api[calleeName]?.__compat?.support) || 149 | // when Window 150 | (typeName && 151 | calleeName && 152 | bcd.api[typeName]?.[calleeName]?.__compat?.support) || 153 | // When `new ResizeObserver()` 154 | (typeName && bcd.api[typeName]?.__compat?.support); 155 | 156 | if (typeName === "WebAssembly") { 157 | support = bcd.webassembly.api[`${calleeName}_static`]?.__compat?.support; 158 | } 159 | 160 | // First check built-in types (e.g., Set, Array) 161 | if (["Set", "Array", "Map"].includes(typeName) && calleeName) { 162 | support = 163 | bcd.javascript.builtins[typeName]?.[calleeName]?.__compat?.support; 164 | 165 | if (!support) { 166 | support = 167 | bcd.javascript.builtins[typeName]?.prototype?.[calleeName]?.__compat 168 | ?.support; 169 | } 170 | } 171 | 172 | if (!support) return {}; 173 | 174 | // eslint-disable-next-line prefer-const 175 | for (let [key, value] of Object.entries(support || {})) { 176 | if (Array.isArray(value)) { 177 | // @ts-expect-error TODO 178 | value = value.find((x) => !x.prefix); 179 | } 180 | // @ts-expect-error TODO 181 | value.version_added = value.version_added || Infinity; 182 | 183 | // @ts-expect-error TODO 184 | if (value.version_added !== Infinity) { 185 | // @ts-expect-error TODO 186 | support[key] = { 187 | // For _some_ reason the dataset has these unicode characters. 188 | // @ts-expect-error TODO 189 | // eslint-disable-next-line 190 | version_added: value.version_added.replace("≤", ""), 191 | }; 192 | } 193 | } 194 | 195 | return support; 196 | } 197 | 198 | function formatBrowserList( 199 | failures: Array<{ name: MDNBrowserName; version: number }>, 200 | ): unknown { 201 | const mdnNamesToHuman: Partial> = { 202 | chrome: "Chrome", 203 | chrome_android: "Chrome Android", 204 | edge: "Edge", 205 | firefox: "Firefox", 206 | firefox_android: "Firefox Android", 207 | webview_android: "Android", 208 | safari_ios: "Safari iOS", 209 | opera: "Opera", 210 | safari: "Safari", 211 | samsunginternet_android: "Samsung", 212 | }; 213 | 214 | return failures 215 | .sort((a, b) => b.name.localeCompare(a.name)) 216 | .map((x) => `${mdnNamesToHuman[x.name]} ${x.version}`) 217 | .join(", "); 218 | } 219 | 220 | const replaceNameRegex = /<.*>/gmu; 221 | function convertToMDNName(checker: TypeChecker, type: Type): string { 222 | if (checker.isArrayType(type)) { 223 | return "Array"; 224 | } 225 | 226 | const typeName = getTypeName(checker, type) 227 | .replace(replaceNameRegex, "") 228 | .replace("Constructor", "") 229 | .replace("typeof ", ""); 230 | 231 | // Safely check for base types 232 | const symbol = type.getSymbol(); 233 | if (symbol) { 234 | // Only check classes/interfaces 235 | if (symbol.getFlags() & (SymbolFlags.Class | SymbolFlags.Interface)) { 236 | // @ts-expect-error TS(2345): we check if its an interface above 237 | const baseTypes = checker.getBaseTypes(type); 238 | if (baseTypes) { 239 | for (const baseType of baseTypes) { 240 | const baseTypeName = convertToMDNName(checker, baseType); 241 | if ( 242 | ["Set", "Array", "Map", "WeakMap", "WeakSet"].includes(baseTypeName) 243 | ) { 244 | return baseTypeName; 245 | } 246 | } 247 | } 248 | } 249 | } 250 | 251 | if (typeName === "string") return "String"; 252 | if (typeName.endsWith("[]")) return "Array"; 253 | return typeName; 254 | } 255 | 256 | function getType( 257 | checker: TypeChecker, 258 | services: ParserServicesWithTypeInformation, 259 | node: TSESTree.Node, 260 | ): Type { 261 | let type = getConstrainedTypeAtLocation(services, node); 262 | // If this is a union type we gotta handle that specfically 263 | 264 | // If this is a union type we gotta handle that specifically 265 | // TODO: Handle this better actually 266 | if (type.isUnionOrIntersection()) { 267 | for (const subtype of type.types) { 268 | const subtypeName = convertToMDNName(checker, subtype); 269 | if (["Set", "Array", "Map"].includes(subtypeName)) { 270 | return subtype; 271 | } 272 | } 273 | } 274 | 275 | if ("types" in type) { 276 | const found = type.types 277 | .map((type) => { 278 | return { typeName: getTypeName(checker, type), type }; 279 | }) 280 | .find(({ typeName }) => typeName === "Window"); 281 | 282 | if (!found) { 283 | return checker.getAnyType(); 284 | } 285 | // @ts-expect-error todo 286 | type = type.types 287 | .map((type) => { 288 | return { typeName: getTypeName(checker, type), type }; 289 | }) 290 | .find(({ typeName }) => typeName === "Window").type; 291 | } 292 | return type; 293 | } 294 | 295 | function formatTypeName(typeName: string): string { 296 | const specialTypes: Record = { 297 | Window: "window", 298 | "typeof globalThis": "globalThis", 299 | }; 300 | 301 | if (typeName in specialTypes) { 302 | return specialTypes[typeName]; 303 | } 304 | 305 | return typeName; 306 | } 307 | 308 | export const tscompat = createRule({ 309 | create(context) { 310 | const services = ESLintUtils.getParserServices(context); 311 | const checker = services.program.getTypeChecker(); 312 | 313 | // @ts-expect-error TODO 314 | const options = context.options[0]; 315 | 316 | // @ts-expect-error TODO 317 | // eslint-disable-next-line 318 | const browsers = browserslist(options?.browserslist); 319 | 320 | return { 321 | MemberExpression(node: TSESTree.MemberExpression) { 322 | // Check for computed properties using Symbol 323 | if (node.computed) { 324 | let symbolName: string | null = null; 325 | if ( 326 | node.property.type === AST_NODE_TYPES.MemberExpression && 327 | !node.property.computed && 328 | node.property.object.type === AST_NODE_TYPES.Identifier && 329 | node.property.object.name === "Symbol" && 330 | node.property.property.type === AST_NODE_TYPES.Identifier 331 | ) { 332 | symbolName = node.property.property.name; 333 | } 334 | 335 | if (!symbolName) { 336 | return; 337 | } 338 | 339 | // Step 2: Get Type Checker 340 | const tsNode = services.esTreeNodeToTSNodeMap.get( 341 | node.object 342 | ); 343 | 344 | // Step 3: Resolve Type of Base Object 345 | const tsType = checker.getTypeAtLocation(tsNode); 346 | let baseTypeName: string | null = null; 347 | 348 | if (tsType) { 349 | // Get the Fully Qualified Type Name 350 | const symbol = tsType.getSymbol(); 351 | if (symbol) { 352 | baseTypeName = checker.getFullyQualifiedName(symbol); 353 | } else { 354 | baseTypeName = checker.typeToString(tsType); 355 | } 356 | } 357 | 358 | if (!baseTypeName) { 359 | return; 360 | } 361 | 362 | // Step 4: Check Symbol Support in Context of Resolved Type 363 | const support = findSupport({ 364 | typeName: baseTypeName, 365 | calleeName: symbolName, 366 | }); 367 | const failures = getFailures({ 368 | support, 369 | browserTargets: findBrowserTargets(browsers), 370 | }); 371 | 372 | if (failures.length) { 373 | context.report({ 374 | data: { 375 | typeName: `${baseTypeName}[Symbol.${symbolName}]`, 376 | browsers: formatBrowserList(failures), 377 | }, 378 | messageId: "incompatable", 379 | node, 380 | }); 381 | } 382 | return; 383 | } 384 | 385 | // Skip check for array index access (e.g., arr[0]) 386 | if (node.property.type !== AST_NODE_TYPES.Identifier) return; 387 | 388 | const tsProp = services.esTreeNodeToTSNodeMap.get(node.property); 389 | const propertySymbol = checker.getSymbolAtLocation(tsProp); 390 | if (propertySymbol) { 391 | const declarations = propertySymbol.getDeclarations(); 392 | if (declarations?.length) { 393 | // Determine if all declarations come from a lib file (built-in) 394 | const isBuiltin = declarations.every((decl) => { 395 | const fileName = decl.getSourceFile().fileName; 396 | // Adjust the check as needed to match the paths of your TS lib files 397 | return ( 398 | fileName.includes("typescript/lib") || 399 | // this is needed so that e.g. `Array#at` is checked correctly 400 | fileName.includes("@types/node/globals.d.ts") 401 | ); 402 | }); 403 | if (!isBuiltin) { 404 | // If at least one declaration is not from a lib file, it's a custom method. 405 | // Skip the compatibility check. 406 | return; 407 | } 408 | } 409 | } 410 | 411 | const checkType = (type: Type) => { 412 | const typeName = convertToMDNName(checker, type); 413 | 414 | if (!typeName || typeName === "{}") return; 415 | 416 | // @ts-expect-error It's possible for the property of the member expression to not have a 417 | // name. We should test for this. TODO 418 | const calleeName = node.property.name; 419 | 420 | const support = findSupport({ typeName, calleeName }); 421 | const browserTargets = findBrowserTargets(browsers); 422 | const failures = getFailures({ support, browserTargets }); 423 | 424 | if (failures.length) { 425 | const humanReadableBrowsers = formatBrowserList(failures); 426 | 427 | context.report({ 428 | data: { 429 | typeName: `${formatTypeName(typeName)}.${calleeName}()`, 430 | browsers: humanReadableBrowsers, 431 | }, 432 | messageId: "incompatable", 433 | node, 434 | }); 435 | } 436 | 437 | if (Object.keys(support).length > 0) { 438 | // Only check most recently overridden definition 439 | return 440 | } 441 | 442 | const symbol = type.getSymbol() 443 | if (symbol && symbol.getFlags() & (SymbolFlags.Class | SymbolFlags.Interface)) { 444 | const baseTypes = checker.getBaseTypes(type); 445 | if (baseTypes) { 446 | baseTypes.forEach((baseType) => checkType(baseType)) 447 | } 448 | } 449 | }; 450 | const type = getType(checker, services, node.object); 451 | checkType(type); 452 | }, 453 | NewExpression(node: TSESTree.NewExpression) { 454 | // If we are doing `window.Map()`, then let `MemberExpression` handle this. 455 | // eslint-disable-next-line 456 | if (node.callee.type === AST_NODE_TYPES.MemberExpression) return; 457 | 458 | const type = getType(checker, services, node); 459 | let typeName = convertToMDNName(checker, type); 460 | // @ts-expect-error TODO `name` can be undefined 461 | typeName = typeName === "any" ? node.callee.name : typeName; 462 | 463 | if (!typeName) return; 464 | 465 | const support = findSupport({ typeName, calleeName: undefined }); 466 | const browserTargets = findBrowserTargets(browsers); 467 | const failures = getFailures({ support, browserTargets }); 468 | 469 | if (failures.length) { 470 | const humanReadableBrowsers = formatBrowserList(failures); 471 | 472 | context.report({ 473 | data: { 474 | typeName: formatTypeName(typeName), 475 | browsers: humanReadableBrowsers, 476 | }, 477 | messageId: "incompatable", 478 | node, 479 | }); 480 | } 481 | }, 482 | CallExpression(node: TSESTree.CallExpression) { 483 | // @ts-expect-error TODO `name` can be undefined 484 | const typeName: string = node.callee.name; 485 | 486 | if (!typeName) return; 487 | 488 | const support = findSupport({ typeName, calleeName: undefined }); 489 | const browserTargets = findBrowserTargets(browsers); 490 | const failures = getFailures({ support, browserTargets }); 491 | 492 | if (failures.length) { 493 | const humanReadableBrowsers = formatBrowserList(failures); 494 | 495 | context.report({ 496 | data: { 497 | typeName: `${formatTypeName(typeName)}()`, 498 | browsers: humanReadableBrowsers, 499 | }, 500 | messageId: "incompatable", 501 | node, 502 | }); 503 | } 504 | }, 505 | }; 506 | }, 507 | meta: { 508 | type: "problem", 509 | docs: { 510 | description: "enforce cross-browser compatability in codebase", 511 | url: "https://github.com/koddsson/eslint-config-tscompat", 512 | recommended: true, 513 | }, 514 | schema: { 515 | type: "array", 516 | items: [ 517 | { 518 | type: "object", 519 | properties: { 520 | browserslist: { 521 | type: "array", 522 | }, 523 | }, 524 | additionalProperties: false, 525 | }, 526 | ], 527 | }, 528 | defaultOptions: [{ browserslist: [">0.3%", "not dead"] }], 529 | messages: { 530 | incompatable: "{{typeName}} is not supported in {{browsers}}", 531 | }, 532 | }, 533 | name: "tscompat", 534 | defaultOptions: [], 535 | }); 536 | -------------------------------------------------------------------------------- /tests/tscompat.ts: -------------------------------------------------------------------------------- 1 | import browserslist from "browserslist"; 2 | 3 | import { ruleTester } from "./test-helper.js"; 4 | import { tscompat } from "../lib/rules/tscompat.js"; 5 | 6 | ruleTester.run("tscompat", tscompat, { 7 | valid: [ 8 | { 9 | code: "const s = new Set(); s.intersection();", 10 | options: [ 11 | { 12 | browserslist: ["chrome 122"], 13 | }, 14 | ], 15 | }, 16 | { 17 | code: `const a = [1,2,3]; a.at(1);`, 18 | options: [{ browserslist: ["chrome >= 123"] }], 19 | }, 20 | { 21 | code: "Promise.allSettled()", 22 | options: [{ browserslist: browserslist.defaults }], 23 | }, 24 | { 25 | code: "new ServiceWorker()", 26 | options: [{ browserslist: browserslist.defaults }], 27 | }, 28 | { 29 | code: "new IntersectionObserver(() => {}, {})", 30 | options: [{ browserslist: browserslist.defaults }], 31 | }, 32 | { 33 | code: "Array.from()", 34 | options: [ 35 | { 36 | browserslist: ["ExplorerMobile 10"], 37 | }, 38 | ], 39 | }, 40 | { 41 | code: "if (fetch) {\n fetch()\n }", 42 | options: [ 43 | { 44 | browserslist: ["ExplorerMobile 10"], 45 | }, 46 | ], 47 | }, 48 | { 49 | code: "if (Array.prototype.flat) {\n new Array.flat()\n }", 50 | options: [ 51 | { 52 | browserslist: ["ExplorerMobile 10"], 53 | }, 54 | ], 55 | }, 56 | { 57 | code: "if (fetch && otherConditions) {\n fetch()\n }", 58 | options: [ 59 | { 60 | browserslist: ["ExplorerMobile 10"], 61 | }, 62 | ], 63 | }, 64 | { 65 | code: "if (window.fetch) {\n fetch()\n }", 66 | options: [ 67 | { 68 | browserslist: ["ExplorerMobile 10"], 69 | }, 70 | ], 71 | }, 72 | { 73 | code: "if ('fetch' in window) {\n fetch()\n }", 74 | options: [ 75 | { 76 | browserslist: ["ExplorerMobile 10"], 77 | }, 78 | ], 79 | }, 80 | { 81 | code: "window", 82 | options: [ 83 | { 84 | browserslist: ["ExplorerMobile 10"], 85 | }, 86 | ], 87 | }, 88 | { 89 | code: "document.fonts()", 90 | options: [ 91 | { 92 | browserslist: ["edge 79"], 93 | }, 94 | ], 95 | }, 96 | { 97 | code: "Promise.resolve()", 98 | options: [ 99 | { 100 | browserslist: ["node 10"], 101 | }, 102 | ], 103 | }, 104 | { 105 | code: "document.documentElement()", 106 | options: [ 107 | { 108 | browserslist: ["Safari 11", "Opera 57", "Edge 17"], 109 | }, 110 | ], 111 | }, 112 | { 113 | code: "document.getElementsByTagName()", 114 | options: [ 115 | { 116 | browserslist: ["Safari 11", "Opera 57", "Edge 17"], 117 | }, 118 | ], 119 | }, 120 | { 121 | code: 'Promise.resolve("foo")', 122 | options: [ 123 | { 124 | browserslist: ["ie 8"], 125 | }, 126 | ], 127 | }, 128 | { 129 | code: "history.back()", 130 | options: [ 131 | { 132 | browserslist: ["Safari 11", "Opera 57", "Edge 17"], 133 | }, 134 | ], 135 | }, 136 | { 137 | code: "document.querySelector()", 138 | options: [ 139 | { 140 | browserslist: ["Safari 11", "Opera 57", "Edge 17"], 141 | }, 142 | ], 143 | }, 144 | { 145 | code: "new ServiceWorker()", 146 | options: [ 147 | { 148 | browserslist: ["chrome 57", "firefox 50"], 149 | }, 150 | ], 151 | }, 152 | { 153 | code: "document.currentScript()", 154 | options: [ 155 | { 156 | browserslist: ["chrome 57", "firefox 50", "safari 10", "edge 14"], 157 | }, 158 | ], 159 | }, 160 | { 161 | code: "document.querySelector()", 162 | options: [ 163 | { 164 | browserslist: ["ChromeAndroid 80"], 165 | }, 166 | ], 167 | }, 168 | { 169 | code: "document.hasFocus()", 170 | options: [ 171 | { 172 | browserslist: ["Chrome 34"], 173 | }, 174 | ], 175 | }, 176 | { 177 | code: "new URL()", 178 | options: [ 179 | { 180 | browserslist: ["ChromeAndroid 78", "ios 11"], 181 | }, 182 | ], 183 | }, 184 | { 185 | code: "document.currentScript('some')", 186 | options: [ 187 | { 188 | browserslist: ["chrome 57", "firefox 50", "safari 10", "edge 14"], 189 | }, 190 | ], 191 | }, 192 | { 193 | code: "WebAssembly.compile()", 194 | options: [ 195 | { 196 | browserslist: ["chrome 57"], 197 | }, 198 | ], 199 | }, 200 | { 201 | code: "new IntersectionObserver(() => {}, {});", 202 | options: [ 203 | { 204 | browserslist: ["chrome 58"], 205 | }, 206 | ], 207 | }, 208 | { 209 | code: "new URL('http://example')", 210 | options: [ 211 | { 212 | browserslist: ["chrome 32", "safari 7.1", "firefox 26"], 213 | }, 214 | ], 215 | }, 216 | { 217 | code: "new URLSearchParams()", 218 | options: [ 219 | { 220 | browserslist: ["chrome 49", "safari 10.1", "firefox 44"], 221 | }, 222 | ], 223 | }, 224 | ], 225 | invalid: [ 226 | { 227 | code: "const s = new Set(); s.intersection();", 228 | options: [ 229 | { 230 | browserslist: ["chrome 121"], 231 | }, 232 | ], 233 | errors: [ 234 | { message: "Set.intersection() is not supported in Chrome 121" }, 235 | ], 236 | }, 237 | { 238 | code: `const a = [1,2,3]; a.at(1);`, 239 | options: [{ browserslist: ["chrome >= 70", "firefox >= 80"] }], 240 | errors: [ 241 | { message: "Array.at() is not supported in Firefox 80, Chrome 70" }, 242 | ], 243 | }, 244 | { 245 | code: "Promise.allSettled()", 246 | options: [ 247 | { 248 | browserslist: [ 249 | "chrome >= 72", 250 | "firefox >= 72", 251 | "safari >= 12", 252 | "edge >= 79", 253 | ], 254 | }, 255 | ], 256 | errors: [ 257 | { 258 | message: 259 | "Promise.allSettled() is not supported in Safari 12, Chrome 72", 260 | }, 261 | ], 262 | }, 263 | { 264 | code: "new ServiceWorker();", 265 | options: [{ browserslist: ["chrome 31"] }], 266 | errors: [ 267 | { 268 | message: "ServiceWorker is not supported in Chrome 31", 269 | type: "NewExpression", 270 | }, 271 | ], 272 | }, 273 | { 274 | code: "new IntersectionObserver(() => {}, {})", 275 | options: [{ browserslist: ["chrome 49"] }], 276 | errors: [ 277 | { 278 | message: "IntersectionObserver is not supported in Chrome 49", 279 | type: "NewExpression", 280 | }, 281 | ], 282 | }, 283 | { 284 | code: "window?.fetch?.('example.com')", 285 | errors: [ 286 | { 287 | message: "window.fetch() is not supported in Chrome 39", 288 | }, 289 | ], 290 | options: [ 291 | { 292 | browserslist: ["chrome 39"], 293 | }, 294 | ], 295 | }, 296 | { 297 | code: "navigator.hardwareConcurrency;\n navigator.serviceWorker;\n new SharedWorker();", 298 | errors: [ 299 | { 300 | message: 301 | "Navigator.hardwareConcurrency() is not supported in Chrome 4", 302 | }, 303 | { 304 | message: "Navigator.serviceWorker() is not supported in Chrome 4", 305 | }, 306 | { 307 | message: "SharedWorker is not supported in Chrome 4", 308 | }, 309 | ], 310 | options: [ 311 | { 312 | browserslist: ["chrome 4"], 313 | }, 314 | ], 315 | }, 316 | { 317 | code: '// it should throw an error here, but it doesn\'t\n const event = new CustomEvent("cat", {\n detail: {\n hazcheeseburger: true\n }\n });\n window.dispatchEvent(event);', 318 | errors: [ 319 | { 320 | message: "CustomEvent is not supported in Chrome 4", 321 | }, 322 | ], 323 | options: [ 324 | { 325 | browserslist: ["chrome 4"], 326 | }, 327 | ], 328 | }, 329 | { 330 | code: "Array.from()", 331 | errors: [ 332 | { 333 | message: "Array.from() is not supported in Chrome 44", 334 | }, 335 | ], 336 | options: [ 337 | { 338 | browserslist: ["chrome 44"], 339 | }, 340 | ], 341 | }, 342 | { 343 | code: "Promise.allSettled()", 344 | errors: [ 345 | { 346 | message: 347 | "Promise.allSettled() is not supported in Safari 12, Chrome 72", 348 | }, 349 | ], 350 | options: [ 351 | { 352 | browserslist: [ 353 | "Chrome >= 72", 354 | "Firefox >= 72", 355 | "Safari >= 12", 356 | "Edge >= 79", 357 | ], 358 | }, 359 | ], 360 | }, 361 | { 362 | code: "location.origin", 363 | errors: [ 364 | { 365 | message: "Location.origin() is not supported in Chrome 7", 366 | }, 367 | ], 368 | options: [ 369 | { 370 | browserslist: ["chrome 7"], 371 | }, 372 | ], 373 | }, 374 | { 375 | code: "import { Map } from 'immutable';\n new Set()", 376 | errors: [ 377 | { 378 | message: "Set is not supported in Chrome 37", 379 | type: "NewExpression", 380 | }, 381 | ], 382 | options: [ 383 | { 384 | browserslist: ["Chrome 37"], 385 | }, 386 | ], 387 | }, 388 | { 389 | code: "new Set()", 390 | errors: [ 391 | { 392 | message: "Set is not supported in Chrome 37", 393 | type: "NewExpression", 394 | }, 395 | ], 396 | options: [ 397 | { 398 | browserslist: ["chrome 37"], 399 | }, 400 | ], 401 | }, 402 | { 403 | code: "new TypedArray()", 404 | errors: [ 405 | { 406 | message: "TypedArray is not supported in Chrome 6", 407 | type: "NewExpression", 408 | }, 409 | ], 410 | options: [ 411 | { 412 | browserslist: ["chrome 6"], 413 | }, 414 | ], 415 | }, 416 | { 417 | code: "new Int8Array()", 418 | errors: [ 419 | { 420 | message: "Int8Array is not supported in Chrome 6", 421 | type: "NewExpression", 422 | }, 423 | ], 424 | options: [ 425 | { 426 | browserslist: ["chrome 6"], 427 | }, 428 | ], 429 | }, 430 | { 431 | code: "new AnimationEvent", 432 | errors: [ 433 | { 434 | message: "AnimationEvent is not supported in Chrome 40", 435 | type: "NewExpression", 436 | }, 437 | ], 438 | options: [ 439 | { 440 | browserslist: ["chrome 40"], 441 | }, 442 | ], 443 | }, 444 | { 445 | code: "Object.values({})", 446 | errors: [ 447 | { 448 | message: "Object.values() is not supported in Safari 9", 449 | type: "MemberExpression", 450 | }, 451 | ], 452 | options: [ 453 | { 454 | browserslist: ["safari 9"], 455 | }, 456 | ], 457 | }, 458 | { 459 | code: "new ServiceWorker()", 460 | errors: [ 461 | { 462 | message: "ServiceWorker is not supported in Chrome 31", 463 | type: "NewExpression", 464 | }, 465 | ], 466 | options: [ 467 | { 468 | browserslist: ["chrome 31"], 469 | }, 470 | ], 471 | }, 472 | { 473 | code: "new IntersectionObserver(() => {}, {});", 474 | errors: [ 475 | { 476 | message: "IntersectionObserver is not supported in Chrome 49", 477 | type: "NewExpression", 478 | }, 479 | ], 480 | options: [ 481 | { 482 | browserslist: ["chrome 49"], 483 | }, 484 | ], 485 | }, 486 | { 487 | code: "WebAssembly.compile()", 488 | errors: [ 489 | { 490 | message: 491 | "WebAssembly.compile() is not supported in Samsung 4, Safari iOS 10.3, Safari 10.1, Opera 12.1, Edge 14, Chrome 23", 492 | type: "MemberExpression", 493 | }, 494 | ], 495 | options: [ 496 | { 497 | browserslist: [ 498 | "Samsung 4", 499 | "Safari 10.1", 500 | "Opera 12.1", 501 | "chrome 23", 502 | "iOS 10.3", 503 | "ExplorerMobile 10", 504 | "chrome 31", 505 | "Edge 14", 506 | "Blackberry 7", 507 | "Baidu 7.12", 508 | "UCAndroid 11.8", 509 | "QQAndroid 1.2", 510 | ], 511 | }, 512 | ], 513 | }, 514 | { 515 | code: "new PaymentRequest(methodData, details, options)", 516 | errors: [ 517 | { 518 | message: "PaymentRequest is not supported in Chrome 57", 519 | type: "NewExpression", 520 | }, 521 | ], 522 | options: [ 523 | { 524 | browserslist: ["chrome 57"], 525 | }, 526 | ], 527 | }, 528 | { 529 | code: "navigator.serviceWorker", 530 | errors: [ 531 | { 532 | message: "Navigator.serviceWorker() is not supported in Safari 10.1", 533 | type: "MemberExpression", 534 | }, 535 | ], 536 | options: [ 537 | { 538 | browserslist: ["safari 10.1"], 539 | }, 540 | ], 541 | }, 542 | { 543 | code: "window.document.fonts()", 544 | errors: [ 545 | { 546 | message: "Document.fonts() is not supported in Chrome 34", 547 | type: "MemberExpression", 548 | }, 549 | ], 550 | options: [ 551 | { 552 | browserslist: ["chrome 34"], 553 | }, 554 | ], 555 | }, 556 | { 557 | code: "new Map().size", 558 | errors: [ 559 | { 560 | message: "Map.size() is not supported in Chrome 37", 561 | type: "MemberExpression", 562 | }, 563 | { 564 | message: "Map is not supported in Chrome 37", 565 | type: "NewExpression", 566 | }, 567 | ], 568 | options: [ 569 | { 570 | browserslist: ["Chrome 37"], 571 | }, 572 | ], 573 | }, 574 | { 575 | code: "new window.Map().size", 576 | errors: [ 577 | { 578 | message: "Map.size() is not supported in Chrome 37", 579 | type: "MemberExpression", 580 | }, 581 | { 582 | message: "window.Map() is not supported in Chrome 37", 583 | type: "MemberExpression", 584 | }, 585 | ], 586 | options: [ 587 | { 588 | browserslist: ["chrome 37"], 589 | }, 590 | ], 591 | }, 592 | { 593 | code: "const m = new window.Map(); m.size", 594 | errors: [ 595 | { 596 | message: "window.Map() is not supported in Chrome 37", 597 | type: "MemberExpression", 598 | }, 599 | { 600 | message: "Map.size() is not supported in Chrome 37", 601 | type: "MemberExpression", 602 | }, 603 | ], 604 | options: [ 605 | { 606 | browserslist: ["chrome 37"], 607 | }, 608 | ], 609 | }, 610 | { 611 | code: "const m = new window.Map(); m.size()", 612 | errors: [ 613 | { 614 | message: "window.Map() is not supported in Chrome 37", 615 | type: "MemberExpression", 616 | }, 617 | { 618 | message: "Map.size() is not supported in Chrome 37", 619 | type: "MemberExpression", 620 | }, 621 | ], 622 | options: [ 623 | { 624 | browserslist: ["chrome 37"], 625 | }, 626 | ], 627 | }, 628 | { 629 | code: "new Array().flat", 630 | errors: [ 631 | { 632 | message: "Array.flat() is not supported in Chrome 68", 633 | type: "MemberExpression", 634 | }, 635 | ], 636 | options: [ 637 | { 638 | browserslist: ["chrome 68"], 639 | }, 640 | ], 641 | }, 642 | { 643 | code: "const a = new Array(); a.flat", 644 | errors: [ 645 | { 646 | message: "Array.flat() is not supported in Chrome 68", 647 | type: "MemberExpression", 648 | }, 649 | ], 650 | options: [ 651 | { 652 | browserslist: ["chrome 68"], 653 | }, 654 | ], 655 | }, 656 | { 657 | code: "const a = new Array(); a.flat()", 658 | errors: [ 659 | { 660 | message: "Array.flat() is not supported in Chrome 68", 661 | type: "MemberExpression", 662 | }, 663 | ], 664 | options: [ 665 | { 666 | browserslist: ["chrome 68"], 667 | }, 668 | ], 669 | }, 670 | { 671 | code: "globalThis.fetch()", 672 | errors: [ 673 | { 674 | message: "globalThis.fetch() is not supported in Chrome 39", 675 | type: "MemberExpression", 676 | }, 677 | ], 678 | options: [ 679 | { 680 | browserslist: ["Chrome 39"], 681 | }, 682 | ], 683 | }, 684 | { 685 | code: "fetch()", 686 | errors: [ 687 | { 688 | message: "fetch() is not supported in Chrome 39", 689 | type: "CallExpression", 690 | }, 691 | ], 692 | options: [ 693 | { 694 | browserslist: ["chrome 39"], 695 | }, 696 | ], 697 | }, 698 | { 699 | code: "Promise.resolve()", 700 | errors: [ 701 | { 702 | message: "Promise.resolve() is not supported in Chrome 31", 703 | type: "MemberExpression", 704 | }, 705 | ], 706 | options: [ 707 | { 708 | browserslist: ["chrome 31"], 709 | }, 710 | ], 711 | }, 712 | { 713 | code: "Promise.all()", 714 | errors: [ 715 | { 716 | message: "Promise.all() is not supported in Chrome 31", 717 | type: "MemberExpression", 718 | }, 719 | ], 720 | options: [ 721 | { 722 | browserslist: ["chrome 31"], 723 | }, 724 | ], 725 | }, 726 | { 727 | code: "Promise.race()", 728 | errors: [ 729 | { 730 | message: "Promise.race() is not supported in Chrome 31", 731 | type: "MemberExpression", 732 | }, 733 | ], 734 | options: [ 735 | { 736 | browserslist: ["chrome 31"], 737 | }, 738 | ], 739 | }, 740 | { 741 | code: "Promise.reject()", 742 | errors: [ 743 | { 744 | message: "Promise.reject() is not supported in Chrome 31", 745 | type: "MemberExpression", 746 | }, 747 | ], 748 | options: [ 749 | { 750 | browserslist: ["chrome 31"], 751 | }, 752 | ], 753 | }, 754 | { 755 | code: "new URL('http://example')", 756 | errors: [ 757 | { 758 | message: "URL is not supported in Safari 6, Firefox 18, Chrome 31", 759 | type: "NewExpression", 760 | }, 761 | ], 762 | options: [ 763 | { 764 | browserslist: ["chrome 31", "safari 6", "firefox 18"], 765 | }, 766 | ], 767 | }, 768 | { 769 | code: "new URLSearchParams()", 770 | errors: [ 771 | { 772 | message: 773 | "URLSearchParams is not supported in Safari 10, Firefox 28, Chrome 48", 774 | type: "NewExpression", 775 | }, 776 | ], 777 | options: [ 778 | { 779 | browserslist: ["chrome 48", "safari 10", "firefox 28"], 780 | }, 781 | ], 782 | }, 783 | { 784 | code: "performance.now()", 785 | errors: [ 786 | { 787 | message: "Performance.now() is not supported in Chrome 19", 788 | type: "MemberExpression", 789 | }, 790 | ], 791 | options: [ 792 | { 793 | browserslist: ["chrome 19"], 794 | }, 795 | ], 796 | }, 797 | { 798 | code: "new ResizeObserver()", 799 | errors: [ 800 | { 801 | message: "ResizeObserver is not supported in Safari 12, Chrome 39", 802 | }, 803 | ], 804 | options: [ 805 | { 806 | browserslist: ["Chrome 39", "safari 12"], 807 | }, 808 | ], 809 | }, 810 | { 811 | code: "'foo'.at(5)", 812 | errors: [ 813 | { 814 | message: "String.at() is not supported in Safari 12, Chrome 39", 815 | }, 816 | ], 817 | options: [ 818 | { 819 | browserslist: ["Chrome 39", "safari 12"], 820 | }, 821 | ], 822 | }, 823 | { 824 | code: "const a = 'foo'; a.at(5)", 825 | errors: [ 826 | { 827 | message: "String.at() is not supported in Safari 12, Chrome 39", 828 | }, 829 | ], 830 | options: [ 831 | { 832 | browserslist: ["Chrome 39", "safari 12"], 833 | }, 834 | ], 835 | }, 836 | { 837 | code: "const a = []; a.at(5)", 838 | errors: [ 839 | { 840 | message: "Array.at() is not supported in Safari 12, Chrome 39", 841 | }, 842 | ], 843 | options: [ 844 | { 845 | browserslist: ["Chrome 39", "safari 12"], 846 | }, 847 | ], 848 | }, 849 | { 850 | code: "[].at(5)", 851 | errors: [ 852 | { 853 | message: "Array.at() is not supported in Safari 12, Chrome 39", 854 | }, 855 | ], 856 | options: [ 857 | { 858 | browserslist: ["Chrome 39", "safari 12"], 859 | }, 860 | ], 861 | }, 862 | { 863 | code: "Object.entries({}), Object.values({})", 864 | errors: [ 865 | { 866 | message: 867 | "Object.entries() is not supported in Android 4, Safari iOS 7", 868 | }, 869 | { 870 | message: 871 | "Object.values() is not supported in Android 4, Safari iOS 7", 872 | }, 873 | ], 874 | options: [ 875 | { 876 | browserslist: ["Android >= 4", "iOS >= 7"], 877 | }, 878 | ], 879 | }, 880 | { 881 | code: "window.requestIdleCallback(() => {})", 882 | errors: [ 883 | { 884 | message: "window.requestIdleCallback() is not supported in Safari 12", 885 | }, 886 | ], 887 | options: [ 888 | { 889 | browserslist: ["safari 12"], 890 | }, 891 | ], 892 | }, 893 | { 894 | code: "window.requestAnimationFrame(() => {})", 895 | errors: [ 896 | { 897 | message: 898 | "window.requestAnimationFrame() is not supported in Chrome 23", 899 | }, 900 | ], 901 | options: [ 902 | { 903 | browserslist: ["chrome 23"], 904 | }, 905 | ], 906 | }, 907 | { 908 | // s.values() is a IterableIterator which extends Iterator and map() is from Iterator. 909 | code: "const s = [0, 1, 2]; s.values().map((x) => x);", 910 | options: [ 911 | { 912 | browserslist: ["chrome 121"], 913 | }, 914 | ], 915 | errors: [{ message: "Iterator.map() is not supported in Chrome 121" }], 916 | }, 917 | ], 918 | }); 919 | --------------------------------------------------------------------------------