diff --git a/package-lock.json b/package-lock.json index 5eb6dc5..81ca961 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,11 +14,12 @@ "verkit": "^0.5.0" }, "devDependencies": { + "@eslint/css": "^2.0.1", "@eslint/js": "^10.0.1", "@eslint/json": "^2.0.1", "@types/node": "^26.1.0", - "@typescript-eslint/rule-tester": "^8.62.1", - "@typescript-eslint/typescript-estree": "^8.62.1", + "@typescript-eslint/rule-tester": "^8.72.0", + "@typescript-eslint/typescript-estree": "^8.72.0", "@vitest/coverage-v8": "^4.1.9", "eslint": "^10.6.0", "eslint-plugin-eslint-plugin": "^7.4.1", @@ -26,7 +27,7 @@ "oxlint": "^1.72.0", "prettier": "^3.9.4", "typescript": "^6.0.3", - "typescript-eslint": "^8.62.1", + "typescript-eslint": "^8.72.0", "vitest": "^4.0.14" }, "peerDependencies": { @@ -219,6 +220,35 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@eslint/css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@eslint/css/-/css-2.0.1.tgz", + "integrity": "sha512-1qIQFzuFeTaHF3ewzsj/HUDC9RTDwfBHGWdFvpmOG0R/Tb3Mt+17yltbE10/LK+oglS1fP9+tiG5UzMvWgQY9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "@eslint/css-tree": "^4.1.1", + "@eslint/plugin-kit": "^0.7.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/css-tree": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@eslint/css-tree/-/css-tree-4.1.2.tgz", + "integrity": "sha512-rVcxLmwSkEL3aWS3vAgsTRrQHoYE1b0XcgDtKHDJ92XOkyUpcbS/g0tUJKhvCtlEhYzgKBWsgDd+Z/aPhd166w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.37.2", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, "node_modules/@eslint/js": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", @@ -267,9 +297,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", - "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1096,17 +1126,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz", - "integrity": "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.0.tgz", + "integrity": "sha512-aysKgysPX9XOv/dk95UkvUDe++Q3n3dJPG9CEWT3RUccxfkPJ+Qn6nhKPDhZXU6Ofo9kgwGWwKyQCnZIXTgngg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.62.1", - "@typescript-eslint/type-utils": "8.62.1", - "@typescript-eslint/utils": "8.62.1", - "@typescript-eslint/visitor-keys": "8.62.1", + "@typescript-eslint/scope-manager": "8.72.0", + "@typescript-eslint/type-utils": "8.72.0", + "@typescript-eslint/utils": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -1119,15 +1149,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.62.1", + "@typescript-eslint/parser": "^8.72.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.13.tgz", + "integrity": "sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==", "dev": true, "license": "MIT", "engines": { @@ -1135,16 +1165,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz", - "integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.0.tgz", + "integrity": "sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.62.1", - "@typescript-eslint/types": "8.62.1", - "@typescript-eslint/typescript-estree": "8.62.1", - "@typescript-eslint/visitor-keys": "8.62.1", + "@typescript-eslint/scope-manager": "8.72.0", + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0", "debug": "^4.4.3" }, "engines": { @@ -1160,14 +1190,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz", - "integrity": "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.0.tgz", + "integrity": "sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.62.1", - "@typescript-eslint/types": "^8.62.1", + "@typescript-eslint/tsconfig-utils": "^8.72.0", + "@typescript-eslint/types": "^8.72.0", "debug": "^4.4.3" }, "engines": { @@ -1182,15 +1212,15 @@ } }, "node_modules/@typescript-eslint/rule-tester": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-8.62.1.tgz", - "integrity": "sha512-qq2Io5qOVAyKsXfcgpqc5qIef++7bFMit3R8gNLNYlxrivwWtZH5pfIuU0nc69mUHAxowv4sUGfbpwykfBGHYQ==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-8.72.0.tgz", + "integrity": "sha512-3DkhIwJJ1EgssS9r613P8mqraY/c0uSg9e5rQdYPKf71poFqIrN/6agYEtTzI9SvVLgFqfbCwpKLr0zRBij0UQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/parser": "8.62.1", - "@typescript-eslint/typescript-estree": "8.62.1", - "@typescript-eslint/utils": "8.62.1", + "@typescript-eslint/parser": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0", + "@typescript-eslint/utils": "8.72.0", "ajv": "^6.12.6", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "4.6.2", @@ -1209,14 +1239,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz", - "integrity": "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.0.tgz", + "integrity": "sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.62.1", - "@typescript-eslint/visitor-keys": "8.62.1" + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1227,9 +1257,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz", - "integrity": "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.0.tgz", + "integrity": "sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA==", "dev": true, "license": "MIT", "engines": { @@ -1244,15 +1274,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz", - "integrity": "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.0.tgz", + "integrity": "sha512-gEXxmmbSTSOVTC37IYYvremOM31v+UN2W772bJxmVYPhAOPSUxtacyb7wkm1EhkYKU6KzFJsdU44DCV4to0LWw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.62.1", - "@typescript-eslint/typescript-estree": "8.62.1", - "@typescript-eslint/utils": "8.62.1", + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0", + "@typescript-eslint/utils": "8.72.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -1269,9 +1299,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz", - "integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.0.tgz", + "integrity": "sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ==", "dev": true, "license": "MIT", "engines": { @@ -1283,16 +1313,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz", - "integrity": "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.0.tgz", + "integrity": "sha512-gwLfFgWcw15tN2KXDCJvxEOQjPn+P3j/n3eSqkZ5sX4VpW9Cosodr+tWwBZRvJT3TcKx6HnPRGZMCNAiP1Xhug==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.62.1", - "@typescript-eslint/tsconfig-utils": "8.62.1", - "@typescript-eslint/types": "8.62.1", - "@typescript-eslint/visitor-keys": "8.62.1", + "@typescript-eslint/project-service": "8.72.0", + "@typescript-eslint/tsconfig-utils": "8.72.0", + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1311,16 +1341,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.1.tgz", - "integrity": "sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.0.tgz", + "integrity": "sha512-0up3oArJZBYnzOaEXKo8DmI43QvMZyPoFEt9LWzRJcyl/RcJjfKKPebknN1VbTgXBf3M/KggUh5uUMdURLnseg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.62.1", - "@typescript-eslint/types": "8.62.1", - "@typescript-eslint/typescript-estree": "8.62.1" + "@typescript-eslint/scope-manager": "8.72.0", + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1335,13 +1365,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz", - "integrity": "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.0.tgz", + "integrity": "sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/types": "8.72.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -2497,6 +2527,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mdn-data": { + "version": "2.37.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.37.2.tgz", + "integrity": "sha512-rz9YOBGItdSbe7HcSArUbhiAv4SB/4DKRYg96CSFS2CcIYMbUvBtZlzA1NCbk2AdgbmYwMqwNblLpR441lLeiQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", @@ -2981,16 +3018,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.1.tgz", - "integrity": "sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==", + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.0.tgz", + "integrity": "sha512-r26Fkc8As+mZvqp7SLsMmNFyYUup/6CjK7/eHBHmhOw6GH6n6YFxHVFrG0jN1c5CMpDzelLPxvgeCOzx7BjN+A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.62.1", - "@typescript-eslint/parser": "8.62.1", - "@typescript-eslint/typescript-estree": "8.62.1", - "@typescript-eslint/utils": "8.62.1" + "@typescript-eslint/eslint-plugin": "8.72.0", + "@typescript-eslint/parser": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0", + "@typescript-eslint/utils": "8.72.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index 643142e..90cec6b 100644 --- a/package.json +++ b/package.json @@ -35,11 +35,12 @@ "lint:format": "prettier --check src" }, "devDependencies": { + "@eslint/css": "^2.0.1", "@eslint/js": "^10.0.1", "@eslint/json": "^2.0.1", "@types/node": "^26.1.0", - "@typescript-eslint/rule-tester": "^8.62.1", - "@typescript-eslint/typescript-estree": "^8.62.1", + "@typescript-eslint/rule-tester": "^8.72.0", + "@typescript-eslint/typescript-estree": "^8.72.0", "@vitest/coverage-v8": "^4.1.9", "eslint": "^10.6.0", "eslint-plugin-eslint-plugin": "^7.4.1", @@ -47,7 +48,7 @@ "oxlint": "^1.72.0", "prettier": "^3.9.4", "typescript": "^6.0.3", - "typescript-eslint": "^8.62.1", + "typescript-eslint": "^8.72.0", "vitest": "^4.0.14" }, "peerDependencies": { diff --git a/src/main.ts b/src/main.ts index b5fac37..71cbe2e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -33,6 +33,7 @@ import {preferGetOrInsert} from './rules/prefer-get-or-insert.js'; import {preferCharCodeAtInLoop} from './rules/prefer-charcode-at-in-loop.js'; import {preferSliceOverSplitIndex} from './rules/prefer-slice-over-split-index.js'; import {preferThrowIfNoEntry} from './rules/prefer-throw-if-no-entry.js'; +import {baselineTarget} from './rules/baseline-target.js'; const plugin: ESLint.Plugin = { meta: { @@ -71,7 +72,8 @@ const plugin: ESLint.Plugin = { preferCharCodeAtInLoop as never as Rule.RuleModule, 'prefer-slice-over-split-index': preferSliceOverSplitIndex, 'prefer-throw-if-no-entry': preferThrowIfNoEntry, - 'ban-dependencies': banDependencies + 'ban-dependencies': banDependencies, + 'baseline-target': baselineTarget as never as Rule.RuleModule } }; diff --git a/src/rules/baseline-target.test.ts b/src/rules/baseline-target.test.ts new file mode 100644 index 0000000..8fc2833 --- /dev/null +++ b/src/rules/baseline-target.test.ts @@ -0,0 +1,136 @@ +import {RuleTester} from 'eslint'; +import css from '@eslint/css'; +import {RuleTester as TSRuleTester} from '@typescript-eslint/rule-tester'; +import {baselineTarget} from './baseline-target.js'; +import * as path from 'node:path'; +import {fileURLToPath} from 'node:url'; + +const rootDir = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../..' +); +const typedRuleTester = new TSRuleTester({ + languageOptions: { + parserOptions: { + projectService: { + allowDefaultProject: ['*.ts'], + defaultProject: './tsconfig.json' + }, + tsconfigRootDir: rootDir + } + } +}); +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module' + } +}); + +ruleTester.run('baseline-target', baselineTarget as never, { + valid: ['const x = a ?? b;', 'x = 1;'], + invalid: [ + { + code: 'new URLPattern();', + errors: [ + { + messageId: 'exceedsYear', + data: {feature: 'urlpattern', year: 2025, floor: 2023}, + line: 1, + column: 5, + endLine: 1, + endColumn: 15 + } + ] + }, + { + code: 'Temporal.Now;', + errors: [ + { + messageId: 'notBaseline', + data: {feature: 'temporal'}, + line: 1, + column: 1, + endLine: 1, + endColumn: 9 + } + ] + }, + { + code: 'const a = 1;\nnew URLPattern();\nnew URLPattern();', + errors: [ + {messageId: 'exceedsYear', line: 2, column: 5}, + {messageId: 'exceedsYear', line: 3, column: 5} + ] + } + ] +}); + +typedRuleTester.run('baseline-target (typed)', baselineTarget, { + valid: ['const x = [1, 2, 3].at(-1);'], + invalid: [ + { + code: 'const {promise} = Promise.withResolvers();', + errors: [ + { + messageId: 'exceedsYear', + data: {feature: 'promise-withresolvers', year: 2024, floor: 2023}, + line: 1, + column: 27, + endLine: 1, + endColumn: 40 + } + ] + } + ] +}); + +const cssRuleTester = new RuleTester({ + plugins: {css}, + language: 'css/css' +}); + +cssRuleTester.run('baseline-target (css)', baselineTarget as never, { + valid: [ + {code: 'a { color: red; }', filename: 'test.css'}, + {code: 'b { height: 100dvh; }', filename: 'test.css'} + ], + invalid: [ + { + code: 'a { text-wrap: balance; }', + filename: 'test.css', + errors: [ + { + messageId: 'exceedsYear', + data: {feature: 'text-wrap', year: 2024, floor: 2023}, + line: 1, + column: 5, + endLine: 1, + endColumn: 14 + }, + { + messageId: 'exceedsYear', + data: {feature: 'text-wrap-balance', year: 2024, floor: 2023}, + line: 1, + column: 16, + endLine: 1, + endColumn: 23 + } + ] + }, + { + code: '.a { anchor-name: --a; }', + filename: 'test.css', + errors: [ + { + messageId: 'notBaseline', + data: {feature: 'anchor-positioning'}, + line: 1, + column: 6, + endLine: 1, + endColumn: 17 + } + ] + } + ] +}); diff --git a/src/rules/baseline-target.ts b/src/rules/baseline-target.ts new file mode 100644 index 0000000..2c00812 --- /dev/null +++ b/src/rules/baseline-target.ts @@ -0,0 +1,79 @@ +import {createRequire} from 'node:module'; +import path from 'node:path'; +import type {TSESLint} from '@typescript-eslint/utils'; +import { + baselineStatusOf, + detectFeaturesForSource, + yearOf +} from 'baseline-detector'; +import {tryGetTypedParserServices} from '../utils/typescript.js'; + +type MessageIds = 'notBaseline' | 'exceedsYear'; + +const BASELINE_YEAR = 2023; + +export const baselineTarget: TSESLint.RuleModule = { + meta: { + type: 'problem', + languages: ['js/js', 'css/css'], + docs: { + description: `Disallow features newer than Baseline ${BASELINE_YEAR}` + }, + schema: [], + messages: { + notBaseline: '`{{feature}}` is not Baseline yet', + exceedsYear: + '`{{feature}}` is Baseline {{year}}, above the {{floor}} floor' + } + }, + defaultOptions: [], + create(context) { + const services = tryGetTypedParserServices(context); + + const check = (): void => { + const features = detectFeaturesForSource(context.sourceCode.text, { + fileName: context.filename, + ...(services && { + typescriptContext: { + ts: createRequire(path.join(context.cwd, 'package.json'))( + 'typescript' + ), + program: services.program, + checker: services.program.getTypeChecker() + } + }) + }); + + for (const feature of features.get(context.filename) ?? []) { + const status = baselineStatusOf(feature.id); + const year = yearOf(feature.id); + + for (const [start, end] of feature.ranges) { + const loc = { + start: context.sourceCode.getLocFromIndex(start), + end: context.sourceCode.getLocFromIndex(end) + }; + + if (status === false) { + context.report({ + loc, + messageId: 'notBaseline', + data: {feature: feature.id} + }); + } else if (year !== null && year > BASELINE_YEAR) { + context.report({ + loc, + messageId: 'exceedsYear', + data: {feature: feature.id, year, floor: BASELINE_YEAR} + }); + } + } + } + }; + + return { + Program: check, + StyleSheet: check + }; + } +};