├── .github └── workflows │ ├── ci.yml │ └── npm-publish.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.closure.json ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── bin └── cli.js ├── eslint.config.js ├── examples ├── browser │ ├── eslint.config.js │ └── index.mjs ├── es2021 │ ├── eslint.config.js │ └── index.mjs ├── es2022 │ ├── eslint.config.js │ └── index.mjs ├── es2023 │ ├── eslint.config.js │ └── index.mjs ├── init.sh ├── lint-all.sh ├── mocha │ ├── eslint.config.js │ ├── index.cjs │ └── test │ │ └── index.cjs ├── node18 │ ├── cjs.cjs │ ├── eslint.config.js │ └── esm.mjs ├── node20 │ ├── cjs.cjs │ ├── eslint.config.js │ └── esm.mjs ├── typescript-cjs │ ├── .npmrc │ ├── cjs.ts │ ├── eslint.config.mjs │ ├── mod.ts │ ├── package.json │ └── tsconfig.json ├── typescript-esm │ ├── cjs.cts │ ├── eslint.config.js │ ├── esm.mts │ ├── index.ts │ ├── jsx.tsx │ ├── mod-cjs.cts │ ├── mod-esm.mts │ └── tsconfig.json └── typescript-type-checked │ ├── eslint.config.js │ ├── index.ts │ └── tsconfig.json ├── package-lock.json ├── package.json ├── renovate.json ├── src ├── build.ts ├── cli.ts ├── configs │ ├── base.ts │ ├── browser.ts │ ├── es2021.ts │ ├── es2022.ts │ ├── es2023.ts │ ├── index.ts │ ├── js-esm.ts │ ├── mocha.ts │ ├── module-base.ts │ ├── node-esm.ts │ ├── node.ts │ ├── node18.ts │ ├── node20.ts │ ├── typescript-type-checked.ts │ └── typescript.ts ├── index.ts ├── merge.ts ├── types │ └── @eslint-community │ │ └── eslint-plugin-eslint-comments.d.ts └── utils.ts ├── templates ├── eslint.config-cjs.mjs └── eslint.config-esm.mjs ├── test ├── configs.mjs └── fixtures │ ├── es2021.@eslint-community#eslint-comments#no-duplicate-disable.fail.js │ ├── es2021.getter-return.fail.js │ ├── es2021.jsdoc#check-tag-names.pass.js │ ├── es2021.no-alert.pass.js │ ├── es2021.no-misleading-character-class.fail.js │ ├── es2021.object-shorthand.pass.js │ ├── es2021.prefer-destructuring.fail.js │ ├── es2021.prefer-object-spread.fail.js │ ├── es2021.unicorn#no-hex-escape.fail.js │ ├── es2021.unicorn#prefer-string-starts-ends-with.fail.js │ ├── es2022.prefer-object-has-own.fail.js │ ├── es2023.prefer-object-has-own.fail.js │ ├── modules │ ├── default-export.ts │ └── named-export-foo.ts │ ├── typescript-type-checked.@typescript-eslint#no-floating-promises.fail.ts │ ├── typescript-type-checked.@typescript-eslint#no-unnecessary-type-assertion.fail.ts │ ├── typescript-type-checked.tsconfig.json │ ├── typescript.@typescript-eslint#no-duplicate-enum-values.fail.ts │ ├── typescript.@typescript-eslint#no-namespace.fail.ts │ ├── typescript.@typescript-eslint#no-unnecessary-type-assertion.pass.ts │ ├── typescript.import-x#first.fail.ts │ └── typescript.no-dupe-keys.pass.ts └── tsconfig.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [18.x, 20.x, 22.x, 23.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - run: npm run init:examples 20 | - run: npm install 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to npm 2 | on: 3 | release: 4 | types: [published] 5 | workflow_dispatch: 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: read 11 | id-token: write 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: "22.x" 17 | registry-url: "https://registry.npmjs.org" 18 | - run: npm run init:examples 19 | - run: npm ci 20 | - run: npm test 21 | - run: npm publish --tag $(npx guess-npm-dist-tag) 22 | env: 23 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.prettierrc.closure.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/prettierrc", 3 | "singleQuote": true, 4 | "quoteProps": "preserve", 5 | "bracketSpacing": false, 6 | "arrowParens": "avoid" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | { 4 | // for projects with multiple configuration files 5 | "mode": "auto" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Teppei Sato 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-config-teppeis 2 | 3 | [ESLint](https://github.com/eslint/eslint) shareable configs for me! 4 | 5 | [![npm version][npm-image]][npm-url] 6 | ![supported node.js version][node-version] 7 | [![build status][ci-image]][ci-url] 8 | ![license][license] 9 | 10 | > [!IMPORTANT] 11 | > This config uses [new flat config style](https://eslint.org/docs/latest/use/configure/configuration-files-new) since v19. 12 | 13 | ## Priority 14 | 15 | 1. Avoid ["Possible Problems"](https://eslint.org/docs/latest/rules/#possible-problems) 16 | 2. Enable ["Suggestions"](https://eslint.org/docs/latest/rules/#suggestions) if reasonable or fixable 17 | 3. Use Prettier for stylistic formatting issues 18 | 19 | ## Install 20 | 21 | ```console 22 | $ npm i -D eslint eslint-config-teppeis 23 | ``` 24 | 25 | and run `npx eslint-config-teppeis --init` to generate initial config files. 26 | 27 | ## Usage 28 | 29 | Load `eslint-config-teppeis` and export default `build()` in your `eslint.config.js`: 30 | 31 | ```js 32 | import { build } from "eslint-config-teppeis"; 33 | import { mocha } from "eslint-config-teppeis/configs/mocha"; 34 | 35 | export default await build( 36 | { base: "node18", typescript: true, esm: true }, 37 | mocha, 38 | { 39 | ignores: ["dist", "test/fixtures"], 40 | }, 41 | ); 42 | ``` 43 | 44 | ### Options 45 | 46 | - `base` (enum, required): `es2021`, `es2022`, `es2023`, `node18` or `node20` 47 | - `typescript` (boolean, default false): use TypeScript 48 | - `project` (boolean|string|srting[], default false): the property of `parserOptions` to enable linting with type information 49 | - `esm` (boolean, default false): treat `.js` and `.ts` as ESM for a project that configures `type:module` in `package.json` 50 | 51 | ## Configs for customization 52 | 53 | ### Pure ECMAScript 54 | 55 | Configs for ECMAScript versions 56 | 57 | ```js 58 | import { es2021 } from "eslint-config-teppeis/configs/es2021"; 59 | 60 | export default [es2021]; 61 | ``` 62 | 63 | - `es2021` 64 | - `es2022` 65 | - `es2023` 66 | 67 | ### Node.js 68 | 69 | Configs for [Node versions](https://github.com/nodejs/Release) 70 | 71 | - `node18` (v18.18+ Maintenance) 72 | - `node20` (v20.9+ Active LTS) 73 | 74 | ```js 75 | import { node18 } from "eslint-config-teppeis/configs/node18"; 76 | 77 | export default [node18]; 78 | ``` 79 | 80 | ### TypeScript 81 | 82 | Configs for TypeScript 83 | 84 | - `typescript`: Enable rules that don't require type information 85 | - `typescriptTypeChecked`: Require type information 86 | 87 | ```js 88 | import { node18, typescript } from "eslint-config-teppeis/configs"; 89 | 90 | export default [node18, typescript]; 91 | ``` 92 | 93 | ### ES Modules 94 | 95 | By default, only `*.mjs` and `*.mts` are treated as ES Modules in configs for Node.js. 96 | If you use `type:module` in package.json, use `esm: true` like: 97 | 98 | ```js 99 | import { build } from "eslint-config-teppeis"; 100 | 101 | export default build({ base: "node18", esm: true }); 102 | ``` 103 | 104 | ### Browsers 105 | 106 | This enables globals for browsers. 107 | 108 | ```js 109 | import { es2023, browser } from "eslint-config-teppeis/configs"; 110 | 111 | export default [es2023, browser]; 112 | ``` 113 | 114 | ### Mocha 115 | 116 | This enables globals for Mocha like `describe` or `it` only in `**/test/*.js`. 117 | 118 | ```js 119 | import { es2023, mocha } from "eslint-config-teppeis"; 120 | 121 | export default [es2023, mocha]; 122 | ``` 123 | 124 | ## Note: Prettier 125 | 126 | Just intall `prettier` and use it with `eslint-config-teppeis`. 127 | These configs don't include rule settings that conflict with Pretteir. 128 | 129 | ## License 130 | 131 | Licensed under the MIT license. 132 | Copyright (c) 2023, Teppei Sato 133 | 134 | [npm-image]: https://badgen.net/npm/v/eslint-config-teppeis?icon=npm&label= 135 | [npm-url]: https://npmjs.org/package/eslint-config-teppeis 136 | [ci-image]: https://github.com/teppeis/eslint-config-teppeis/workflows/ci/badge.svg 137 | [ci-url]: https://github.com/teppeis/eslint-config-teppeis/actions?query=workflow%3A%22ci%22 138 | [deps-image]: https://img.shields.io/librariesio/release/npm/eslint-config-teppeis 139 | [deps-url]: https://libraries.io/npm/eslint-config-teppeis 140 | [node-version]: https://badgen.net/npm/node/eslint-config-teppeis 141 | [license]: https://badgen.net/npm/license/eslint-config-teppeis 142 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { run } from "../dist/cli.js"; 4 | 5 | run(); 6 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { build } from "eslint-config-teppeis"; 2 | import { mocha } from "eslint-config-teppeis/configs/mocha"; 3 | 4 | export default await build( 5 | { base: "node18", typescript: true, esm: true }, 6 | { 7 | ignores: ["dist", "examples", "test/fixtures"], 8 | }, 9 | { 10 | files: ["templates/*"], 11 | rules: { 12 | "n/no-missing-import": "off", 13 | }, 14 | }, 15 | mocha, 16 | ); 17 | -------------------------------------------------------------------------------- /examples/browser/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { build } from "eslint-config-teppeis"; 2 | import { browser } from "eslint-config-teppeis/configs"; 3 | 4 | export default await build({ base: "es2023" }, browser, { 5 | ignores: ["eslint.config.{js,mjs}"], 6 | }); 7 | -------------------------------------------------------------------------------- /examples/browser/index.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | // globals.browser loaded 4 | alert("foo"); 5 | 6 | // eslint-disable-next-line unicorn/prefer-dom-node-append 7 | foo.appendChild(bar); 8 | -------------------------------------------------------------------------------- /examples/es2021/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { build } from "eslint-config-teppeis"; 2 | 3 | export default await build( 4 | { base: "es2021" }, 5 | { 6 | ignores: ["eslint.config.{js,mjs}"], 7 | }, 8 | ); 9 | -------------------------------------------------------------------------------- /examples/es2021/index.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | // ES2021: unicorn/numeric-separators-style 4 | const num1 = 100000000; // onlyIfContainsSeparator: true 5 | const num2 = 1_000; // number.minimumDigits: 0 6 | const num3 = 10_000; 7 | 8 | // ES2021: globals 9 | new WeakRef(); 10 | new FinalizationRegistry(); 11 | new AggregateError(); 12 | 13 | // base 14 | const x = 0; 15 | // eslint-disable-next-line no-compare-neg-zero 16 | if (x === -0) { 17 | x.toString(); 18 | } 19 | 20 | // eslint-plugin-jsdoc 21 | /** 22 | * @param {number} a 23 | * @param {number} b 24 | * @return {number} 25 | */ 26 | function bar(a, b) { 27 | return a + b; 28 | } 29 | -------------------------------------------------------------------------------- /examples/es2022/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { build } from "eslint-config-teppeis"; 2 | 3 | export default await build( 4 | { base: "es2022" }, 5 | { 6 | ignores: ["eslint.config.{js,mjs}"], 7 | }, 8 | ); 9 | -------------------------------------------------------------------------------- /examples/es2022/index.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars, no-undef */ 2 | 3 | // ES2022 new features 4 | // eslint-disable-next-line prefer-object-has-own 5 | Object.prototype.hasOwnProperty.call(obj, "a"); 6 | // -> Object.hasOwn(obj, "a"); 7 | 8 | // eslint-disable-next-line unicorn/prefer-at 9 | const last = array[array.length - 1]; 10 | // -> const last = array.at(-1); 11 | 12 | // check base config enabled 13 | // eslint-disable-next-line no-compare-neg-zero 14 | if (x === -0) { 15 | x.toString(); 16 | } 17 | -------------------------------------------------------------------------------- /examples/es2023/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { build } from "eslint-config-teppeis"; 2 | 3 | export default await build( 4 | { base: "es2023" }, 5 | { 6 | ignores: ["eslint.config.{js,mjs}"], 7 | }, 8 | ); 9 | -------------------------------------------------------------------------------- /examples/es2023/index.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | // ES2023 no new features 4 | 5 | // check base config enabled 6 | // eslint-disable-next-line no-compare-neg-zero 7 | if (x === -0) { 8 | x.toString(); 9 | } 10 | -------------------------------------------------------------------------------- /examples/init.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | examples_dir=$(cd $(dirname $0);pwd) 5 | 6 | init() { 7 | local current_dir=$(pwd) 8 | local target_dir="$examples_dir/$1" 9 | echo "- $1" 10 | cd "$target_dir" 11 | npm install 12 | cd $current_dir 13 | } 14 | 15 | echo "Initializing examples..." 16 | init "typescript-cjs" 17 | -------------------------------------------------------------------------------- /examples/lint-all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | examples_dir=$(cd $(dirname $0);pwd) 5 | error=0 6 | 7 | lint() { 8 | local current_dir=$(pwd) 9 | local target_dir="$examples_dir/$1" 10 | echo "- $1" 11 | cd "$target_dir" 12 | set +e 13 | npx eslint --max-warnings 0 . 14 | if [ $? -ne 0 ]; then 15 | error=1 16 | fi 17 | set -e 18 | cd $current_dir 19 | } 20 | 21 | tsc() { 22 | local current_dir=$(pwd) 23 | local target_dir="$examples_dir/$1" 24 | cd "$target_dir" 25 | set +e 26 | npx tsc 27 | if [ $? -ne 0 ]; then 28 | error=1 29 | fi 30 | set -e 31 | cd $current_dir 32 | } 33 | 34 | echo "Linting examples..." 35 | lint "es2021" 36 | lint "es2022" 37 | lint "es2023" 38 | lint "node18" 39 | lint "node20" 40 | lint "typescript-cjs" 41 | tsc "typescript-cjs" 42 | lint "typescript-esm" 43 | tsc "typescript-esm" 44 | lint "typescript-type-checked" 45 | tsc "typescript-type-checked" 46 | lint "mocha" 47 | lint "browser" 48 | 49 | exit $error 50 | -------------------------------------------------------------------------------- /examples/mocha/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { build } from "eslint-config-teppeis"; 2 | import { mocha } from "eslint-config-teppeis/configs/mocha"; 3 | 4 | export default await build({ base: "node20" }, mocha, { 5 | ignores: ["eslint.config.{js,mjs}"], 6 | }); 7 | -------------------------------------------------------------------------------- /examples/mocha/index.cjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // enabled globals.node 4 | require(); 5 | // disabled globals.mocha not in test dir 6 | it(); // eslint-disable-line no-undef 7 | -------------------------------------------------------------------------------- /examples/mocha/test/index.cjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const assert = require("node:assert/strict"); 4 | 5 | describe("foo", () => { 6 | it("bar", () => { 7 | assert(true); 8 | }); 9 | it("baz", function () { 10 | // https://eslint.org/docs/rules/no-invalid-this 11 | this.timeout(5000); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /examples/node18/cjs.cjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const assert = require("node:assert/strict"); 4 | assert.ok("OK!"); 5 | -------------------------------------------------------------------------------- /examples/node18/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { build } from "eslint-config-teppeis"; 2 | 3 | export default await build( 4 | { base: "node18" }, 5 | { 6 | ignores: ["eslint.config.{js,mjs}"], 7 | }, 8 | ); 9 | -------------------------------------------------------------------------------- /examples/node18/esm.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import fs from "node:fs"; 3 | 4 | // eslint-disable-next-line n/no-deprecated-api 5 | fs.exists("./foo", () => {}); 6 | 7 | // eslint-disable-next-line unicorn/prefer-module 8 | require("node:assert/strict"); 9 | 10 | // ES2022 new syntax: class fields 11 | class C { 12 | // Public instance and static fields (Node v12.0+) 13 | static foo; 14 | 15 | // Private class fields (Node v14.6+) 16 | #x = "x"; 17 | static check(obj) { 18 | // Ergonomic brand checks (Node v16.4+) 19 | return #x in obj; 20 | } 21 | static { 22 | // Static initialization blocks (Node v16.11+) 23 | C.foo = 2; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/node20/cjs.cjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const assert = require("node:assert/strict"); 4 | assert.ok("OK!"); 5 | -------------------------------------------------------------------------------- /examples/node20/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { build } from "eslint-config-teppeis"; 2 | 3 | export default await build( 4 | { base: "node20" }, 5 | { 6 | ignores: ["eslint.config.{js,mjs}"], 7 | }, 8 | ); 9 | -------------------------------------------------------------------------------- /examples/node20/esm.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import fs from "node:fs"; 3 | 4 | // eslint-disable-next-line n/no-deprecated-api 5 | fs.exists("./foo", () => {}); 6 | 7 | // eslint-disable-next-line unicorn/prefer-module 8 | require("node:assert/strict"); 9 | 10 | // ES2022 new syntax: class fields 11 | class C { 12 | // Public instance and static fields (Node v12.0+) 13 | static foo; 14 | 15 | // Private class fields (Node v14.6+) 16 | #x = "x"; 17 | static check(obj) { 18 | // Ergonomic brand checks (Node v16.4+) 19 | return #x in obj; 20 | } 21 | static { 22 | // Static initialization blocks (Node v16.11+) 23 | C.foo = 2; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/typescript-cjs/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock = false 2 | -------------------------------------------------------------------------------- /examples/typescript-cjs/cjs.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | /** 3 | * @fileoverview A TypeScript file in type:commonjs and module:commonjs 4 | */ 5 | 6 | // (faux) import is available 7 | import fs from "node:fs"; 8 | import type { Foo } from "./mod"; 9 | import { Bar, Baz } from "./mod"; 10 | 11 | // import/require() is available 12 | import mod = require("./mod"); 13 | 14 | // global `require()` is available 15 | require("node:assert"); 16 | 17 | // module-base is loaded 18 | // eslint-disable-next-line import-x/no-self-import 19 | import("./cjs"); 20 | 21 | // configs/base is loaded 22 | const x = 0; 23 | // eslint-disable-next-line no-compare-neg-zero 24 | if (x === -0) { 25 | x.toString(); 26 | } 27 | 28 | // configs/node20 is loaded 29 | // eslint-disable-next-line n/no-deprecated-api 30 | fs.exists("./foo", () => {}); 31 | 32 | // configs/typescript is loaded 33 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 34 | interface Empty {} 35 | 36 | /* eslint-disable jsdoc/check-param-names */ 37 | /** 38 | * @param {T} notFoo 39 | * @return {T} 40 | * @template T 41 | */ 42 | function id(foo: T): T { 43 | return foo; 44 | } 45 | /* eslint-enable jsdoc/check-param-names */ 46 | -------------------------------------------------------------------------------- /examples/typescript-cjs/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { build } from "eslint-config-teppeis"; 2 | 3 | export default await build( 4 | { base: "node20", typescript: true }, 5 | { 6 | ignores: ["eslint.config.{js,mjs}", "dist"], 7 | }, 8 | ); 9 | -------------------------------------------------------------------------------- /examples/typescript-cjs/mod.ts: -------------------------------------------------------------------------------- 1 | export interface Foo { 2 | foo: string; 3 | } 4 | export class Bar {} 5 | export class Baz {} 6 | -------------------------------------------------------------------------------- /examples/typescript-cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs", 3 | "dependencies": { 4 | "eslint-config-teppeis": "file:../.." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/typescript-cjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Node 18", 4 | "compilerOptions": { 5 | "lib": ["es2023"], 6 | "module": "CommonJS", 7 | "target": "es2022", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/typescript-esm/cjs.cts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | /** 3 | * @fileoverview A CommonJS TypeScript file in type:module and module:NodeNext 4 | */ 5 | 6 | // (faux) import is available 7 | import fs from "node:fs"; 8 | import type { Foo } from "./mod-cjs.cjs"; 9 | // @ts-expect-error TS2307 10 | import { Bar } from "./mod-cjs"; 11 | 12 | // @ts-expect-error TS1471 13 | import esm = require("./mod-esm.mjs"); 14 | import cjs = require("./mod-cjs.cjs"); 15 | 16 | // node-esm is not loaded 17 | require("node:assert"); 18 | 19 | console.log(__filename); 20 | 21 | // module-base is loaded 22 | // eslint-disable-next-line import-x/no-self-import 23 | import("./cjs.cjs"); 24 | 25 | // configs/base is loaded 26 | const x = 0; 27 | // eslint-disable-next-line no-compare-neg-zero 28 | if (x === -0) { 29 | x.toString(); 30 | } 31 | 32 | // configs/node20 is loaded 33 | // eslint-disable-next-line n/no-deprecated-api 34 | fs.exists("./foo", () => {}); 35 | 36 | // configs/typescript is loaded 37 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 38 | interface Empty {} 39 | -------------------------------------------------------------------------------- /examples/typescript-esm/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { build } from "eslint-config-teppeis"; 2 | 3 | export default await build( 4 | { base: "node20", typescript: true, esm: true }, 5 | { 6 | ignores: ["eslint.config.{js,mjs}", "dist"], 7 | }, 8 | ); 9 | -------------------------------------------------------------------------------- /examples/typescript-esm/esm.mts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | /** 3 | * @fileoverview An ESM TypeScript file in type:module and module:NodeNext 4 | */ 5 | 6 | // ESM import is available 7 | import fs from "node:fs"; 8 | import type { Foo } from "./mod-esm.mjs"; 9 | // requires an extension 10 | // @ts-expect-error TS2835 11 | import { Bar } from "./mod-esm"; 12 | 13 | // @ts-expect-error TS1471 14 | import esm = require("./mod-esm.mjs"); 15 | import cjs = require("./mod-cjs.cjs"); 16 | 17 | // node-esm is loaded 18 | // eslint-disable-next-line unicorn/prefer-module 19 | require("node:assert"); 20 | 21 | // eslint-disable-next-line unicorn/prefer-module 22 | console.log(__filename); 23 | 24 | // module-base is loaded 25 | // eslint-disable-next-line import-x/no-self-import 26 | import("./esm.mjs"); 27 | 28 | // configs/base is loaded 29 | const x = 0; 30 | // eslint-disable-next-line no-compare-neg-zero 31 | if (x === -0) { 32 | x.toString(); 33 | } 34 | 35 | // configs/node20 is loaded 36 | // eslint-disable-next-line n/no-deprecated-api 37 | fs.exists("./foo", () => {}); 38 | 39 | // configs/typescript is loaded 40 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 41 | interface Empty {} 42 | -------------------------------------------------------------------------------- /examples/typescript-esm/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | /** 3 | * @fileoverview An ESM TypeScript file in type:module and module:NodeNext 4 | */ 5 | 6 | // ESM import is available 7 | import fs from "node:fs"; 8 | import type { Foo } from "./mod-esm.mjs"; 9 | // requires an extension 10 | // @ts-expect-error TS2835 11 | import { Bar } from "./mod-esm"; 12 | 13 | // @ts-expect-error TS1471 14 | import esm = require("./mod-esm.mjs"); 15 | import cjs = require("./mod-cjs.cjs"); 16 | 17 | // node-esm is loaded 18 | // eslint-disable-next-line unicorn/prefer-module 19 | require("node:assert"); 20 | 21 | // eslint-disable-next-line unicorn/prefer-module 22 | console.log(__filename); 23 | 24 | // module-base is loaded 25 | // eslint-disable-next-line import-x/no-self-import 26 | import("./index.js"); 27 | 28 | // configs/base is loaded 29 | const x = 0; 30 | // eslint-disable-next-line no-compare-neg-zero 31 | if (x === -0) { 32 | x.toString(); 33 | } 34 | 35 | // configs/node20 is loaded 36 | // eslint-disable-next-line n/no-deprecated-api 37 | fs.exists("./foo", () => {}); 38 | 39 | // configs/typescript is loaded 40 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 41 | interface Empty {} 42 | -------------------------------------------------------------------------------- /examples/typescript-esm/jsx.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | /** 3 | * @fileoverview A JSX TypeScript file in type:module and module:NodeNext 4 | */ 5 | 6 | // ESM import is available 7 | import fs from "node:fs"; 8 | import type { Foo } from "./mod-esm.mjs"; 9 | // requires an extension 10 | // @ts-expect-error TS2835 11 | import { Bar } from "./mod-esm"; 12 | 13 | // @ts-expect-error TS1471 14 | import esm = require("./mod-esm.mjs"); 15 | import cjs = require("./mod-cjs.cjs"); 16 | 17 | // node-esm is loaded 18 | // eslint-disable-next-line unicorn/prefer-module 19 | require("node:assert"); 20 | 21 | // eslint-disable-next-line unicorn/prefer-module 22 | console.log(__filename); 23 | 24 | // module-base is loaded 25 | // eslint-disable-next-line import-x/no-self-import 26 | import("./jsx.js"); 27 | 28 | // configs/base is loaded 29 | const x = 0; 30 | // eslint-disable-next-line no-compare-neg-zero 31 | if (x === -0) { 32 | x.toString(); 33 | } 34 | 35 | // configs/node20 is loaded 36 | // eslint-disable-next-line n/no-deprecated-api 37 | fs.exists("./foo", () => {}); 38 | 39 | // configs/typescript is loaded 40 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 41 | interface Empty {} 42 | 43 | // JSX 44 | const Button = ; 45 | -------------------------------------------------------------------------------- /examples/typescript-esm/mod-cjs.cts: -------------------------------------------------------------------------------- 1 | export interface Foo { 2 | foo: string; 3 | } 4 | export class Bar {} 5 | export class Baz {} 6 | -------------------------------------------------------------------------------- /examples/typescript-esm/mod-esm.mts: -------------------------------------------------------------------------------- 1 | export interface Foo { 2 | foo: string; 3 | } 4 | export class Bar {} 5 | export class Baz {} 6 | -------------------------------------------------------------------------------- /examples/typescript-esm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Node 18", 4 | "compilerOptions": { 5 | "lib": ["es2023"], 6 | "module": "Node16", 7 | "moduleResolution": "Node16", 8 | "target": "es2022", 9 | "jsx": "preserve", 10 | "strict": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/typescript-type-checked/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { build } from "eslint-config-teppeis"; 2 | 3 | export default await build( 4 | { 5 | base: "node20", 6 | typescript: true, 7 | project: true, 8 | }, 9 | { 10 | ignores: ["eslint.config.{js,mjs}"], 11 | }, 12 | ); 13 | -------------------------------------------------------------------------------- /examples/typescript-type-checked/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | 3 | const arg1 = [1, 2]; 4 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 5 | const msg1 = `arg1 = ${arg1}`; 6 | -------------------------------------------------------------------------------- /examples/typescript-type-checked/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Node 18", 4 | "compilerOptions": { 5 | "lib": ["es2023"], 6 | "module": "Node16", 7 | "moduleResolution": "Node16", 8 | "target": "es2022", 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-teppeis", 3 | "description": "ESLint config set for me!", 4 | "version": "19.1.2", 5 | "author": "Teppei Sato ", 6 | "publishConfig": { 7 | "provenance": true 8 | }, 9 | "engines": { 10 | "node": "^18.18.2 || >=20.9.0" 11 | }, 12 | "type": "module", 13 | "files": [ 14 | "bin", 15 | "dist", 16 | "templates" 17 | ], 18 | "bin": { 19 | "eslint-config-teppeis": "bin/cli.js" 20 | }, 21 | "exports": { 22 | ".": { 23 | "import": "./dist/index.js" 24 | }, 25 | "./configs": { 26 | "import": "./dist/configs/index.js" 27 | }, 28 | "./configs/*": { 29 | "import": "./dist/configs/*.js" 30 | }, 31 | "./package.json": "./package.json" 32 | }, 33 | "scripts": { 34 | "build": "tsc", 35 | "clean": "rm -rf dist", 36 | "init:examples": "./examples/init.sh", 37 | "lint": "run-p -l -c --aggregate-output lint:*", 38 | "lint:eslint": "eslint --max-warnings 0 .", 39 | "lint:prettier": "prettier --check .", 40 | "fix": "run-s fix:prettier fix:eslint", 41 | "fix:eslint": "npm run lint:eslint -- --fix", 42 | "fix:prettier": "npm run lint:prettier -- --write", 43 | "test": "npm-run-all -l -c --aggregate-output clean build -p lint:* test:*", 44 | "test:examples": "./examples/lint-all.sh", 45 | "test:unit": "mocha test" 46 | }, 47 | "dependencies": { 48 | "@eslint-community/eslint-plugin-eslint-comments": "^4.5.0", 49 | "@eslint/js": "^9.24.0", 50 | "deepmerge": "^4.3.1", 51 | "eslint-import-resolver-typescript": "^4.3.5", 52 | "eslint-plugin-import-x": "^4.10.6", 53 | "eslint-plugin-jsdoc": "^50.6.17", 54 | "eslint-plugin-n": "^17.17.0", 55 | "eslint-plugin-unicorn": "^58.0.0", 56 | "globals": "^16.0.0", 57 | "typescript-eslint": "^8.30.1" 58 | }, 59 | "devDependencies": { 60 | "@types/node": "^18.19.110", 61 | "@types/react": "^19.1.6", 62 | "eslint": "^9.24.0", 63 | "eslint-config-prettier": "^10.1.5", 64 | "glob": "^10.4.5", 65 | "mocha": "^11.5.0", 66 | "npm-run-all": "^4.1.5", 67 | "prettier": "^3.5.3", 68 | "typescript": "^5.8.3" 69 | }, 70 | "peerDependencies": { 71 | "eslint": "^9.24.0", 72 | "prettier": "^3.5.3" 73 | }, 74 | "peerDependenciesMeta": { 75 | "prettier": { 76 | "optional": true 77 | } 78 | }, 79 | "homepage": "https://github.com/teppeis/eslint-config-teppeis", 80 | "repository": "https://github.com/teppeis/eslint-config-teppeis", 81 | "bugs": { 82 | "url": "https://github.com/teppeis/eslint-config-teppeis" 83 | }, 84 | "keywords": [ 85 | "eslint", 86 | "eslint-config", 87 | "prettier", 88 | "typescript" 89 | ], 90 | "license": "MIT" 91 | } 92 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>teppeis/renovate-config"], 3 | "dependencyDashboard": true 4 | } 5 | -------------------------------------------------------------------------------- /src/build.ts: -------------------------------------------------------------------------------- 1 | import type { Linter } from "eslint"; 2 | import { jsEsm } from "./configs/js-esm.js"; 3 | import { merge } from "./merge.js"; 4 | import { nonNull } from "./utils.js"; 5 | 6 | interface BuildOptions { 7 | base: "es2021" | "es2022" | "es2023" | "node18" | "node20"; 8 | esm?: boolean; 9 | typescript?: boolean; 10 | project?: boolean | string | string[]; 11 | } 12 | 13 | export async function build( 14 | options: BuildOptions, 15 | ...additionalConfigs: Linter.Config[] 16 | ): Promise { 17 | const { base, typescript, project, esm } = options; 18 | 19 | const baseConfigNames = ["es2021", "es2022", "es2023", "node18", "node20"]; 20 | if (!baseConfigNames.includes(base)) { 21 | throw new TypeError(`Unexpected base: ${base}`); 22 | } 23 | const baseConfig = (await import(`./configs/${base}.js`))[base]; 24 | 25 | let nodeEsmConfig = {}; 26 | const nodeConfigNames = ["node18", "node20"]; 27 | if (nodeConfigNames.includes(base)) { 28 | const { nodeEsm } = await import("./configs/node-esm.js"); 29 | nodeEsmConfig = nodeEsm; 30 | } 31 | 32 | let tsConfig; 33 | if (typescript) { 34 | if (project) { 35 | if ( 36 | typeof project === "boolean" || 37 | typeof project === "string" || 38 | Array.isArray(project) 39 | ) { 40 | tsConfig = (await import("./configs/typescript-type-checked.js")) 41 | .typescriptTypeChecked; 42 | nonNull(nonNull(tsConfig.languageOptions).parserOptions).project = 43 | project; 44 | } else { 45 | throw new TypeError(`project is unexpected: ${project}`); 46 | } 47 | } else { 48 | tsConfig = (await import("./configs/typescript.js")).typescript; 49 | } 50 | } else if (project) { 51 | throw new TypeError("Specify both `typescript` and `project`."); 52 | } 53 | 54 | let cjsExtensions = "**/*.{js,jsx,cjs}"; 55 | let esmExtensions = "**/*.mjs"; 56 | let cjsTsExtensions = "**/*.{ts,tsx,cts}"; 57 | let esmTsExtensions = "**/*.mts"; 58 | if (esm) { 59 | cjsExtensions = "**/*.cjs"; 60 | esmExtensions = "**/*.{js,jsx,mjs}"; 61 | cjsTsExtensions = "**/*.cts"; 62 | esmTsExtensions = "**/*.{ts,tsx,mts}"; 63 | } 64 | const configArray = [ 65 | { 66 | ...baseConfig, 67 | files: [cjsExtensions], 68 | }, 69 | { 70 | ...merge(baseConfig, jsEsm, nodeEsmConfig), 71 | files: [esmExtensions], 72 | }, 73 | ]; 74 | if (tsConfig) { 75 | configArray.push( 76 | { 77 | ...merge(baseConfig, tsConfig), 78 | files: [cjsTsExtensions], 79 | }, 80 | { 81 | ...merge(baseConfig, nodeEsmConfig, tsConfig), 82 | files: [esmTsExtensions], 83 | }, 84 | ); 85 | } 86 | configArray.push(...additionalConfigs); 87 | return configArray; 88 | } 89 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | import { parseArgs } from "node:util"; 4 | 5 | export function run() { 6 | // parse args and only accept `--init` option 7 | const { values } = parseArgs({ 8 | options: { init: { type: "boolean" } }, 9 | }); 10 | if (!values.init) { 11 | console.log("Usage: eslint-config-teppeis --init"); 12 | // eslint-disable-next-line unicorn/no-process-exit 13 | process.exit(1); 14 | } 15 | 16 | if (fs.existsSync("eslint.config.js")) { 17 | throw new Error("eslint.config.js already exists."); 18 | } 19 | 20 | if (fs.existsSync("eslint.config.mjs")) { 21 | throw new Error("eslint.config.mjs already exists."); 22 | } 23 | 24 | const { type } = findUpPackageJson(); 25 | if (type === "module") { 26 | const templateUrl = resolveUrl(`../templates/eslint.config-esm.mjs`); 27 | fs.copyFileSync(templateUrl, "eslint.config.js"); 28 | console.log("create: eslint.config.js"); 29 | } else { 30 | const templateUrl = resolveUrl(`../templates/eslint.config-cjs.mjs`); 31 | fs.copyFileSync(templateUrl, "eslint.config.mjs"); 32 | console.log("create: eslint.config.mjs"); 33 | } 34 | } 35 | 36 | function findUpPackageJson(): any { 37 | let dir = process.cwd(); 38 | const { root } = path.parse(dir); 39 | while (dir !== root) { 40 | const pkgPath = path.join(dir, "package.json"); 41 | if (fs.existsSync(pkgPath)) { 42 | const pkg = fs.readFileSync(pkgPath, "utf8"); 43 | return JSON.parse(pkg); 44 | } 45 | dir = path.dirname(dir); 46 | } 47 | throw new Error("package.json not found"); 48 | } 49 | 50 | function resolveUrl(specifier: string): URL { 51 | return new URL(specifier, import.meta.url); 52 | } 53 | -------------------------------------------------------------------------------- /src/configs/base.ts: -------------------------------------------------------------------------------- 1 | import comments from "@eslint-community/eslint-plugin-eslint-comments/configs"; 2 | import js from "@eslint/js"; 3 | import jsdoc from "eslint-plugin-jsdoc"; 4 | import unicorn from "eslint-plugin-unicorn"; 5 | import { merge } from "../merge.js"; 6 | 7 | export const base = merge(js.configs.recommended, comments.recommended, { 8 | languageOptions: { 9 | ecmaVersion: 2020, 10 | sourceType: "script", 11 | }, 12 | plugins: { 13 | // Be careful when removing these plugins! 14 | // They may be referenced by other config files. 15 | unicorn, 16 | jsdoc, 17 | }, 18 | linterOptions: { 19 | // replace "eslint-comments/no-unused-disable" 20 | reportUnusedDisableDirectives: "error", 21 | }, 22 | rules: { 23 | // ## Possible Problems 24 | "array-callback-return": 2, 25 | "no-async-promise-executor": 2, 26 | // this is a useful case of `await` 27 | // 'no-await-in-loop': 0, 28 | // overwrite recommended: allow `while (true)` 29 | "no-constant-condition": [2, { checkLoops: false }], 30 | // overwrite recommended: allow `try {foo();} catch (e) {}` 31 | "no-empty": [2, { allowEmptyCatch: true }], 32 | // use "import-x/no-duplicates" instead 33 | // "no-duplicate-imports": 2, 34 | "no-import-assign": 2, 35 | "no-misleading-character-class": 2, 36 | "no-self-compare": 2, 37 | "no-unmodified-loop-condition": 2, 38 | // overwrite recommended: disallow `if (! a < b) {}` 39 | "no-unsafe-negation": [2, { enforceForOrderingRelations: true }], 40 | "no-unsafe-optional-chaining": 2, 41 | // overwrite recommended: allow args 42 | "no-unused-vars": [2, { args: "none" }], 43 | "require-atomic-updates": 2, 44 | // overwrite recommended: disallow `array.indexOf(NaN)` 45 | "use-isnan": [2, { enforceForIndexOf: true }], 46 | 47 | // ## Suggestions 48 | "arrow-body-style": 2, 49 | // prefer `let` or `const` 50 | "block-scoped-var": 2, 51 | "default-case": 2, 52 | // not for Closure 53 | "dot-notation": 2, 54 | eqeqeq: [2, "smart"], 55 | "no-array-constructor": 2, 56 | "no-caller": 2, 57 | "no-eval": 2, 58 | // don't touch native prototype! 59 | "no-extend-native": 2, 60 | "no-extra-bind": 2, 61 | "no-extra-label": 2, 62 | // use `String(a)` instead of `'' + a` 63 | "no-implicit-coercion": [2, { allow: ["!!"] }], 64 | "no-implied-eval": 2, 65 | "no-invalid-this": 2, 66 | "no-iterator": 2, 67 | "no-label-var": 2, 68 | "no-labels": 2, 69 | "no-lone-blocks": 2, 70 | "no-loop-func": 2, 71 | "no-multi-str": 2, 72 | "no-new-func": 2, 73 | "no-new-wrappers": 2, 74 | "no-object-constructor": 2, 75 | "no-octal-escape": 2, 76 | // want to warn only props... 77 | // "no-param-reassign": [2, {"props": true}], 78 | "no-proto": 2, 79 | "no-return-assign": 2, 80 | "no-sequences": 2, 81 | "no-throw-literal": 2, 82 | "no-undef-init": 2, 83 | // prevented by `no-global-assign` and `no-shadow-restricted-names` 84 | // 'no-undefined': 2, 85 | "no-unneeded-ternary": 2, 86 | "no-unused-expressions": [ 87 | 2, 88 | { 89 | allowShortCircuit: true, 90 | allowTernary: true, 91 | allowTaggedTemplates: true, 92 | }, 93 | ], 94 | "no-useless-call": 2, 95 | "no-useless-computed-key": 2, 96 | "no-useless-concat": 2, 97 | "no-useless-rename": 2, 98 | "no-useless-return": 2, 99 | "no-var": 2, 100 | "no-void": 2, 101 | "object-shorthand": [2, "methods"], 102 | "operator-assignment": [2, "always"], 103 | "prefer-arrow-callback": [2, { allowNamedFunctions: true }], 104 | "prefer-const": 2, 105 | "prefer-destructuring": [2, { object: true, array: false }], 106 | "prefer-exponentiation-operator": 2, 107 | "prefer-numeric-literals": 2, 108 | "prefer-object-spread": 2, 109 | "prefer-rest-params": 2, 110 | "prefer-template": 2, 111 | radix: 2, 112 | strict: [2, "global"], 113 | yoda: [2, "never", { onlyEquality: true }], 114 | 115 | // ## Layout & Formatting 116 | "unicode-bom": 2, 117 | 118 | // # prettier 119 | // recommended rules but conflict with prettier 120 | // https://github.com/prettier/eslint-config-prettier 121 | "no-unexpected-multiline": 0, 122 | // TODO: The following two rules were deprecated in v8.53.0 123 | // and will be removed from recommended in v9.0.0. 124 | "no-extra-semi": 0, 125 | "no-mixed-spaces-and-tabs": 0, 126 | 127 | // # @eslint-community/eslint-plugin-eslint-comments 128 | // https://github.com/eslint-community/eslint-plugin-eslint-comments 129 | "@eslint-community/eslint-comments/disable-enable-pair": [ 130 | 2, 131 | { allowWholeFile: true }, 132 | ], 133 | // overwrite recommended 134 | "@eslint-community/eslint-comments/no-unlimited-disable": 0, 135 | 136 | // # eslint-plugin-unicorn 137 | // https://github.com/sindresorhus/eslint-plugin-unicorn 138 | "unicorn/custom-error-definition": 2, 139 | "unicorn/escape-case": 2, 140 | "unicorn/new-for-builtins": 2, 141 | "unicorn/no-for-loop": 2, 142 | "unicorn/no-hex-escape": 2, 143 | "unicorn/no-instanceof-array": 2, 144 | "unicorn/no-typeof-undefined": 2, 145 | "unicorn/no-useless-fallback-in-spread": 2, 146 | "unicorn/no-useless-promise-resolve-reject": 2, 147 | "unicorn/no-useless-spread": 2, 148 | "unicorn/prefer-array-index-of": 2, 149 | "unicorn/prefer-date-now": 2, 150 | "unicorn/prefer-default-parameters": 2, 151 | "unicorn/prefer-includes": 2, 152 | "unicorn/prefer-logical-operator-over-ternary": 2, 153 | "unicorn/prefer-math-trunc": 2, 154 | "unicorn/prefer-negative-index": 2, 155 | "unicorn/prefer-number-properties": [2, { checkInfinity: false }], 156 | "unicorn/prefer-optional-catch-binding": 2, 157 | "unicorn/prefer-regexp-test": 2, 158 | "unicorn/prefer-string-slice": 2, 159 | "unicorn/prefer-string-starts-ends-with": 2, 160 | "unicorn/prefer-string-trim-start-end": 2, 161 | "unicorn/prefer-type-error": 2, 162 | 163 | // # eslint-plugin-jsdoc 164 | // https://github.com/gajus/eslint-plugin-jsdoc 165 | "jsdoc/check-param-names": 2, 166 | "jsdoc/check-tag-names": 2, 167 | "jsdoc/check-types": [2, { unifyParentAndChildTypeChecks: true }], 168 | // missing many global types 169 | // 'jsdoc/no-undefined-types': 2, 170 | "jsdoc/require-hyphen-before-param-description": [2, "never"], 171 | "jsdoc/require-param-name": 2, 172 | "jsdoc/require-returns-check": 2, 173 | "jsdoc/valid-types": 2, 174 | }, 175 | settings: { 176 | jsdoc: { 177 | mode: "typescript", 178 | // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-types 179 | preferredTypes: { 180 | object: "Object", 181 | ".": "<>", 182 | }, 183 | // https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler 184 | tagNamePreference: { 185 | augments: "extends", 186 | class: "constructor", 187 | constant: "const", 188 | exports: "export", 189 | file: "fileoverview", 190 | inheritdoc: "inheritDoc", 191 | returns: "return", 192 | }, 193 | }, 194 | }, 195 | }); 196 | -------------------------------------------------------------------------------- /src/configs/browser.ts: -------------------------------------------------------------------------------- 1 | import type { Linter } from "eslint"; 2 | import unicorn from "eslint-plugin-unicorn"; 3 | import globals from "globals"; 4 | 5 | export const browser: Linter.Config = { 6 | files: ["**/*.{js,cjs,mjs,jsx,ts,tsx,cts,mts}"], 7 | languageOptions: { 8 | globals: { 9 | ...globals.browser, 10 | }, 11 | }, 12 | plugins: { unicorn }, 13 | rules: { 14 | "unicorn/prefer-blob-reading-methods": 2, 15 | "unicorn/prefer-dom-node-append": 2, 16 | "unicorn/prefer-dom-node-dataset": 2, 17 | "unicorn/prefer-dom-node-remove": 2, 18 | "unicorn/prefer-dom-node-text-content": 2, 19 | "unicorn/prefer-modern-dom-apis": 2, 20 | "unicorn/require-post-message-target-origin": 2, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/configs/es2021.ts: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import { merge } from "../merge.js"; 3 | import { base } from "./base.js"; 4 | 5 | export const es2021 = merge(base, { 6 | languageOptions: { 7 | // NOTE: ES2021 doesn't support top-level await. 8 | ecmaVersion: 2021, 9 | globals: { 10 | ...globals.es2021, 11 | }, 12 | }, 13 | rules: { 14 | "unicorn/numeric-separators-style": [ 15 | "error", 16 | { onlyIfContainsSeparator: true, number: { minimumDigits: 0 } }, 17 | ], 18 | "unicorn/prefer-string-replace-all": 2, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /src/configs/es2022.ts: -------------------------------------------------------------------------------- 1 | import { merge } from "../merge.js"; 2 | import { es2021 } from "./es2021.js"; 3 | 4 | export const es2022 = merge(es2021, { 5 | languageOptions: { 6 | ecmaVersion: 2022, 7 | // no new globals 8 | }, 9 | rules: { 10 | "prefer-object-has-own": 2, 11 | "unicorn/prefer-at": 2, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/configs/es2023.ts: -------------------------------------------------------------------------------- 1 | import { merge } from "../merge.js"; 2 | import { es2022 } from "./es2022.js"; 3 | 4 | export const es2023 = merge(es2022, { 5 | languageOptions: { 6 | ecmaVersion: 2023, 7 | // no new globals 8 | }, 9 | rules: {}, 10 | }); 11 | -------------------------------------------------------------------------------- /src/configs/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./browser.js"; 2 | export * from "./es2021.js"; 3 | export * from "./es2022.js"; 4 | export * from "./es2023.js"; 5 | export * from "./mocha.js"; 6 | export * from "./node18.js"; 7 | export * from "./node20.js"; 8 | export * from "./typescript-type-checked.js"; 9 | export * from "./typescript.js"; 10 | -------------------------------------------------------------------------------- /src/configs/js-esm.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Config for Node ESM in JS (not TS, tsc checks them instead) 3 | */ 4 | import { merge } from "../merge.js"; 5 | import { moduleBase } from "./module-base.js"; 6 | 7 | export const jsEsm = merge(moduleBase, { 8 | rules: { 9 | // ** Helpful warnings ** 10 | "import-x/export": 2, 11 | "import-x/no-named-as-default": 2, 12 | // "import-x/no-named-as-default-member": 2, 13 | 14 | // ** Static analysis ** 15 | "import-x/default": 2, 16 | "import-x/named": 2, 17 | "import-x/namespace": 2, 18 | 19 | // ** Style guide ** 20 | "import-x/extensions": [2, "always", { ignorePackages: true }], 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /src/configs/mocha.ts: -------------------------------------------------------------------------------- 1 | import type { Linter } from "eslint"; 2 | import globals from "globals"; 3 | 4 | export const mocha: Linter.Config = { 5 | files: ["**/test/**/*.{js,cjs,mjs,jsx,ts,tsx,cts,mts}"], 6 | languageOptions: { 7 | globals: { 8 | ...globals.mocha, 9 | }, 10 | }, 11 | rules: { 12 | // allow `this.timeout(1000)` 13 | "no-invalid-this": 0, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/configs/module-base.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Base config for TS and Node ESM JS 3 | */ 4 | import type { Linter } from "eslint"; 5 | import importX from "eslint-plugin-import-x"; 6 | 7 | export const moduleBase: Linter.Config = { 8 | languageOptions: { 9 | sourceType: "module", 10 | }, 11 | plugins: { "import-x": importX as any }, 12 | rules: { 13 | // for both TypeScript and non-TypeScript rules 14 | 15 | // ** Helpful warnings ** 16 | "import-x/no-mutable-exports": 2, 17 | 18 | // ** Static analysis ** 19 | "import-x/no-absolute-path": 2, 20 | "import-x/no-self-import": 2, 21 | "import-x/no-useless-path-segments": 2, 22 | 23 | // ** Style guide ** 24 | "import-x/first": 2, 25 | "import-x/newline-after-import": 2, 26 | "import-x/no-duplicates": 2, 27 | "import-x/order": [ 28 | 2, 29 | { 30 | groups: [ 31 | ["builtin", "external"], 32 | "internal", 33 | "index", 34 | "parent", 35 | "sibling", 36 | ], 37 | }, 38 | ], 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/configs/node-esm.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Disable globals unavailable in Node ESM 3 | */ 4 | import type { Linter } from "eslint"; 5 | import unicorn from "eslint-plugin-unicorn"; 6 | 7 | export const nodeEsm: Linter.Config = { 8 | languageOptions: { 9 | sourceType: "module", 10 | }, 11 | plugins: { unicorn }, 12 | rules: { 13 | "unicorn/prefer-module": 2, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/configs/node.ts: -------------------------------------------------------------------------------- 1 | import type { Linter } from "eslint"; 2 | import n from "eslint-plugin-n"; 3 | import unicorn from "eslint-plugin-unicorn"; 4 | import globals from "globals"; 5 | 6 | export const node: Linter.Config = { 7 | languageOptions: { 8 | sourceType: "commonjs", 9 | globals: { 10 | ...globals.node, 11 | }, 12 | }, 13 | plugins: { n, unicorn }, 14 | rules: { 15 | // ## eslint-plugin-n 16 | "n/handle-callback-err": 2, 17 | "n/no-deprecated-api": 2, 18 | "n/no-extraneous-import": 2, 19 | "n/no-extraneous-require": 2, 20 | "n/no-missing-import": 2, 21 | "n/no-missing-require": 2, 22 | "n/no-new-require": 2, 23 | "n/no-unpublished-bin": 2, 24 | "n/no-unpublished-import": 2, 25 | "n/no-unpublished-require": 2, 26 | // "n/no-unsupported-features/es-builtins": 2, 27 | // "n/no-unsupported-features/es-syntax": 2, 28 | // "n/no-unsupported-features/node-builtins": 2, 29 | "n/process-exit-as-throw": 2, 30 | "n/shebang": 2, 31 | 32 | // ## eslint-plugin-unicorn 33 | "unicorn/no-process-exit": 2, 34 | "unicorn/prefer-node-protocol": 2, 35 | }, 36 | settings: { 37 | n: { 38 | typescriptExtensionMap: [ 39 | // Add for CJS (ex: `import "./foo"` -> `./foo.ts`). 40 | // These are overridden and ignored in the TS to JS mapping. 41 | [".ts", ""], 42 | [".tsx", ""], 43 | // Default mappings (for react non-preserve) 44 | ["", ".js"], 45 | [".ts", ".js"], 46 | [".cts", ".cjs"], 47 | [".mts", ".mjs"], 48 | [".tsx", ".js"], 49 | ], 50 | }, 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /src/configs/node18.ts: -------------------------------------------------------------------------------- 1 | import { merge } from "../merge.js"; 2 | import { es2023 } from "./es2023.js"; 3 | import { node } from "./node.js"; 4 | 5 | export const node18 = merge(es2023, node); 6 | -------------------------------------------------------------------------------- /src/configs/node20.ts: -------------------------------------------------------------------------------- 1 | import { merge } from "../merge.js"; 2 | import { es2023 } from "./es2023.js"; 3 | import { node } from "./node.js"; 4 | 5 | export const node20 = merge(es2023, node); 6 | -------------------------------------------------------------------------------- /src/configs/typescript-type-checked.ts: -------------------------------------------------------------------------------- 1 | import { merge } from "../merge.js"; 2 | import { typescript } from "./typescript.js"; 3 | 4 | export const typescriptTypeChecked = merge(typescript, { 5 | languageOptions: { 6 | parserOptions: { 7 | project: true, 8 | }, 9 | }, 10 | rules: { 11 | // Extend ESLint rules 12 | "no-throw-literal": 0, 13 | "@typescript-eslint/only-throw-error": 2, 14 | "prefer-destructuring": 0, 15 | "@typescript-eslint/prefer-destructuring": 2, 16 | 17 | "@typescript-eslint/await-thenable": 2, 18 | "@typescript-eslint/consistent-type-exports": 2, 19 | "@typescript-eslint/no-floating-promises": 2, 20 | "@typescript-eslint/no-for-in-array": 2, 21 | "@typescript-eslint/no-misused-promises": 2, 22 | "@typescript-eslint/no-mixed-enums": 2, 23 | "@typescript-eslint/no-unnecessary-condition": [ 24 | 2, 25 | { checkTypePredicates: true }, 26 | ], 27 | "@typescript-eslint/no-unnecessary-type-assertion": 2, 28 | "@typescript-eslint/restrict-plus-operands": 2, 29 | "@typescript-eslint/restrict-template-expressions": 2, 30 | "@typescript-eslint/switch-exhaustiveness-check": 2, 31 | // override with jest/unbound-method for testing 32 | // https://github.com/jest-community/eslint-plugin-jest/blob/main/docs/rules/unbound-method.md 33 | "@typescript-eslint/unbound-method": 2, 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/configs/typescript.ts: -------------------------------------------------------------------------------- 1 | import type { Linter } from "eslint"; 2 | import tsEslint from "typescript-eslint"; 3 | import { merge } from "../merge.js"; 4 | import { moduleBase } from "./module-base.js"; 5 | 6 | export const typescript = merge( 7 | moduleBase, 8 | ...(tsEslint.configs.recommended as Linter.Config[]), 9 | { 10 | files: ["**/*.{ts,tsx,mts,cts}"], 11 | languageOptions: { 12 | ecmaVersion: "latest", 13 | }, 14 | rules: { 15 | // Allow special triple slashes comment: "/// " 16 | "spaced-comment": [ 17 | 2, 18 | "always", 19 | { line: { markers: ["/"] }, block: { balanced: true } }, 20 | ], 21 | 22 | // Check with TSC instead 23 | "n/no-missing-import": 0, 24 | 25 | // Extend ESLint rules (enabled in base config) 26 | // NOTE: skip extending stylistic rules that are overrided by prettier 27 | "no-invalid-this": 0, 28 | "@typescript-eslint/no-invalid-this": 2, 29 | "no-loop-func": 0, 30 | "@typescript-eslint/no-loop-func": 2, 31 | 32 | // Override recommended rules 33 | "@typescript-eslint/no-explicit-any": 0, 34 | "@typescript-eslint/no-namespace": [2, { allowDeclarations: true }], 35 | "@typescript-eslint/no-require-imports": 0, 36 | "@typescript-eslint/no-unused-expressions": [ 37 | 2, 38 | { 39 | allowShortCircuit: true, 40 | allowTernary: true, 41 | allowTaggedTemplates: true, 42 | }, 43 | ], 44 | "@typescript-eslint/no-unused-vars": [2, { args: "none" }], 45 | 46 | // Stylistic rules 47 | "@typescript-eslint/adjacent-overload-signatures": 2, 48 | "@typescript-eslint/consistent-type-assertions": 2, 49 | "@typescript-eslint/no-inferrable-types": 2, 50 | 51 | // Strict rules 52 | "@typescript-eslint/consistent-type-imports": 2, 53 | "@typescript-eslint/no-import-type-side-effects": 2, 54 | "@typescript-eslint/no-non-null-assertion": 2, 55 | "@typescript-eslint/prefer-literal-enum-member": 2, 56 | }, 57 | settings: { 58 | // Use only resolver, don't config `import-x/extensions` or `import-x/parsers` 59 | // that cause extra TS parsing and perf issues. 60 | "import-x/resolver": "typescript", 61 | }, 62 | }, 63 | ); 64 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { build } from "./build.js"; 2 | export { merge } from "./merge.js"; 3 | -------------------------------------------------------------------------------- /src/merge.ts: -------------------------------------------------------------------------------- 1 | import type { ArrayMergeOptions } from "deepmerge"; 2 | import deepmerge from "deepmerge"; 3 | import type { Linter } from "eslint"; 4 | 5 | type FlatConfig = Linter.Config; 6 | 7 | export function merge(first: FlatConfig, ...rest: FlatConfig[]): FlatConfig { 8 | if (rest.length === 0) { 9 | return { ...first }; 10 | } 11 | const second = rest[0]; 12 | const merged = { 13 | ...first, 14 | ...second, 15 | settings: deepObjectMerge(first.settings ?? {}, second.settings ?? {}), 16 | linterOptions: { 17 | ...first?.linterOptions, 18 | ...second?.linterOptions, 19 | }, 20 | languageOptions: { 21 | ...first?.languageOptions, 22 | ...second?.languageOptions, 23 | globals: { 24 | ...first?.languageOptions?.globals, 25 | ...second?.languageOptions?.globals, 26 | }, 27 | parserOptions: deepObjectMerge( 28 | first?.languageOptions?.parserOptions ?? {}, 29 | second?.languageOptions?.parserOptions ?? {}, 30 | ), 31 | }, 32 | plugins: { 33 | ...first?.plugins, 34 | ...second?.plugins, 35 | }, 36 | rules: deepObjectMerge(first.rules ?? {}, second.rules ?? {}), 37 | } as const; 38 | 39 | // TODO: eslint-plugin-import-x requires `parserOptions.ecmaVersion` yet 40 | if (merged.languageOptions.ecmaVersion) { 41 | merged.languageOptions.parserOptions.ecmaVersion = 42 | merged.languageOptions.ecmaVersion; 43 | } 44 | 45 | if (rest.length > 1) { 46 | return merge(merged, rest[1], ...rest.slice(2)); 47 | } else { 48 | return merged; 49 | } 50 | } 51 | 52 | const overwriteMerge = (dest: any[], src: any[], options?: ArrayMergeOptions) => 53 | src; 54 | function deepObjectMerge(first: Partial, second: Partial): T { 55 | return deepmerge(first, second, { 56 | arrayMerge: overwriteMerge, 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /src/types/@eslint-community/eslint-plugin-eslint-comments.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@eslint-community/eslint-plugin-eslint-comments/configs" { 2 | import type { Linter } from "eslint"; 3 | 4 | interface Configs { 5 | recommended: { 6 | name: "@eslint-community/eslint-comments/recommended"; 7 | plugins: { 8 | "@eslint-community/eslint-comments": Linter.Plugin; 9 | }; 10 | rules: { 11 | "@eslint-community/eslint-comments/disable-enable-pair": "error"; 12 | "@eslint-community/eslint-comments/no-aggregating-enable": "error"; 13 | "@eslint-community/eslint-comments/no-duplicate-disable": "error"; 14 | "@eslint-community/eslint-comments/no-unlimited-disable": "error"; 15 | "@eslint-community/eslint-comments/no-unused-enable": "error"; 16 | }; 17 | }; 18 | } 19 | 20 | const configs: Configs; 21 | export default configs; 22 | } 23 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Assert that the given value is not null or undefined. 3 | */ 4 | export function nonNull(x: T): NonNullable { 5 | if (x == null) { 6 | throw new Error("Unexpected null or undefined"); 7 | } 8 | return x; 9 | } 10 | -------------------------------------------------------------------------------- /templates/eslint.config-cjs.mjs: -------------------------------------------------------------------------------- 1 | import { build } from "eslint-config-teppeis"; 2 | 3 | export default await build( 4 | { base: "node18", typescript: true }, 5 | { 6 | ignores: ["dist"], 7 | }, 8 | ); 9 | -------------------------------------------------------------------------------- /templates/eslint.config-esm.mjs: -------------------------------------------------------------------------------- 1 | import { build } from "eslint-config-teppeis"; 2 | 3 | export default await build( 4 | { base: "node18", typescript: true, esm: true }, 5 | { 6 | ignores: ["dist"], 7 | }, 8 | ); 9 | -------------------------------------------------------------------------------- /test/configs.mjs: -------------------------------------------------------------------------------- 1 | import { Linter } from "eslint"; 2 | import prettierConfig from "eslint-config-prettier"; 3 | import { globSync } from "glob"; 4 | import assert from "node:assert/strict"; 5 | import { readFile } from "node:fs/promises"; 6 | import path from "node:path"; 7 | import { fileURLToPath } from "node:url"; 8 | import { es2021 } from "../dist/configs/es2021.js"; 9 | import { es2022 } from "../dist/configs/es2022.js"; 10 | import { es2023 } from "../dist/configs/es2023.js"; 11 | import { typescriptTypeChecked } from "../dist/configs/typescript-type-checked.js"; 12 | import { typescript } from "../dist/configs/typescript.js"; 13 | import { merge } from "../dist/merge.js"; 14 | 15 | const __dirname = fileURLToPath(new URL(".", import.meta.url)); 16 | 17 | describe("es2021 config", () => { 18 | testConfig(es2021, "es2021"); 19 | }); 20 | 21 | describe("es2022 config", () => { 22 | testConfig(es2022, "es2022"); 23 | }); 24 | 25 | describe("es2023 config", () => { 26 | testConfig(es2022, "es2022"); 27 | 28 | it("should not enable rules that are overridden by prettier", () => { 29 | const es2023rules = new Set(Object.keys(es2023.rules)); 30 | const commonRules = Object.keys(prettierConfig.rules).filter( 31 | (rule) => es2023rules.has(rule) && isEnabledRule(es2023.rules[rule]), 32 | ); 33 | assert.deepEqual(commonRules, []); 34 | }); 35 | }); 36 | 37 | describe("typescript config", () => { 38 | testConfig(typescript, "typescript"); 39 | 40 | it("should not enable rules that are overridden by prettier", () => { 41 | const tsRules = new Set(Object.keys(typescript.rules)); 42 | const commonRules = Object.keys(prettierConfig.rules).filter( 43 | (rule) => tsRules.has(rule) && isEnabledRule(typescript.rules[rule]), 44 | ); 45 | assert.deepEqual(commonRules, []); 46 | }); 47 | }); 48 | 49 | describe("typescript-type-checked config", function () { 50 | this.timeout(10000); 51 | const config = merge(typescriptTypeChecked, { 52 | languageOptions: { 53 | parserOptions: { 54 | project: `${__dirname}fixtures/typescript-type-checked.tsconfig.json`, 55 | tsconfigRootDir: `${__dirname}fixtures`, 56 | }, 57 | }, 58 | }); 59 | testConfig(config, "typescript-type-checked"); 60 | 61 | it("should not enable rules that are overridden by prettier", () => { 62 | const tsRules = new Set(Object.keys(typescriptTypeChecked.rules)); 63 | const commonRules = Object.keys(prettierConfig.rules).filter( 64 | (rule) => 65 | tsRules.has(rule) && isEnabledRule(typescriptTypeChecked.rules[rule]), 66 | ); 67 | assert.deepEqual(commonRules, []); 68 | }); 69 | }); 70 | 71 | /** 72 | * @param {*} ruleLevel 0/1/2/"off"/"warn"/"error" or [0, ] 73 | * @return {boolean} 74 | */ 75 | function isEnabledRule(ruleLevel) { 76 | assert(ruleLevel != null); 77 | const level = Array.isArray(ruleLevel) ? ruleLevel[0] : ruleLevel; 78 | if (level === 0 || level === "off") { 79 | return false; 80 | } else if ( 81 | level === 1 || 82 | level === 2 || 83 | level === "warn" || 84 | level === "error" 85 | ) { 86 | return true; 87 | } else { 88 | throw new TypeError(`Unexpected rule level: ${ruleLevel}`); 89 | } 90 | } 91 | 92 | /** 93 | * @param {import("eslint").Linter.Config} config 94 | * @param {string} configName 95 | */ 96 | function testConfig(config, configName) { 97 | const fixtures = globSync( 98 | `${__dirname}/fixtures/${configName}.*.@(js|ts)`, 99 | ).sort(); 100 | for (const fixture of fixtures) { 101 | testFile(fixture, config); 102 | } 103 | } 104 | 105 | /** 106 | * @param {string} filePath 107 | * @param {import("eslint").Linter.Config} config 108 | */ 109 | async function testFile(filePath, config) { 110 | const match = /([^.]*)\.(pass|fail)\.(?:js|ts)$/.exec(filePath); 111 | if (!match) { 112 | throw new Error(`Invalid filePath: ${filePath}`); 113 | } 114 | // Support rules from plugins 115 | const ruleAndTestCase = match[1].split("%"); 116 | const rule = ruleAndTestCase[0].replaceAll("#", "/"); 117 | const testCase = ruleAndTestCase[1]; 118 | const expected = match[2]; 119 | 120 | it(`${rule}${testCase ? ` (${testCase})` : ""}: ${expected}`, async () => { 121 | const messages = await verify(filePath, config); 122 | const fatals = messages.filter((msg) => !!msg.fatal); 123 | if (fatals.length) { 124 | fatals.forEach((fatal) => { 125 | console.error( 126 | `${filePath}:${fatal.line}:${fatal.column} ${fatal.message}`, 127 | ); 128 | }); 129 | throw new Error("Fatal error"); 130 | } 131 | 132 | const messagesForTheRule = messages.filter((msg) => msg.ruleId === rule); 133 | if (expected === "pass" && messagesForTheRule.length > 0) { 134 | assert.fail(formatMessages(messagesForTheRule).join("\n")); 135 | } else if (expected === "fail" && messagesForTheRule.length === 0) { 136 | if (messages.length > 0) { 137 | assert.fail( 138 | `Passed the rule unexpectedly and failed other rules:\n${formatMessages( 139 | messages, 140 | ).join("\n")}`, 141 | ); 142 | } else { 143 | assert.fail("Passed the rule unexpectedly."); 144 | } 145 | } 146 | }); 147 | } 148 | 149 | /** 150 | * @param {string} file 151 | * @param {import("eslint").Linter.Config} config 152 | */ 153 | async function verify(file, config) { 154 | const linter = new Linter({ configType: "flat" }); 155 | const code = await readFile(file, "utf8"); 156 | return linter.verify(code, [config], path.basename(file)); 157 | } 158 | 159 | /** 160 | * @param {import('eslint').Linter.LintMessage[]} messages 161 | */ 162 | function formatMessages(messages) { 163 | return messages.map( 164 | (msg) => 165 | `${msg.line}:${msg.column} ${msg.message.slice(0, -1)} - ${msg.ruleId}`, 166 | ); 167 | } 168 | -------------------------------------------------------------------------------- /test/fixtures/es2021.@eslint-community#eslint-comments#no-duplicate-disable.fail.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable no-undef */ 2 | 3 | var foo = bar(); //eslint-disable-line no-undef 4 | -------------------------------------------------------------------------------- /test/fixtures/es2021.getter-return.fail.js: -------------------------------------------------------------------------------- 1 | // just recommended rule 2 | var p = { 3 | get name() { 4 | // no returns. 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /test/fixtures/es2021.jsdoc#check-tag-names.pass.js: -------------------------------------------------------------------------------- 1 | /** @type {number} */ 2 | var id = 1; 3 | -------------------------------------------------------------------------------- /test/fixtures/es2021.no-alert.pass.js: -------------------------------------------------------------------------------- 1 | // disabled rule 2 | alert("no warning"); 3 | -------------------------------------------------------------------------------- /test/fixtures/es2021.no-misleading-character-class.fail.js: -------------------------------------------------------------------------------- 1 | const jp = /^[🇯🇵]$/u; 2 | -------------------------------------------------------------------------------- /test/fixtures/es2021.object-shorthand.pass.js: -------------------------------------------------------------------------------- 1 | const foo = { 2 | foo: (bar, baz) => { 3 | return bar + baz; 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/es2021.prefer-destructuring.fail.js: -------------------------------------------------------------------------------- 1 | const foo = object.foo; 2 | -------------------------------------------------------------------------------- /test/fixtures/es2021.prefer-object-spread.fail.js: -------------------------------------------------------------------------------- 1 | const foo = { a: 1 }; 2 | const bar = Object.assign({}, foo); 3 | -------------------------------------------------------------------------------- /test/fixtures/es2021.unicorn#no-hex-escape.fail.js: -------------------------------------------------------------------------------- 1 | var foo = "\x1B"; 2 | -------------------------------------------------------------------------------- /test/fixtures/es2021.unicorn#prefer-string-starts-ends-with.fail.js: -------------------------------------------------------------------------------- 1 | /^bar/.test(foo); 2 | -------------------------------------------------------------------------------- /test/fixtures/es2022.prefer-object-has-own.fail.js: -------------------------------------------------------------------------------- 1 | // es2023 doesn't introduce new rules. so run same test as es2022 2 | 3 | Object.prototype.hasOwnProperty.call(obj, "a"); 4 | -------------------------------------------------------------------------------- /test/fixtures/es2023.prefer-object-has-own.fail.js: -------------------------------------------------------------------------------- 1 | Object.prototype.hasOwnProperty.call(obj, "a"); 2 | -------------------------------------------------------------------------------- /test/fixtures/modules/default-export.ts: -------------------------------------------------------------------------------- 1 | export default "foo"; 2 | -------------------------------------------------------------------------------- /test/fixtures/modules/named-export-foo.ts: -------------------------------------------------------------------------------- 1 | export const foo = "foo!"; 2 | -------------------------------------------------------------------------------- /test/fixtures/typescript-type-checked.@typescript-eslint#no-floating-promises.fail.ts: -------------------------------------------------------------------------------- 1 | // A rule that requires type information is effective. 2 | Promise.reject("value").catch(); 3 | -------------------------------------------------------------------------------- /test/fixtures/typescript-type-checked.@typescript-eslint#no-unnecessary-type-assertion.fail.ts: -------------------------------------------------------------------------------- 1 | // A rule that requires type information is effective. 2 | const foo = "foo" as const; 3 | -------------------------------------------------------------------------------- /test/fixtures/typescript-type-checked.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Node 18", 4 | "compilerOptions": { 5 | "lib": ["es2023"], 6 | "module": "ESNext", 7 | "moduleResolution": "Node16", 8 | "target": "es2022", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/typescript.@typescript-eslint#no-duplicate-enum-values.fail.ts: -------------------------------------------------------------------------------- 1 | // Test that @typescript-eslint:recommended is loaded. 2 | 3 | enum E { 4 | A = 0, 5 | B = 0, 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/typescript.@typescript-eslint#no-namespace.fail.ts: -------------------------------------------------------------------------------- 1 | // Test that custom rule settings are loaded. 2 | 3 | namespace foo {} 4 | -------------------------------------------------------------------------------- /test/fixtures/typescript.@typescript-eslint#no-unnecessary-type-assertion.pass.ts: -------------------------------------------------------------------------------- 1 | // This rule is disabled in `typescript` config, because it requires type information. 2 | // It is enabled in `typescript-type-checked` config. 3 | const foo = 3; 4 | const bar = foo!; 5 | -------------------------------------------------------------------------------- /test/fixtures/typescript.import-x#first.fail.ts: -------------------------------------------------------------------------------- 1 | // Test that module-base and eslint-plugin-import are loaded. 2 | 3 | console.log("some statements before import"); 4 | import { foo } from "./modules/named-export-foo"; 5 | -------------------------------------------------------------------------------- /test/fixtures/typescript.no-dupe-keys.pass.ts: -------------------------------------------------------------------------------- 1 | // Test that @typescript-eslint:eslint-recommended is loaded. 2 | 3 | const foo = { 4 | bar: "baz", 5 | bar: "qux", 6 | }; 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "lib": ["es2023"], 5 | "module": "node16", 6 | "moduleResolution": "node16", 7 | "target": "es2022", 8 | "allowJs": true, 9 | "outDir": "dist", 10 | "declaration": true, 11 | "strict": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true 14 | }, 15 | "include": ["src"] 16 | } 17 | --------------------------------------------------------------------------------