Skip to content

Commit ac08e1d

Browse files
committed
feat(yarn-plugin-eslint): Yarn plugin with integrated ESLint command
1 parent de8149c commit ac08e1d

File tree

11 files changed

+394
-6
lines changed

11 files changed

+394
-6
lines changed

.changeset/spotty-cars-call.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@rnx-kit/yarn-plugin-eslint": minor
3+
---
4+
5+
Introduce a Yarn plugin with integrated ESLint and opinionated defaults
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
diff --git a/lib/config/config-loader.js b/lib/config/config-loader.js
2+
index 845cd0c92861353481d3369bb09e034e90f269ea..2eaaaade1e6b4949f1e38cee1c884f70b491b2d2 100644
3+
--- a/lib/config/config-loader.js
4+
+++ b/lib/config/config-loader.js
5+
@@ -158,7 +158,9 @@ async function loadConfigFile(filePath, allowTS) {
6+
* the require cache only if the file has been changed.
7+
*/
8+
if (importedConfigFileModificationTime.get(filePath) !== mtime) {
9+
- delete require.cache[filePath];
10+
+ if (require.cache) {
11+
+ delete require.cache[filePath];
12+
+ }
13+
}
14+
15+
const isTS = isFileTS(filePath);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# @rnx-kit/yarn-plugin-eslint
2+
3+
[![Build](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml/badge.svg)](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml)
4+
[![npm version](https://img.shields.io/npm/v/@rnx-kit/yarn-plugin-eslint)](https://www.npmjs.com/package/@rnx-kit/yarn-plugin-eslint)
5+
6+
🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧
7+
8+
### THIS TOOL IS EXPERIMENTAL — USE WITH CAUTION
9+
10+
🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧
11+
12+
This is a Yarn plugin that integrates ESLint and opinionated (but overridable)
13+
defaults, allowing you to run ESLint without having to install/configure it
14+
separately in every workspace.
15+
16+
## Installation
17+
18+
The plugin needs to be installed via `yarn plugin install` command. This needs
19+
to reference the produced bundle out of the `lib` folder.
20+
21+
```sh
22+
yarn plugin import ./path/to/@rnx-kit/yarn-plugin-eslint/lib/index.js
23+
```
24+
25+
## Usage
26+
27+
```sh
28+
yarn rnx-lint
29+
```
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"name": "@rnx-kit/yarn-plugin-eslint",
3+
"version": "0.0.1",
4+
"description": "EXPERIMENTAL - USE WITH CAUTION - yarn-plugin-eslint",
5+
"homepage": "https://github.com/microsoft/rnx-kit/tree/main/incubator/yarn-plugin-eslint#readme",
6+
"license": "MIT",
7+
"author": {
8+
"name": "Microsoft Open Source",
9+
"email": "[email protected]"
10+
},
11+
"files": [
12+
"lib/index.js"
13+
],
14+
"main": "lib/index.js",
15+
"repository": {
16+
"type": "git",
17+
"url": "https://github.com/microsoft/rnx-kit",
18+
"directory": "incubator/yarn-plugin-eslint"
19+
},
20+
"engines": {
21+
"node": ">=18.19"
22+
},
23+
"scripts": {
24+
"build": "rnx-kit-scripts build",
25+
"bundle": "rnx-kit-scripts bundle --platform yarn --minify",
26+
"format": "rnx-kit-scripts format"
27+
},
28+
"peerDependencies": {
29+
"jiti": "*"
30+
},
31+
"peerDependenciesMeta": {
32+
"jiti": {
33+
"optional": true
34+
}
35+
},
36+
"devDependencies": {
37+
"@eslint/compat": "^1.2.8",
38+
"@rnx-kit/eslint-plugin": "*",
39+
"@rnx-kit/scripts": "*",
40+
"@rnx-kit/tsconfig": "*",
41+
"@yarnpkg/cli": "^4.6.0",
42+
"@yarnpkg/core": "^4.0.0",
43+
"clipanion": "^4.0.0-rc.4",
44+
"eslint-formatter-pretty": "^6.0.1"
45+
},
46+
"experimental": true
47+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { includeIgnoreFile } from "@eslint/compat";
2+
import { BaseCommand } from "@yarnpkg/cli";
3+
import { Configuration, Project } from "@yarnpkg/core";
4+
import { Option } from "clipanion";
5+
import type { Linter } from "eslint";
6+
import { ESLint } from "eslint";
7+
import { spawnSync } from "node:child_process";
8+
import * as fs from "node:fs";
9+
import * as path from "node:path";
10+
11+
type LinterConfigs = Linter.Config<Linter.RulesRecord>[];
12+
13+
export class Lint extends BaseCommand {
14+
static override paths = [["rnx-lint"]];
15+
16+
fix = Option.Boolean("--fix", false, {
17+
description: "Automatically fix problems",
18+
});
19+
20+
patterns = Option.Rest({ name: "file.js" });
21+
22+
async execute(): Promise<number | void> {
23+
const cwd = this.context.cwd;
24+
const configuration = await Configuration.find(cwd, this.context.plugins);
25+
const { project } = await Project.find(configuration, cwd);
26+
27+
const [overrideConfigFile, overrideConfig] = await this.loadConfig(cwd);
28+
29+
const eslint = new ESLint({
30+
cwd,
31+
ignorePatterns: this.ignorePatterns(project),
32+
warnIgnored: false,
33+
overrideConfigFile,
34+
overrideConfig,
35+
fix: this.fix,
36+
});
37+
38+
const results = await eslint.lintFiles(this.filePatterns());
39+
await ESLint.outputFixes(results);
40+
41+
const formatter = await this.loadFormatter();
42+
const output = formatter.format(results);
43+
44+
if (output) {
45+
if (ESLint.getErrorResults(results).length > 0) {
46+
console.error(output);
47+
return 1;
48+
}
49+
50+
console.log(output);
51+
}
52+
53+
return 0;
54+
}
55+
56+
private filePatterns() {
57+
if (this.patterns.length > 0) {
58+
return this.patterns;
59+
}
60+
61+
const args = [
62+
"ls-files",
63+
"*.cjs",
64+
"*.js",
65+
"*.jsx",
66+
"*.mjs",
67+
"*.ts",
68+
"*.tsx",
69+
];
70+
const { stdout } = spawnSync("git", args);
71+
return stdout.toString().trim().split("\n");
72+
}
73+
74+
private ignorePatterns(project: Project): string[] {
75+
const patterns: string[] = [];
76+
77+
const locations = [project.cwd, this.context.cwd];
78+
for (const location of locations) {
79+
const gitignore = path.join(location, ".gitignore");
80+
if (fs.existsSync(gitignore)) {
81+
const { ignores } = includeIgnoreFile(gitignore);
82+
if (ignores) {
83+
patterns.push(...ignores);
84+
}
85+
}
86+
}
87+
88+
return patterns;
89+
}
90+
91+
private async loadConfig(
92+
cwd: string
93+
): Promise<[boolean | string, LinterConfigs | undefined]> {
94+
const eslintConfigPath = path.join(cwd, "eslint.config.js");
95+
const overrideConfigFile = fs.existsSync(eslintConfigPath)
96+
? eslintConfigPath
97+
: true;
98+
99+
if (overrideConfigFile !== true) {
100+
return [eslintConfigPath, undefined];
101+
}
102+
103+
const rnx = await import("@rnx-kit/eslint-plugin");
104+
const config = [
105+
...rnx.configs.strict,
106+
...rnx.configs.stylistic,
107+
{
108+
rules: {
109+
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
110+
},
111+
},
112+
];
113+
114+
return [true, config];
115+
}
116+
117+
/**
118+
* ESLint will try to dynamically import a formatter and fail. We bundle our
119+
* own formatter to bypass this.
120+
*/
121+
private async loadFormatter() {
122+
const { default: format } = await import("eslint-formatter-pretty");
123+
return { format };
124+
}
125+
}
126+
127+
// eslint-disable-next-line no-restricted-exports
128+
export default {
129+
name: "@rnx-kit/yarn-plugin-eslint",
130+
commands: [Lint],
131+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "@rnx-kit/tsconfig/tsconfig.json",
3+
"compilerOptions": {
4+
"target": "ES2021",
5+
"noEmit": true
6+
},
7+
"include": ["src"]
8+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"@react-native-community/cli-types": "^15.0.0",
6363
"@rnx-kit/react-native-host": "workspace:*",
6464
"@vue/compiler-sfc": "link:./incubator/ignore",
65+
"eslint": "patch:eslint@npm%3A9.17.0#~/.yarn/patches/eslint-npm-9.17.0-75805166d6.patch",
6566
"react-native-macos/@react-native/assets-registry": "^0.76.0",
6667
"react-native-macos/@react-native/codegen": "^0.76.0",
6768
"react-native-macos/@react-native/community-cli-plugin": "^0.76.0",

packages/eslint-plugin/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"eslint": ">=8.57.0"
5454
},
5555
"devDependencies": {
56+
"@eslint/js": "^9.0.0",
5657
"@microsoft/eslint-plugin-sdl": "^1.0.0",
5758
"@rnx-kit/scripts": "*",
5859
"@rnx-kit/tsconfig": "*",

packages/eslint-plugin/src/rules/no-export-all.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
/**
55
* @typedef {import("@typescript-eslint/types/dist/index").TSESTree.Node} Node
6-
* @typedef {import("eslint").Linter.FlatConfig} FlatConfig
6+
* @typedef {import("eslint").Linter.Config} Config
77
* @typedef {import("eslint").Rule.RuleContext} ESLintRuleContext
88
* @typedef {import("eslint").Rule.ReportFixer} ESLintReportFixer
99
* @typedef {{ exports: string[], types: string[] }} NamedExports
@@ -16,7 +16,7 @@
1616
* maxDepth: number;
1717
* };
1818
* filename: string;
19-
* languageOptions: FlatConfig["languageOptions"];
19+
* languageOptions: Config["languageOptions"];
2020
* parserOptions: ESLintRuleContext["parserOptions"];
2121
* parserPath: ESLintRuleContext["parserPath"];
2222
* sourceCode: ESLintRuleContext["sourceCode"];

test-repos/rnx-kit-workspaces.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"@rnx-kit/tsconfig": "packages/tsconfig",
4949
"@rnx-kit/typescript-service": "packages/typescript-service",
5050
"@rnx-kit/yarn-plugin-dynamic-extensions": "incubator/yarn-plugin-dynamic-extensions",
51+
"@rnx-kit/yarn-plugin-eslint": "incubator/yarn-plugin-eslint",
5152
"@rnx-kit/yarn-plugin-external-workspaces": "incubator/yarn-plugin-external-workspaces"
5253
}
5354
}

0 commit comments

Comments
 (0)