From 774951c562fdda9a150c50a157bd4bcb9b05d180 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen <4123478+tido64@users.noreply.github.com> Date: Thu, 10 Apr 2025 11:19:02 +0200 Subject: [PATCH] feat(yarn-plugin-eslint): Yarn plugin with integrated ESLint command --- .changeset/spotty-cars-call.md | 5 + .../eslint-npm-9.17.0-75805166d6.patch | 15 ++ incubator/yarn-plugin-eslint/README.md | 29 ++++ incubator/yarn-plugin-eslint/package.json | 47 ++++++ incubator/yarn-plugin-eslint/src/index.ts | 131 +++++++++++++++ incubator/yarn-plugin-eslint/tsconfig.json | 8 + package.json | 1 + packages/eslint-plugin/package.json | 1 + .../eslint-plugin/src/rules/no-export-all.js | 4 +- test-repos/rnx-kit-workspaces.json | 1 + yarn.lock | 156 +++++++++++++++++- 11 files changed, 392 insertions(+), 6 deletions(-) create mode 100644 .changeset/spotty-cars-call.md create mode 100644 .yarn/patches/eslint-npm-9.17.0-75805166d6.patch create mode 100644 incubator/yarn-plugin-eslint/README.md create mode 100644 incubator/yarn-plugin-eslint/package.json create mode 100644 incubator/yarn-plugin-eslint/src/index.ts create mode 100644 incubator/yarn-plugin-eslint/tsconfig.json diff --git a/.changeset/spotty-cars-call.md b/.changeset/spotty-cars-call.md new file mode 100644 index 0000000000..9cd517056c --- /dev/null +++ b/.changeset/spotty-cars-call.md @@ -0,0 +1,5 @@ +--- +"@rnx-kit/yarn-plugin-eslint": minor +--- + +Introduce a Yarn plugin with integrated ESLint and opinionated defaults diff --git a/.yarn/patches/eslint-npm-9.17.0-75805166d6.patch b/.yarn/patches/eslint-npm-9.17.0-75805166d6.patch new file mode 100644 index 0000000000..e14ef17480 --- /dev/null +++ b/.yarn/patches/eslint-npm-9.17.0-75805166d6.patch @@ -0,0 +1,15 @@ +diff --git a/lib/config/config-loader.js b/lib/config/config-loader.js +index 845cd0c92861353481d3369bb09e034e90f269ea..2eaaaade1e6b4949f1e38cee1c884f70b491b2d2 100644 +--- a/lib/config/config-loader.js ++++ b/lib/config/config-loader.js +@@ -158,7 +158,9 @@ async function loadConfigFile(filePath, allowTS) { + * the require cache only if the file has been changed. + */ + if (importedConfigFileModificationTime.get(filePath) !== mtime) { +- delete require.cache[filePath]; ++ if (require.cache) { ++ delete require.cache[filePath]; ++ } + } + + const isTS = isFileTS(filePath); diff --git a/incubator/yarn-plugin-eslint/README.md b/incubator/yarn-plugin-eslint/README.md new file mode 100644 index 0000000000..99faa37fae --- /dev/null +++ b/incubator/yarn-plugin-eslint/README.md @@ -0,0 +1,29 @@ +# @rnx-kit/yarn-plugin-eslint + +[![Build](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml/badge.svg)](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml) +[![npm version](https://img.shields.io/npm/v/@rnx-kit/yarn-plugin-eslint)](https://www.npmjs.com/package/@rnx-kit/yarn-plugin-eslint) + +🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧 + +### THIS TOOL IS EXPERIMENTAL — USE WITH CAUTION + +🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧 + +This is a Yarn plugin that integrates ESLint and opinionated (but overridable) +defaults, allowing you to run ESLint without having to install/configure it +separately in every workspace. + +## Installation + +The plugin needs to be installed via `yarn plugin install` command. This needs +to reference the produced bundle out of the `lib` folder. + +```sh +yarn plugin import ./path/to/@rnx-kit/yarn-plugin-eslint/lib/index.js +``` + +## Usage + +```sh +yarn rnx-lint +``` diff --git a/incubator/yarn-plugin-eslint/package.json b/incubator/yarn-plugin-eslint/package.json new file mode 100644 index 0000000000..1f40ef1230 --- /dev/null +++ b/incubator/yarn-plugin-eslint/package.json @@ -0,0 +1,47 @@ +{ + "name": "@rnx-kit/yarn-plugin-eslint", + "version": "0.0.1", + "description": "EXPERIMENTAL - USE WITH CAUTION - yarn-plugin-eslint", + "homepage": "https://github.com/microsoft/rnx-kit/tree/main/incubator/yarn-plugin-eslint#readme", + "license": "MIT", + "author": { + "name": "Microsoft Open Source", + "email": "microsoftopensource@users.noreply.github.com" + }, + "files": [ + "lib/index.js" + ], + "main": "lib/index.js", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/rnx-kit", + "directory": "incubator/yarn-plugin-eslint" + }, + "engines": { + "node": ">=18.19" + }, + "scripts": { + "build": "rnx-kit-scripts build", + "bundle": "rnx-kit-scripts bundle --platform yarn --minify", + "format": "rnx-kit-scripts format" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + }, + "devDependencies": { + "@eslint/compat": "^1.2.8", + "@rnx-kit/eslint-plugin": "*", + "@rnx-kit/scripts": "*", + "@rnx-kit/tsconfig": "*", + "@yarnpkg/cli": "^4.6.0", + "@yarnpkg/core": "^4.0.0", + "clipanion": "^4.0.0-rc.4", + "eslint-formatter-pretty": "^6.0.1" + }, + "experimental": true +} diff --git a/incubator/yarn-plugin-eslint/src/index.ts b/incubator/yarn-plugin-eslint/src/index.ts new file mode 100644 index 0000000000..6045cfdeaa --- /dev/null +++ b/incubator/yarn-plugin-eslint/src/index.ts @@ -0,0 +1,131 @@ +import { includeIgnoreFile } from "@eslint/compat"; +import { BaseCommand } from "@yarnpkg/cli"; +import { Configuration, Project } from "@yarnpkg/core"; +import { Option } from "clipanion"; +import type { Linter } from "eslint"; +import { ESLint } from "eslint"; +import { spawnSync } from "node:child_process"; +import * as fs from "node:fs"; +import * as path from "node:path"; + +type LinterConfigs = Linter.Config[]; + +export class Lint extends BaseCommand { + static override paths = [["rnx-lint"]]; + + fix = Option.Boolean("--fix", false, { + description: "Automatically fix problems", + }); + + patterns = Option.Rest({ name: "file.js" }); + + async execute(): Promise { + const cwd = this.context.cwd; + const configuration = await Configuration.find(cwd, this.context.plugins); + const { project } = await Project.find(configuration, cwd); + + const [overrideConfigFile, overrideConfig] = await this.loadConfig(cwd); + + const eslint = new ESLint({ + cwd, + ignorePatterns: this.ignorePatterns(project), + warnIgnored: false, + overrideConfigFile, + overrideConfig, + fix: this.fix, + }); + + const results = await eslint.lintFiles(this.filePatterns()); + await ESLint.outputFixes(results); + + const formatter = await this.loadFormatter(); + const output = formatter.format(results); + + if (output) { + if (ESLint.getErrorResults(results).length > 0) { + console.error(output); + return 1; + } + + console.log(output); + } + + return 0; + } + + private filePatterns() { + if (this.patterns.length > 0) { + return this.patterns; + } + + const args = [ + "ls-files", + "*.cjs", + "*.js", + "*.jsx", + "*.mjs", + "*.ts", + "*.tsx", + ]; + const { stdout } = spawnSync("git", args); + return stdout.toString().trim().split("\n"); + } + + private ignorePatterns(project: Project): string[] { + const patterns: string[] = []; + + const locations = [project.cwd, this.context.cwd]; + for (const location of locations) { + const gitignore = path.join(location, ".gitignore"); + if (fs.existsSync(gitignore)) { + const { ignores } = includeIgnoreFile(gitignore); + if (ignores) { + patterns.push(...ignores); + } + } + } + + return patterns; + } + + private async loadConfig( + cwd: string + ): Promise<[boolean | string, LinterConfigs | undefined]> { + const eslintConfigPath = path.join(cwd, "eslint.config.js"); + const overrideConfigFile = fs.existsSync(eslintConfigPath) + ? eslintConfigPath + : true; + + if (overrideConfigFile !== true) { + return [eslintConfigPath, undefined]; + } + + const rnx = await import("@rnx-kit/eslint-plugin"); + const config = [ + ...rnx.configs.strict, + ...rnx.configs.stylistic, + { + rules: { + "@typescript-eslint/consistent-type-definitions": ["error", "type"], + }, + }, + ]; + + return [true, config]; + } + + /** + * ESLint will try to dynamically import a formatter and fail. We bundle our + * own formatter to bypass this. + */ + private async loadFormatter() { + const { default: format } = await import("eslint-formatter-pretty"); + return { format }; + } +} + +// eslint-disable-next-line no-restricted-exports +export default { + name: "@rnx-kit/yarn-plugin-eslint", + commands: [Lint], +}; diff --git a/incubator/yarn-plugin-eslint/tsconfig.json b/incubator/yarn-plugin-eslint/tsconfig.json new file mode 100644 index 0000000000..d2f19bc815 --- /dev/null +++ b/incubator/yarn-plugin-eslint/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@rnx-kit/tsconfig/tsconfig.json", + "compilerOptions": { + "target": "ES2021", + "noEmit": true + }, + "include": ["src"] +} diff --git a/package.json b/package.json index caca1429a3..7c3db18f19 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@react-native-community/cli-types": "^15.0.0", "@rnx-kit/react-native-host": "workspace:*", "@vue/compiler-sfc": "link:./incubator/ignore", + "eslint": "patch:eslint@npm%3A9.17.0#~/.yarn/patches/eslint-npm-9.17.0-75805166d6.patch", "react-native-macos/@react-native/assets-registry": "^0.76.0", "react-native-macos/@react-native/codegen": "^0.76.0", "react-native-macos/@react-native/community-cli-plugin": "^0.76.0", diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index ddf06a5f9d..a0ea5c024b 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -53,6 +53,7 @@ "eslint": ">=8.57.0" }, "devDependencies": { + "@eslint/js": "^9.0.0", "@microsoft/eslint-plugin-sdl": "^1.0.0", "@rnx-kit/scripts": "*", "@rnx-kit/tsconfig": "*", diff --git a/packages/eslint-plugin/src/rules/no-export-all.js b/packages/eslint-plugin/src/rules/no-export-all.js index 386311b44d..0b59f91b87 100644 --- a/packages/eslint-plugin/src/rules/no-export-all.js +++ b/packages/eslint-plugin/src/rules/no-export-all.js @@ -3,7 +3,7 @@ /** * @typedef {import("@typescript-eslint/types/dist/index").TSESTree.Node} Node - * @typedef {import("eslint").Linter.FlatConfig} FlatConfig + * @typedef {import("eslint").Linter.Config} Config * @typedef {import("eslint").Rule.RuleContext} ESLintRuleContext * @typedef {import("eslint").Rule.ReportFixer} ESLintReportFixer * @typedef {{ exports: string[], types: string[] }} NamedExports @@ -16,7 +16,7 @@ * maxDepth: number; * }; * filename: string; - * languageOptions: FlatConfig["languageOptions"]; + * languageOptions: Config["languageOptions"]; * parserOptions: ESLintRuleContext["parserOptions"]; * parserPath: ESLintRuleContext["parserPath"]; * sourceCode: ESLintRuleContext["sourceCode"]; diff --git a/test-repos/rnx-kit-workspaces.json b/test-repos/rnx-kit-workspaces.json index 7b83282a55..3a22405da3 100644 --- a/test-repos/rnx-kit-workspaces.json +++ b/test-repos/rnx-kit-workspaces.json @@ -48,6 +48,7 @@ "@rnx-kit/tsconfig": "packages/tsconfig", "@rnx-kit/typescript-service": "packages/typescript-service", "@rnx-kit/yarn-plugin-dynamic-extensions": "incubator/yarn-plugin-dynamic-extensions", + "@rnx-kit/yarn-plugin-eslint": "incubator/yarn-plugin-eslint", "@rnx-kit/yarn-plugin-external-workspaces": "incubator/yarn-plugin-external-workspaces" } } diff --git a/yarn.lock b/yarn.lock index 84c6281824..8dd86d3af6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2232,6 +2232,18 @@ __metadata: languageName: node linkType: hard +"@eslint/compat@npm:^1.2.8": + version: 1.2.8 + resolution: "@eslint/compat@npm:1.2.8" + peerDependencies: + eslint: ^9.10.0 + peerDependenciesMeta: + eslint: + optional: true + checksum: 10c0/1e004c6917220ff1731fdc562ada9ddcbcecc6f3ba2e4b0433fb6d8eddf2a443e009f1f9796b78128b78a0a588c723b78021319055ac6e5dda55c0ace346496b + languageName: node + linkType: hard + "@eslint/config-array@npm:^0.19.0": version: 0.19.1 resolution: "@eslint/config-array@npm:0.19.1" @@ -4009,10 +4021,11 @@ __metadata: languageName: unknown linkType: soft -"@rnx-kit/eslint-plugin@workspace:*, @rnx-kit/eslint-plugin@workspace:packages/eslint-plugin": +"@rnx-kit/eslint-plugin@npm:*, @rnx-kit/eslint-plugin@workspace:*, @rnx-kit/eslint-plugin@workspace:packages/eslint-plugin": version: 0.0.0-use.local resolution: "@rnx-kit/eslint-plugin@workspace:packages/eslint-plugin" dependencies: + "@eslint/js": "npm:^9.0.0" "@microsoft/eslint-plugin-sdl": "npm:^1.0.0" "@react-native/eslint-plugin": "npm:^0.76.0" "@rnx-kit/scripts": "npm:*" @@ -4686,6 +4699,26 @@ __metadata: languageName: unknown linkType: soft +"@rnx-kit/yarn-plugin-eslint@workspace:incubator/yarn-plugin-eslint": + version: 0.0.0-use.local + resolution: "@rnx-kit/yarn-plugin-eslint@workspace:incubator/yarn-plugin-eslint" + dependencies: + "@eslint/compat": "npm:^1.2.8" + "@rnx-kit/eslint-plugin": "npm:*" + "@rnx-kit/scripts": "npm:*" + "@rnx-kit/tsconfig": "npm:*" + "@yarnpkg/cli": "npm:^4.6.0" + "@yarnpkg/core": "npm:^4.0.0" + clipanion: "npm:^4.0.0-rc.4" + eslint-formatter-pretty: "npm:^6.0.1" + peerDependencies: + jiti: "*" + peerDependenciesMeta: + jiti: + optional: true + languageName: unknown + linkType: soft + "@rnx-kit/yarn-plugin-external-workspaces@workspace:incubator/yarn-plugin-external-workspaces": version: 0.0.0-use.local resolution: "@rnx-kit/yarn-plugin-external-workspaces@workspace:incubator/yarn-plugin-external-workspaces" @@ -4931,6 +4964,16 @@ __metadata: languageName: node linkType: hard +"@types/eslint@npm:^8.44.6": + version: 8.56.12 + resolution: "@types/eslint@npm:8.56.12" + dependencies: + "@types/estree": "npm:*" + "@types/json-schema": "npm:*" + checksum: 10c0/e4ca426abe9d55f82b69a3250bec78b6d340ad1e567f91c97ecc59d3b2d6a1d8494955ac62ad0ea14b97519db580611c02be8277cbea370bdfb0f96aa2910504 + languageName: node + linkType: hard + "@types/eslint__js@npm:^8.0.0": version: 8.42.3 resolution: "@types/eslint__js@npm:8.42.3" @@ -6087,6 +6130,13 @@ __metadata: languageName: node linkType: hard +"ansi-escapes@npm:^6.2.0": + version: 6.2.1 + resolution: "ansi-escapes@npm:6.2.1" + checksum: 10c0/a2c6f58b044be5f69662ee17073229b492daa2425a7fd99a665db6c22eab6e4ab42752807def7281c1c7acfed48f87f2362dda892f08c2c437f1b39c6b033103 + languageName: node + linkType: hard + "ansi-fragments@npm:^0.2.1": version: 0.2.1 resolution: "ansi-fragments@npm:0.2.1" @@ -8164,6 +8214,22 @@ __metadata: languageName: node linkType: hard +"eslint-formatter-pretty@npm:^6.0.1": + version: 6.0.1 + resolution: "eslint-formatter-pretty@npm:6.0.1" + dependencies: + "@types/eslint": "npm:^8.44.6" + ansi-escapes: "npm:^6.2.0" + chalk: "npm:^5.3.0" + eslint-rule-docs: "npm:^1.1.235" + log-symbols: "npm:^6.0.0" + plur: "npm:^5.1.0" + string-width: "npm:^7.0.0" + supports-hyperlinks: "npm:^3.0.0" + checksum: 10c0/8fd60a810c17f02f95bf1e0a112195a43d95efd1eb4e828114d1bcc30bd185e48946ab64ae55396e4e272949571e7baaa74b8583fe4188c3077fd0eb9dd6c1ed + languageName: node + linkType: hard + "eslint-plugin-es-x@npm:^7.5.0": version: 7.8.0 resolution: "eslint-plugin-es-x@npm:7.8.0" @@ -8241,6 +8307,13 @@ __metadata: languageName: node linkType: hard +"eslint-rule-docs@npm:^1.1.235": + version: 1.1.235 + resolution: "eslint-rule-docs@npm:1.1.235" + checksum: 10c0/76a735c1e13a511ddff1017d5913b2526643827c8fdc86a23467f680b8dcbdfd07806cb092c82dd8d0e99789f23c8a38b9d2b838cd1cd62cc1932612ed606b8e + languageName: node + linkType: hard + "eslint-scope@npm:^8.2.0": version: 8.2.0 resolution: "eslint-scope@npm:8.2.0" @@ -8265,7 +8338,7 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^9.0.0": +"eslint@npm:9.17.0": version: 9.17.0 resolution: "eslint@npm:9.17.0" dependencies: @@ -8314,6 +8387,55 @@ __metadata: languageName: node linkType: hard +"eslint@patch:eslint@npm%3A9.17.0#~/.yarn/patches/eslint-npm-9.17.0-75805166d6.patch": + version: 9.17.0 + resolution: "eslint@patch:eslint@npm%3A9.17.0#~/.yarn/patches/eslint-npm-9.17.0-75805166d6.patch::version=9.17.0&hash=a8d2b7" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.2.0" + "@eslint-community/regexpp": "npm:^4.12.1" + "@eslint/config-array": "npm:^0.19.0" + "@eslint/core": "npm:^0.9.0" + "@eslint/eslintrc": "npm:^3.2.0" + "@eslint/js": "npm:9.17.0" + "@eslint/plugin-kit": "npm:^0.2.3" + "@humanfs/node": "npm:^0.16.6" + "@humanwhocodes/module-importer": "npm:^1.0.1" + "@humanwhocodes/retry": "npm:^0.4.1" + "@types/estree": "npm:^1.0.6" + "@types/json-schema": "npm:^7.0.15" + ajv: "npm:^6.12.4" + chalk: "npm:^4.0.0" + cross-spawn: "npm:^7.0.6" + debug: "npm:^4.3.2" + escape-string-regexp: "npm:^4.0.0" + eslint-scope: "npm:^8.2.0" + eslint-visitor-keys: "npm:^4.2.0" + espree: "npm:^10.3.0" + esquery: "npm:^1.5.0" + esutils: "npm:^2.0.2" + fast-deep-equal: "npm:^3.1.3" + file-entry-cache: "npm:^8.0.0" + find-up: "npm:^5.0.0" + glob-parent: "npm:^6.0.2" + ignore: "npm:^5.2.0" + imurmurhash: "npm:^0.1.4" + is-glob: "npm:^4.0.0" + json-stable-stringify-without-jsonify: "npm:^1.0.1" + lodash.merge: "npm:^4.6.2" + minimatch: "npm:^3.1.2" + natural-compare: "npm:^1.4.0" + optionator: "npm:^0.9.3" + peerDependencies: + jiti: "*" + peerDependenciesMeta: + jiti: + optional: true + bin: + eslint: bin/eslint.js + checksum: 10c0/b3cba7259d8a63998afc044395095b886f5eb192347161d207ed002c43b7ed75d41850fb7166f8fceb4356fbb7d37acdc31e0260ea6d8a7225a68dfc0626479b + languageName: node + linkType: hard + "espree@npm:^10.0.1, espree@npm:^10.3.0": version: 10.3.0 resolution: "espree@npm:10.3.0" @@ -9619,6 +9741,13 @@ __metadata: languageName: node linkType: hard +"irregular-plurals@npm:^3.3.0": + version: 3.5.0 + resolution: "irregular-plurals@npm:3.5.0" + checksum: 10c0/7c033bbe7325e5a6e0a26949cc6863b6ce273403d4cd5b93bd99b33fecb6605b0884097c4259c23ed0c52c2133bf7d1cdcdd7a0630e8c325161fe269b3447918 + languageName: node + linkType: hard + "is-array-buffer@npm:^3.0.4, is-array-buffer@npm:^3.0.5": version: 3.0.5 resolution: "is-array-buffer@npm:3.0.5" @@ -12792,6 +12921,15 @@ __metadata: languageName: node linkType: hard +"plur@npm:^5.1.0": + version: 5.1.0 + resolution: "plur@npm:5.1.0" + dependencies: + irregular-plurals: "npm:^3.3.0" + checksum: 10c0/26bb622b8545fcfd47bbf56fbcca66c08693708a232e403fa3589e00003c56c14231ac57c7588ca5db83ef4be1f61383402c4ea954000768f779f8aef6eb6da8 + languageName: node + linkType: hard + "pngjs@npm:^5.0.0": version: 5.0.0 resolution: "pngjs@npm:5.0.0" @@ -14373,7 +14511,7 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^7.2.0": +"string-width@npm:^7.0.0, string-width@npm:^7.2.0": version: 7.2.0 resolution: "string-width@npm:7.2.0" dependencies: @@ -14600,7 +14738,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^7.1.0": +"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" dependencies: @@ -14618,6 +14756,16 @@ __metadata: languageName: node linkType: hard +"supports-hyperlinks@npm:^3.0.0": + version: 3.2.0 + resolution: "supports-hyperlinks@npm:3.2.0" + dependencies: + has-flag: "npm:^4.0.0" + supports-color: "npm:^7.0.0" + checksum: 10c0/bca527f38d4c45bc95d6a24225944675746c515ddb91e2456d00ae0b5c537658e9dd8155b996b191941b0c19036195a098251304b9082bbe00cd1781f3cd838e + languageName: node + linkType: hard + "supports-preserve-symlinks-flag@npm:^1.0.0": version: 1.0.0 resolution: "supports-preserve-symlinks-flag@npm:1.0.0"