diff --git a/eslint/eslint-bulk/README.md b/eslint/eslint-bulk/README.md index cb4a0a12250..acaec5dcb8c 100755 --- a/eslint/eslint-bulk/README.md +++ b/eslint/eslint-bulk/README.md @@ -1,29 +1,37 @@ # @rushstack/eslint-bulk -This is a companion package for @rushstack/eslint-patch that should be installed globally as follows: -```bash -npm i -g @rushstack/eslint-bulk -``` +This package provides the command-line interface (CLI) for the **ESLint bulk suppressions** +feature from +[`@rushstack/eslint-patch`](https://www.npmjs.com/package/@rushstack/eslint-patch). -The **eslint-bulk** package is a set of command line tools to use with the ESLint bulk suppressions patch. -eslint-bulk commands must be run in the same current working directory containing your package's pertaining -.eslintrc.js or .eslintrc.cjs file. +> 👉 See the `@rushstack/eslint-patch` [documentation](https://www.npmjs.com/package/@rushstack/eslint-patch) +> for details. -## eslint-bulk suppress +### Typical workflow -Use this command to automatically generate bulk suppressions for the given files and given rules. -Supply the paths as the main argument. The paths argument is a glob pattern that follows the same -rules as the "files" argument in the "eslint" command. +1. Checkout your `main` branch, which is in a clean state where ESLint reports no violations. +2. Update your configuration to enable the latest lint rules; ESLint now reports thousands of legacy violations. +3. Run `eslint-bulk suppress --all ./src` to update **.eslint-bulk-suppressions.json.** +4. ESLint now no longer reports violations, so commit the results to Git and merge your pull request. +5. Over time, engineers may improve some of the suppressed code, in which case the associated suppressions are no longer needed. +6. Run `eslint-bulk prune` periodically to find and remove unnecessary suppressions from **.eslint-bulk-suppressions.json**, ensuring that new violations will now get caught in those scopes. + +### "eslint-bulk suppress" command ```bash eslint-bulk suppress --rule NAME1 [--rule NAME2...] PATH1 [PATH2...] eslint-bulk suppress --all PATH1 [PATH2...] ``` -## eslint-bulk prune +Use this command to automatically generate bulk suppressions for the specified lint rules and file paths. +The path argument is a [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) with the same syntax +as path arguments for the `eslint` command. + -Use this command to automatically delete all unused suppression entries in all .eslint-bulk-suppressions.json -files under the current working directory. +### "eslint-bulk prune" command + +Use this command to automatically delete all unnecessary suppression entries in all +**.eslint-bulk-suppressions.json** files under the current working directory. ```bash eslint-bulk prune @@ -34,4 +42,7 @@ eslint-bulk prune - [CHANGELOG.md](https://github.com/microsoft/rushstack/blob/main/eslint/eslint-bulk/CHANGELOG.md) - Find out what's new in the latest version +- [`@rushstack/eslint-patch`](https://www.npmjs.com/package/@rushstack/eslint-patch) required companion package + + `@rushstack/eslint-bulk` is part of the [Rush Stack](https://rushstack.io/) family of projects. diff --git a/eslint/eslint-bulk/package.json b/eslint/eslint-bulk/package.json index 15f864747de..8cc2c9dfbec 100755 --- a/eslint/eslint-bulk/package.json +++ b/eslint/eslint-bulk/package.json @@ -1,7 +1,7 @@ { "name": "@rushstack/eslint-bulk", "version": "0.1.5", - "description": "A set of helper CLIs to use with the rushstack ESLint toolchain", + "description": "Roll out new ESLint rules in a large monorepo without cluttering up your code with \"eslint-ignore-next-line\"", "main": "index.js", "license": "MIT", "repository": { @@ -26,7 +26,9 @@ "retroactive", "disable", "ignore", - "suppression" + "suppression", + "monkey", + "patch" ], "devDependencies": { "@rushstack/heft": "workspace:*", diff --git a/eslint/eslint-patch/README.md b/eslint/eslint-patch/README.md index 231a5d5e509..b1556202c0f 100644 --- a/eslint/eslint-patch/README.md +++ b/eslint/eslint-patch/README.md @@ -1,160 +1,252 @@ # @rushstack/eslint-patch -A patch that improves how ESLint loads plugins when working in a monorepo with a reusable toolchain +Enhance your [ESLint](https://eslint.org/) with better support for large scale monorepos! -# modern-module-resolution +This is a runtime patch that enables new/experimental features for ESLint. It operates as a "monkey patch" +that gets loaded with **.eslintrc.js** and modifies the ESLint engine in memory. This approach works +with your existing ESLint version (no need to install a forked ESLint), and is fully interoperable with +companion tools such as the ESLint extensions for VS Code and WebStorm. -## What it does +This package provides several independently loadable features: -This patch is a workaround for a longstanding [ESLint feature request](https://github.com/eslint/eslint/issues/3458) -that would allow a shared ESLint config to bring along its own plugins, rather than imposing peer dependencies -on every consumer of the config. In a monorepo scenario, this enables your lint setup to be consolidated in a -single NPM package. Doing so greatly reduces the copy+pasting and version management for all the other projects -that use your standard lint rule set, but don't want to be bothered with the details. +- `eslint-bulk-suppressions` enables you to roll out new lint rules in your monorepo without having to + clutter up source files with thousands of machine-generated `// eslint-ignore-next-line` directives. + Instead, the "bulk suppressions" for legacy violations are managed in a separate file called + **.eslint-bulk-suppressions.json**. -ESLint provides partial solutions such as the `--resolve-plugins-relative-to` CLI option, however they are -awkward to use. For example, the VS Code extension for ESLint must be manually configured with this CLI option. -If some developers use other editors such as WebStorm, a different manual configuration is needed. -Also, the `--resolve-plugins-relative-to` parameter does not support multiple paths, for example if a config package -builds upon another package that also provides plugins. See -[this discussion](https://github.com/eslint/eslint/issues/3458#issuecomment-516666620) -for additional technical background. +- `modern-module-resolution` allows an ESLint config package to provide plugin dependencies, avoiding the + problem where hundreds of projects in a monorepo need to copy+paste the same `"devDependencies"` in + every **package.json** file. + > NOTE: ESLint 8.21.0 has now introduced a new `ESLINT_USE_FLAT_CONFIG` mode that may reduce the need + for the `modern-module-resolution` patch. -## Why it's a patch +- `custom-config-package-names` enables [rig packages](https://heft.rushstack.io/pages/intro/rig_packages/) + to provide shareable configs for ESLint, by removing the requirement that `eslint-config` must appear in + the NPM package name. -ESLint's long awaited module resolver overhaul still has not materialized as of ESLint 8. As a stopgap, -we created a small **.eslintrc.js** patch that solves the problem adequately for most real world scenarios. -This patch was proposed as an ESLint feature with [PR 12460](https://github.com/eslint/eslint/pull/12460), however -the maintainers were not able to accept it unless it is reworked into a fully correct design. Such a requirement -would impose the same hurdles as the original GitHub issue; thus, it seems best to stay with the patch approach. +Contributions welcome! If you have more ideas for experimental ESLint enhancements that might benefit +large scale monorepos, consider adding them to this patch. -Since the patch is now in wide use, we've converted it into a proper NPM package to simplify maintenance. +# eslint-bulk-suppressions feature -## How to use it + -Add a `require()` call to the to top of the **.eslintrc.js** file for each project that depends on your shared -ESLint config, for example: +### What it does -**.eslintrc.js** -```ts -require("@rushstack/eslint-patch/modern-module-resolution"); +As your monorepo evolves and grows, there's an ongoing need to expand and improve lint rules. But whenever a +new rule is enabled, there may be hundreds or thousands of "legacy violations" in existing source files. +How to handle that? We could fix the old code, but that's often prohibitively expensive and may even cause +regressions. We could disable the rule for those projects or files, but we want new code to follow the rule. +An effective solution is to inject thousands of `// eslint-ignore-next-line` lines, but these "bulk suppressions" +have an unintended side effect: It normalizes the practice of suppressing lint rules. If people get used to +seeing `// eslint-ignore-next-line` everywhere, nobody will notice when humans suppress the rules for new code. +That would undermine the mission of establishing better code standards. -// Add your "extends" boilerplate here, for example: -module.exports = { - extends: ['@your-company/eslint-config'], - parserOptions: { tsconfigRootDir: __dirname } -}; -``` +The `eslint-bulk-suppressions` feature introduces a way to store machine-generated suppressions in a separate +file **.eslint-bulk-suppressions.json** which can even be protected using `CODEOWNERS` policies, since that file +will generally only change when new lint rules are introduced, or in occasional circumstances when existing files +are being moved or renamed. In this way `// eslint-ignore-next-line` remains a directive written by humans +and hopefully rarely needed. -With this change, the local project no longer needs any ESLint plugins in its **package.json** file. -Instead, the hypothetical `@your-company/eslint-config` NPM package would declare the plugins as its -own dependencies. -This patch works by modifying the ESLint engine so that its module resolver will load relative to the folder of -the referencing config file, rather than the project folder. The patch is compatible with ESLint 6, 7, and 8. -It also works with any editor extensions that load ESLint as a library. +### Why it's a patch -There is a second patch in this package that removes the restriction on eslint configuration package names. -Similarly to the first, this patch is applied by adding a `require()` call to the top of the **.eslintrc.js**, -for example: +As with `modern-module-resolution`, our hope is for this feature to eventually be incorporated as an official +feature of ESLint. Starting out as an unofficial patch allows faster iteration and community feedback. -**.eslintrc.js** -```ts -require("@rushstack/eslint-patch/modern-module-resolution"); -require("@rushstack/eslint-patch/custom-config-package-names"); // <-- Add this line -// Add your "extends" boilerplate here, for example: -module.exports = { - extends: [ - '@your-company/build-rig/profile/default/includes/eslint/node' // Notice the package name does not start with "eslint-config-" - ], - parserOptions: { tsconfigRootDir: __dirname } -}; -``` +### How to use it -For an even leaner setup, `@your-company/eslint-config` can provide the patches as its own dependency. See -[@rushstack/eslint-config](https://www.npmjs.com/package/@rushstack/eslint-config) for a real world example -and recommended approach. +1. Add `@rushstack/eslint-patch` as a dependency of your project: -# eslint-bulk-suppressions + ```bash + cd your-project + npm install --save-dev @rushstack/eslint-patch + ``` -A tool that allows bulk suppression of ESLint warnings/errors in a large, old codebase when introducing new ESLint rules. +2. Globally install the [`@rushstack/eslint-bulk`](https://www.npmjs.com/package/@rushstack/eslint-bulk) + command line interface (CLI) package. For example: -## What it does + ```bash + npm install --global @rushstack/eslint-bulk + ``` -This tool is designed to address the issue of introducing new ESLint rules to a large, old codebase, which often -results in hundreds to tens of thousands of retroactive issues being reported by ESLint. This can clutter the -ESLint output, making it difficult to read and potentially causing developers to overlook new ESLint issues. It -also makes it impractical to use merge request pipelines that block ESLint warnings/errors. + This installs the `eslint-bulk` shell command for managing the **.eslint-bulk-suppressions.json** files. + With it you can generate new suppressions as well as "prune" old suppressions that are no longer needed. -The tool provides a mechanism for recording all retroactively introduced ESLint warnings/errors in a -"bulk suppressions" file, hiding them from the ESLint output. This allows developers to still get most of the -benefits of ESLint, as any new code written will be annotated by ESLint and can be fixed in bite-sized portions. -It also allows the use of merge request pipelines to block newly written error-prone code without blocking legacy -code that has been battle-tested. +3. Load the patch by adding the following `require()` statement as the first line of + your **.eslintrc.js** file. For example: -## Why it's a patch -The bulk suppressions feature is implemented as a monkey-patch, inspired by the modern-module-resolution -implementation. We prefer it as a patch because it allows users to opt-in to using the tool at their own discretion. -Similar to modern-module-resolution, the use case is much more pronounced in large codebases where ESLint -warnings/errors can appear at magnitudes of thousands rather than tens. Besides reducing bundle size, this also -allows us to gauge interest in this tool. + **.eslintrc.js** + ```js + require("@rushstack/eslint-patch/eslint-bulk-suppressions"); // 👈 add this line -This approach inevitably results in forwards compatibility issues with versions of ESLint. The patch has -some logic to determine which version of ESLint you're using and uses the corresponding patch file. + module.exports = { + rules: { + rule1: 'error', + rule2: 'warning' + }, + parserOptions: { tsconfigRootDir: __dirname } + }; + ``` -## How to use it +Typical workflow: -To use the tool, you need to add a `require()` call to the top of the **.eslintrc.js** file for each project -that you want to use the tool with, for example: +1. Checkout your `main` branch, which is in a clean state where ESLint reports no violations. +2. Update your configuration to enable the latest lint rules; ESLint now reports thousands of legacy violations. +3. Run `eslint-bulk suppress --all ./src` to update **.eslint-bulk-suppressions.json.** +4. ESLint now no longer reports violations, so commit the results to Git and merge your pull request. +5. Over time, engineers may improve some of the suppressed code, in which case the associated suppressions are no longer needed. +6. Run `eslint-bulk prune` periodically to find and remove unnecessary suppressions from **.eslint-bulk-suppressions.json**, ensuring that new violations will now get caught in those scopes. -**.eslintrc.js** -```js -require("@rushstack/eslint-patch/eslint-bulk-suppressions"); +### "eslint-bulk suppress" command -module.exports = { - rules: { - rule1: 'error', - rule2: 'warning' - }, - parserOptions: { tsconfigRootDir: __dirname } -}; +```bash +eslint-bulk suppress --rule NAME1 [--rule NAME2...] PATH1 [PATH2...] +eslint-bulk suppress --all PATH1 [PATH2...] ``` -We also highly recommend globally installing the companion CLI tool to your local system with +Use this command to automatically generate bulk suppressions for the specified lint rules and file paths. +The path argument is a [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) with the same syntax +as path arguments for the `eslint` command. + + +### "eslint-bulk prune" command + +Use this command to automatically delete all unnecessary suppression entries in all +**.eslint-bulk-suppressions.json** files under the current working directory. + ```bash -npm i -g @rushstack/eslint-bulk +eslint-bulk prune ``` -The **eslint-bulk** package is a set of command line tools to use with the ESLint bulk suppressions patch. -eslint-bulk commands must be run in the same current working directory containing your package's pertaining -.eslintrc.js or .eslintrc.cjs file. +### Implementation notes -## eslint-bulk suppress +The `eslint-bulk` command is a thin wrapper whose behavior is actually provided by the patch itself. +In this way, if your monorepo contains projects using different versions of this package, the same globally +installed `eslint-bulk` command can be used under any project folder, and it will always invoke the correct +version of the engine compatible with that project. Because the patch is loaded by ESLint, the `eslint-bulk` +command must be invoked in a project folder that contains an **.eslintrc.js** configuration with correctly +installed **package.json** dependencies. -Use this command to automatically generate bulk suppressions for the given files and given rules. -Supply the paths as the main argument. The paths argument is a glob pattern that follows the same -rules as the "files" argument in the "eslint" command. +Here's an example of the bulk suppressions file content: -```bash -eslint-bulk suppress --rule NAME1 [--rule NAME2...] PATH1 [PATH2...] -eslint-bulk suppress --all PATH1 [PATH2...] +**.eslint-bulk-suppressions.json** +```js +{ + "suppressions": [ + { + "rule": "no-var", + "file": "./src/your-file.ts", + "scopeId": ".ExampleClass.exampleMethod" + } + ] +} ``` +The `rule` field is the ESLint rule name. The `file` field is the source file path, relative to the **eslintrc.js** file. The `scopeId` is a special string built from the names of containing structures. (For implementation details, take a look at the [calculateScopeId()](https://github.com/microsoft/rushstack/blob/e95c51088341f01516ee5a7639d57c3f6dce8772/eslint/eslint-patch/src/eslint-bulk-suppressions/bulk-suppressions-patch.ts#L52) function.) The `scopeId` identifies a region of code where the rule should be suppressed, while being reasonably stable across edits of the source file. -## eslint-bulk prune +# modern-module-resolution feature -Use this command to automatically delete all unused suppression entries in all .eslint-bulk-suppressions.json -files under the current working directory. +### What it does -```bash -eslint-bulk prune +This patch is a workaround for a longstanding [ESLint feature request](https://github.com/eslint/eslint/issues/3458) +that would allow a shareable ESLint config to bring along its own plugins, rather than imposing peer dependencies +on every consumer of the config. In a monorepo scenario, this enables your lint setup to be consolidated in a +single NPM package. Doing so greatly reduces the copy+pasting and version management for all the other projects +that use your standard lint rule set, but don't want to be bothered with the details. + +> **NOTE:** ESLint 8.21.0 has now introduced a new `ESLINT_USE_FLAT_CONFIG` mode that may reduce the need +> for this patch. + + +### Why it's a patch + +We initially proposed this feature in a pull request for the official ESLint back in 2019, however the +maintainers preferred to implement a more comprehensive overhaul of the ESLint config engine. It ultimately +shipped with the experimental new `ESLINT_USE_FLAT_CONFIG` mode (still opt-in as of ESLint 8). +While waiting for that, Rush Stack's `modern-module-resolution` patch provided a reliable interim solution. +We will continue to maintain this patch as long as it is being widely used, but we encourage you to check out +`ESLINT_USE_FLAT_CONFIG` and see if it meets your needs. + + +### How to use it + +1. Add `@rushstack/eslint-patch` as a dependency of your project: + + ```bash + cd your-project + npm install --save-dev @rushstack/eslint-patch + ``` + +7. Add a `require()` call to the to top of the **.eslintrc.js** file for each project that depends + on your shareable ESLint config, for example: + + **.eslintrc.js** + ```ts + require("@rushstack/eslint-patch/modern-module-resolution"); // 👈 add this line + + // Add your "extends" boilerplate here, for example: + module.exports = { + extends: ['@your-company/eslint-config'], + parserOptions: { tsconfigRootDir: __dirname } + }; + ``` + +With this change, the local project no longer needs any ESLint plugins in its **package.json** file. +Instead, the hypothetical `@your-company/eslint-config` NPM package would declare the plugins as its +own dependencies. + +This patch works by modifying the ESLint engine so that its module resolver will load relative to the folder of +the referencing config file, rather than the project folder. The patch is compatible with ESLint 6, 7, and 8. +It also works with any editor extensions that load ESLint as a library. + +For an even leaner setup, `@your-company/eslint-config` can provide the patches as its own dependency. +See [@rushstack/eslint-config](https://github.com/microsoft/rushstack/blob/main/eslint/eslint-config/patch/modern-module-resolution.js) for a real world example. + + +# custom-config-package-names feature + +### What it does + +Load the `custom-config-package-names` patch to remove ESLint's +[naming requirement](https://eslint.org/docs/latest/extend/shareable-configs) +that `eslint-config` must be part of the NPM package name for shareable configs. + +This is useful because Rush Stack's [rig package](https://heft.rushstack.io/pages/intro/rig_packages/) +specification defines a way for many different tooling configurations and dependencies to be shared +via a single NPM package, for example +[`@rushstack/heft-web-rig`](https://www.npmjs.com/package/@rushstack/heft-web-rig). +Rigs avoid a lot of copy+pasting of dependencies in a large scale monorepo. +Rig packages always include the `-rig` suffix in their name. It doesn't make sense to enforce +that `eslint-config` should also appear in the name of a package that includes shareable configs +for many other tools besides ESLint. + +### How to use it + +Continuing the example above, to load this patch you would add a second line to your config file: + +**.eslintrc.js** +```ts +require("@rushstack/eslint-patch/modern-module-resolution"); +require("@rushstack/eslint-patch/custom-config-package-names"); // 👈 add this line + +// Add your "extends" boilerplate here, for example: +module.exports = { + extends: [ + '@your-company/build-rig/profile/default/includes/eslint/node' // Notice the package name does not start with "eslint-config-" + ], + parserOptions: { tsconfigRootDir: __dirname } +}; ``` + # Links - [CHANGELOG.md](https://github.com/microsoft/rushstack/blob/main/eslint/eslint-patch/CHANGELOG.md) - Find out what's new in the latest version +- [`@rushstack/eslint-bulk`](https://www.npmjs.com/package/@rushstack/eslint-bulk) CLI package + `@rushstack/eslint-patch` is part of the [Rush Stack](https://rushstack.io/) family of projects. diff --git a/eslint/eslint-patch/package.json b/eslint/eslint-patch/package.json index b330f3948e9..389a691175d 100644 --- a/eslint/eslint-patch/package.json +++ b/eslint/eslint-patch/package.json @@ -1,7 +1,7 @@ { "name": "@rushstack/eslint-patch", "version": "1.7.0", - "description": "A patch that improves how ESLint loads plugins when working in a monorepo with a reusable toolchain", + "description": "Enhance ESLint with better support for large scale monorepos", "main": "lib/usage.js", "license": "MIT", "repository": { @@ -22,7 +22,12 @@ "resolver", "plugin", "relative", - "package" + "package", + "bulk", + "suppressions", + "monorepo", + "monkey", + "patch" ], "devDependencies": { "@rushstack/heft": "0.64.0",