From 88ec9f66e1187cfcc0dba2cc3ff3fefffcec84a3 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Fri, 8 Nov 2024 15:15:13 +1300 Subject: [PATCH] feat: convert configs to flat style BREAKING CHANGE: configs are now flat by default, unless `ESLINT_USE_FLAT_CONFIG` is `'false'` --- @typescript-eslint.js | 448 ++++++++++++++++------- README.md | 171 ++++++++- configs.d.ts | 14 +- eslint.config.js | 27 +- index.js | 632 ++++++++++++++++++++++----------- jest.js | 171 ++++++--- package-lock.json | 137 +------ package.json | 5 +- react.js | 271 ++++++++++---- test/configs.spec.ts | 12 +- tools/generate-configs-list.ts | 8 +- types.d.ts | 122 +++++++ 12 files changed, 1385 insertions(+), 633 deletions(-) create mode 100644 types.d.ts diff --git a/@typescript-eslint.js b/@typescript-eslint.js index f7b5086c..6ce57dc6 100644 --- a/@typescript-eslint.js +++ b/@typescript-eslint.js @@ -1,141 +1,319 @@ -/** @type {import('eslint').Linter.Config} */ -const config = { - parser: '@typescript-eslint/parser', - parserOptions: { sourceType: 'module' }, - plugins: ['@typescript-eslint', '@stylistic/ts', 'prettier'], - extends: [ - 'plugin:@typescript-eslint/recommended-type-checked', - 'plugin:@typescript-eslint/stylistic-type-checked', - 'plugin:prettier/recommended' - ], - rules: { - // explicitly (re)enable this as it's disabled by eslint-config-prettier - // and its likely our standard JS config will be used alongside this one - 'curly': 'error', +/** + * Generates an ESLint config for TypeScript, based on the Ackama style guide + * + * @return {import('eslint').Linter.FlatConfig|import('eslint').Linter.LegacyConfig} + */ +const generateConfig = () => { + if (process.env.ESLINT_USE_FLAT_CONFIG !== 'false') { + /* eslint-disable n/global-require */ + const pluginStylisticTS = require('@stylistic/eslint-plugin-ts'); + const pluginTypeScriptESLint = require('@typescript-eslint/eslint-plugin'); + const parserTypeScriptESLint = require('@typescript-eslint/parser'); + const pluginPrettier = require('eslint-plugin-prettier'); + const pluginPrettierRecommended = require('eslint-plugin-prettier/recommended'); + /* eslint-enable n/global-require */ - '@typescript-eslint/array-type': ['error', { default: 'array-simple' }], - '@typescript-eslint/default-param-last': 'error', - '@typescript-eslint/explicit-member-accessibility': 'error', - '@typescript-eslint/explicit-module-boundary-types': 'error', - '@stylistic/ts/lines-between-class-members': [ - 'error', - 'always', - { exceptAfterSingleLine: true } - ], - '@typescript-eslint/naming-convention': [ - 'error', - { - selector: 'default', - format: ['camelCase', 'PascalCase', 'UPPER_CASE'] - }, - { selector: 'property', format: null }, - { selector: 'typeLike', format: ['PascalCase'] }, - { - selector: 'typeParameter', - format: ['PascalCase'], - custom: { match: true, regex: /^T([A-Z][a-zA-Z]+)$|^[A-Z]$/u.source } - }, - { selector: 'enumMember', format: ['PascalCase', 'UPPER_CASE'] }, - { - selector: 'interface', - format: ['PascalCase'], // disallow "I" prefixing, but allow names like "IAM" - custom: { match: false, regex: /^I[A-Z][a-z]/u.source } - }, - { - selector: 'parameter', - format: ['camelCase'], - leadingUnderscore: 'allow' - }, - { - selector: 'memberLike', - modifiers: ['private'], - format: ['PascalCase', 'camelCase'], - leadingUnderscore: 'require' + /** @type {import('eslint').Linter.FlatConfig} */ + const config = { + languageOptions: { parser: parserTypeScriptESLint }, + plugins: { + '@typescript-eslint': pluginTypeScriptESLint, + '@stylistic/ts': pluginStylisticTS, + 'prettier': pluginPrettier }, - { - selector: 'memberLike', - modifiers: ['protected'], - format: ['PascalCase', 'camelCase'], - leadingUnderscore: 'require' - }, - { - selector: 'memberLike', - modifiers: ['public'], - format: ['PascalCase', 'camelCase'], - leadingUnderscore: 'forbid' - } - ], - '@typescript-eslint/no-confusing-void-expression': [ - 'error', - { ignoreArrowShorthand: true } - ], - '@typescript-eslint/no-dupe-class-members': 'error', - '@typescript-eslint/no-dynamic-delete': 'error', - '@typescript-eslint/no-extraneous-class': 'error', - '@typescript-eslint/no-invalid-this': 'error', - '@typescript-eslint/no-loop-func': 'error', - '@typescript-eslint/no-meaningless-void-operator': 'error', - '@typescript-eslint/no-mixed-enums': 'error', - '@typescript-eslint/no-non-null-assertion': 'error', - '@typescript-eslint/no-namespace': [ - 'off', // todo: need to audit existing codebase to see if declare is fine - { - allowDeclarations: true, - allowDefinitionFiles: true + rules: { + ...pluginTypeScriptESLint.configs['recommended-type-checked'].rules, + ...pluginTypeScriptESLint.configs['stylistic-type-checked'].rules, + ...pluginPrettierRecommended.rules, + + // explicitly (re)enable this as it's disabled by eslint-config-prettier + // and its likely our standard JS config will be used alongside this one + 'curly': 'error', + + '@typescript-eslint/array-type': ['error', { default: 'array-simple' }], + '@typescript-eslint/default-param-last': 'error', + '@typescript-eslint/explicit-member-accessibility': 'error', + '@typescript-eslint/explicit-module-boundary-types': 'error', + '@stylistic/ts/lines-between-class-members': [ + 'error', + 'always', + { exceptAfterSingleLine: true } + ], + '@typescript-eslint/naming-convention': [ + 'error', + { + selector: 'default', + format: ['camelCase', 'PascalCase', 'UPPER_CASE'] + }, + { selector: 'property', format: null }, + { selector: 'typeLike', format: ['PascalCase'] }, + { + selector: 'typeParameter', + format: ['PascalCase'], + custom: { + match: true, + regex: /^T([A-Z][a-zA-Z]+)$|^[A-Z]$/u.source + } + }, + { selector: 'enumMember', format: ['PascalCase', 'UPPER_CASE'] }, + { + selector: 'interface', + format: ['PascalCase'], // disallow "I" prefixing, but allow names like "IAM" + custom: { match: false, regex: /^I[A-Z][a-z]/u.source } + }, + { + selector: 'parameter', + format: ['camelCase'], + leadingUnderscore: 'allow' + }, + { + selector: 'memberLike', + modifiers: ['private'], + format: ['PascalCase', 'camelCase'], + leadingUnderscore: 'require' + }, + { + selector: 'memberLike', + modifiers: ['protected'], + format: ['PascalCase', 'camelCase'], + leadingUnderscore: 'require' + }, + { + selector: 'memberLike', + modifiers: ['public'], + format: ['PascalCase', 'camelCase'], + leadingUnderscore: 'forbid' + } + ], + '@typescript-eslint/no-confusing-void-expression': [ + 'error', + { ignoreArrowShorthand: true } + ], + '@typescript-eslint/no-dupe-class-members': 'error', + '@typescript-eslint/no-dynamic-delete': 'error', + '@typescript-eslint/no-extraneous-class': 'error', + '@typescript-eslint/no-invalid-this': 'error', + '@typescript-eslint/no-loop-func': 'error', + '@typescript-eslint/no-meaningless-void-operator': 'error', + '@typescript-eslint/no-mixed-enums': 'error', + '@typescript-eslint/no-non-null-assertion': 'error', + '@typescript-eslint/no-namespace': [ + 'off', // todo: need to audit existing codebase to see if declare is fine + { + allowDeclarations: true, + allowDefinitionFiles: true + } + ], + '@typescript-eslint/no-redeclare': 'error', + '@typescript-eslint/no-require-imports': 'error', + '@typescript-eslint/no-shadow': 'warn', + '@typescript-eslint/no-this-alias': [ + 'error', + { allowDestructuring: true } + ], + '@typescript-eslint/no-throw-literal': 'error', + '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error', + '@typescript-eslint/no-unnecessary-condition': 'error', + '@typescript-eslint/no-unnecessary-qualifier': 'error', + '@typescript-eslint/no-unnecessary-type-arguments': 'error', + '@typescript-eslint/no-unused-expressions': 'error', + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_' } + ], + '@typescript-eslint/no-use-before-define': [ + 'error', // Purely stylistic b/c of TS + { typedefs: false, variables: false } + ], + '@typescript-eslint/no-useless-constructor': 'error', + '@typescript-eslint/parameter-properties': 'error', + '@typescript-eslint/prefer-includes': 'error', + '@typescript-eslint/prefer-readonly': 'warn', + '@typescript-eslint/prefer-reduce-type-parameter': 'error', + '@typescript-eslint/prefer-string-starts-ends-with': 'warn', + '@typescript-eslint/prefer-ts-expect-error': 'error', + '@typescript-eslint/promise-function-async': 'error', + '@typescript-eslint/require-array-sort-compare': 'warn', + '@typescript-eslint/sort-type-constituents': 'error', + '@typescript-eslint/switch-exhaustiveness-check': 'error', + '@typescript-eslint/unified-signatures': 'warn', // can be a bit wrong + 'array-callback-return': 'off', + 'block-scoped-var': 'off', + 'camelcase': 'off', + 'consistent-return': 'off', // via --noImplicitReturns + 'default-param-last': 'off', + 'dot-notation': 'off', // @typescript-eslint + 'guard-for-in': 'off', + 'init-declarations': 'off', // handled by TS & --noImplicitAny + 'lines-between-class-members': 'off', + '@stylistic/js/lines-between-class-members': 'off', + 'no-dupe-class-members': 'off', // @typescript-eslint + 'no-import-assign': 'off', + 'no-invalid-this': 'off', // @typescript-eslint + 'no-iterator': 'off', + 'no-loop-func': 'off', // @typescript-eslint + 'no-proto': 'off', // TS2339 + 'no-setter-return': 'off', // TS2408 + 'no-shadow': 'off', // @typescript-eslint + 'no-throw-literal': 'off', // @typescript-eslint + 'no-underscore-dangle': 'off', + 'no-unused-expressions': 'off', + 'no-use-before-define': 'off', + 'no-useless-constructor': 'off', // @typescript-eslint + 'strict': 'off' // via --alwaysStrict } - ], - '@typescript-eslint/no-redeclare': 'error', - '@typescript-eslint/no-require-imports': 'error', - '@typescript-eslint/no-shadow': 'warn', - '@typescript-eslint/no-this-alias': ['error', { allowDestructuring: true }], - '@typescript-eslint/no-throw-literal': 'error', - '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error', - '@typescript-eslint/no-unnecessary-condition': 'error', - '@typescript-eslint/no-unnecessary-qualifier': 'error', - '@typescript-eslint/no-unnecessary-type-arguments': 'error', - '@typescript-eslint/no-unused-expressions': 'error', - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], - '@typescript-eslint/no-use-before-define': [ - 'error', // Purely stylistic b/c of TS - { typedefs: false, variables: false } - ], - '@typescript-eslint/no-useless-constructor': 'error', - '@typescript-eslint/parameter-properties': 'error', - '@typescript-eslint/prefer-includes': 'error', - '@typescript-eslint/prefer-readonly': 'warn', - '@typescript-eslint/prefer-reduce-type-parameter': 'error', - '@typescript-eslint/prefer-string-starts-ends-with': 'warn', - '@typescript-eslint/prefer-ts-expect-error': 'error', - '@typescript-eslint/promise-function-async': 'error', - '@typescript-eslint/require-array-sort-compare': 'warn', - '@typescript-eslint/sort-type-constituents': 'error', - '@typescript-eslint/switch-exhaustiveness-check': 'error', - '@typescript-eslint/unified-signatures': 'warn', // can be a bit wrong - 'array-callback-return': 'off', - 'block-scoped-var': 'off', - 'camelcase': 'off', - 'consistent-return': 'off', // via --noImplicitReturns - 'default-param-last': 'off', - 'dot-notation': 'off', // @typescript-eslint - 'guard-for-in': 'off', - 'init-declarations': 'off', // handled by TS & --noImplicitAny - 'lines-between-class-members': 'off', - '@stylistic/js/lines-between-class-members': 'off', - 'no-dupe-class-members': 'off', // @typescript-eslint - 'no-import-assign': 'off', - 'no-invalid-this': 'off', // @typescript-eslint - 'no-iterator': 'off', - 'no-loop-func': 'off', // @typescript-eslint - 'no-proto': 'off', // TS2339 - 'no-setter-return': 'off', // TS2408 - 'no-shadow': 'off', // @typescript-eslint - 'no-throw-literal': 'off', // @typescript-eslint - 'no-underscore-dangle': 'off', - 'no-unused-expressions': 'off', - 'no-use-before-define': 'off', - 'no-useless-constructor': 'off', // @typescript-eslint - 'strict': 'off' // via --alwaysStrict + }; + + return config; } + + /** @type {import('eslint').Linter.LegacyConfig} */ + const config = { + parser: '@typescript-eslint/parser', + parserOptions: { sourceType: 'module' }, + plugins: ['@typescript-eslint', '@stylistic/ts', 'prettier'], + extends: [ + 'plugin:@typescript-eslint/recommended-type-checked', + 'plugin:@typescript-eslint/stylistic-type-checked', + 'plugin:prettier/recommended' + ], + rules: { + // explicitly (re)enable this as it's disabled by eslint-config-prettier + // and its likely our standard JS config will be used alongside this one + 'curly': 'error', + + '@typescript-eslint/array-type': ['error', { default: 'array-simple' }], + '@typescript-eslint/default-param-last': 'error', + '@typescript-eslint/explicit-member-accessibility': 'error', + '@typescript-eslint/explicit-module-boundary-types': 'error', + '@stylistic/ts/lines-between-class-members': [ + 'error', + 'always', + { exceptAfterSingleLine: true } + ], + '@typescript-eslint/naming-convention': [ + 'error', + { + selector: 'default', + format: ['camelCase', 'PascalCase', 'UPPER_CASE'] + }, + { selector: 'property', format: null }, + { selector: 'typeLike', format: ['PascalCase'] }, + { + selector: 'typeParameter', + format: ['PascalCase'], + custom: { match: true, regex: /^T([A-Z][a-zA-Z]+)$|^[A-Z]$/u.source } + }, + { selector: 'enumMember', format: ['PascalCase', 'UPPER_CASE'] }, + { + selector: 'interface', + format: ['PascalCase'], // disallow "I" prefixing, but allow names like "IAM" + custom: { match: false, regex: /^I[A-Z][a-z]/u.source } + }, + { + selector: 'parameter', + format: ['camelCase'], + leadingUnderscore: 'allow' + }, + { + selector: 'memberLike', + modifiers: ['private'], + format: ['PascalCase', 'camelCase'], + leadingUnderscore: 'require' + }, + { + selector: 'memberLike', + modifiers: ['protected'], + format: ['PascalCase', 'camelCase'], + leadingUnderscore: 'require' + }, + { + selector: 'memberLike', + modifiers: ['public'], + format: ['PascalCase', 'camelCase'], + leadingUnderscore: 'forbid' + } + ], + '@typescript-eslint/no-confusing-void-expression': [ + 'error', + { ignoreArrowShorthand: true } + ], + '@typescript-eslint/no-dupe-class-members': 'error', + '@typescript-eslint/no-dynamic-delete': 'error', + '@typescript-eslint/no-extraneous-class': 'error', + '@typescript-eslint/no-invalid-this': 'error', + '@typescript-eslint/no-loop-func': 'error', + '@typescript-eslint/no-meaningless-void-operator': 'error', + '@typescript-eslint/no-mixed-enums': 'error', + '@typescript-eslint/no-non-null-assertion': 'error', + '@typescript-eslint/no-namespace': [ + 'off', // todo: need to audit existing codebase to see if declare is fine + { + allowDeclarations: true, + allowDefinitionFiles: true + } + ], + '@typescript-eslint/no-redeclare': 'error', + '@typescript-eslint/no-require-imports': 'error', + '@typescript-eslint/no-shadow': 'warn', + '@typescript-eslint/no-this-alias': [ + 'error', + { allowDestructuring: true } + ], + '@typescript-eslint/no-throw-literal': 'error', + '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error', + '@typescript-eslint/no-unnecessary-condition': 'error', + '@typescript-eslint/no-unnecessary-qualifier': 'error', + '@typescript-eslint/no-unnecessary-type-arguments': 'error', + '@typescript-eslint/no-unused-expressions': 'error', + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_' } + ], + '@typescript-eslint/no-use-before-define': [ + 'error', // Purely stylistic b/c of TS + { typedefs: false, variables: false } + ], + '@typescript-eslint/no-useless-constructor': 'error', + '@typescript-eslint/parameter-properties': 'error', + '@typescript-eslint/prefer-includes': 'error', + '@typescript-eslint/prefer-readonly': 'warn', + '@typescript-eslint/prefer-reduce-type-parameter': 'error', + '@typescript-eslint/prefer-string-starts-ends-with': 'warn', + '@typescript-eslint/prefer-ts-expect-error': 'error', + '@typescript-eslint/promise-function-async': 'error', + '@typescript-eslint/require-array-sort-compare': 'warn', + '@typescript-eslint/sort-type-constituents': 'error', + '@typescript-eslint/switch-exhaustiveness-check': 'error', + '@typescript-eslint/unified-signatures': 'warn', // can be a bit wrong + 'array-callback-return': 'off', + 'block-scoped-var': 'off', + 'camelcase': 'off', + 'consistent-return': 'off', // via --noImplicitReturns + 'default-param-last': 'off', + 'dot-notation': 'off', // @typescript-eslint + 'guard-for-in': 'off', + 'init-declarations': 'off', // handled by TS & --noImplicitAny + 'lines-between-class-members': 'off', + '@stylistic/js/lines-between-class-members': 'off', + 'no-dupe-class-members': 'off', // @typescript-eslint + 'no-import-assign': 'off', + 'no-invalid-this': 'off', // @typescript-eslint + 'no-iterator': 'off', + 'no-loop-func': 'off', // @typescript-eslint + 'no-proto': 'off', // TS2339 + 'no-setter-return': 'off', // TS2408 + 'no-shadow': 'off', // @typescript-eslint + 'no-throw-literal': 'off', // @typescript-eslint + 'no-underscore-dangle': 'off', + 'no-unused-expressions': 'off', + 'no-use-before-define': 'off', + 'no-useless-constructor': 'off', // @typescript-eslint + 'strict': 'off' // via --alwaysStrict + } + }; + + return config; }; -module.exports = config; +module.exports = generateConfig(); diff --git a/README.md b/README.md index 997ae1e0..303ad04c 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,31 @@ Install this package & the required plugins: npm install --save-dev eslint-config-ackama @types/eslint @eslint-community/eslint-plugin-eslint-comments @stylistic/eslint-plugin-js eslint eslint-plugin-import eslint-plugin-n eslint-plugin-prettier prettier -Add an `.eslintrc.js` to your repo that extends from this config: +Add an `eslint.config.js` to your repo that imports this config: ```js -/** @type {import('eslint').Linter.Config} */ +const configAckamaBase = require('eslint-config-ackama'); + +/** @type {import('eslint').Linter.FlatConfig[]} */ +const config = [ + { files: ['**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts}'] }, + /** @type {import('eslint').Linter.FlatConfig} */ (configAckamaBase) +]; + +module.exports = config; +``` + +By default, the configurations use the +["flat config"](https://eslint.org/blog/2022/08/new-config-system-part-2/) +system introduced in ESLint 8.x. This allows for a more flexible and powerful +configuration system, but it does require a bit more boilerplate to set up. + +You can set the `ESLINT_USE_FLAT_CONFIG` environment variable to `false` if you +wish to have the configurations use the legacy format for use with +`.eslintrc.js`: + +```js +/** @type {import('eslint').Linter.LegacyConfig} */ const config = { extends: ['ackama'] }; @@ -19,24 +40,79 @@ const config = { module.exports = config; ``` -You'll want to tell ESLint about the environment you're working in using the -[`env`](https://eslint.org/docs/user-guide/configuring#specifying-environments) -toplevel property. +> [!NOTE] +> +> This environment variable is also what tells ESLint v9 to use the legacy +> configuration format. To reduce potential errors, the configurations provided by this package -deliberately avoid enabling or disabling any envs without good reason, opting to -set only the `es2017` env, since the majority of projects should be using ES2017 -or higher. +deliberately avoid making assumptions about the environment you're working in, +meaning you will need to configure what globals are available using the +`globals` package: + +```js +const configAckamaBase = require('eslint-config-ackama'); +const globals = require('globals'); + +/** @type {import('eslint').Linter.FlatConfig[]} */ +const config = [ + { files: ['**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts}'] }, + /** @type {import('eslint').Linter.FlatConfig} */ (configAckamaBase), + { + languageOptions: { + globals: { + ...globals.node, // for NodeJS apps + ...globals.browser, // for browser apps + ...globals.commonjs // for browser apps that are bundled using a bundler such as webpack + } + } + } +]; -These are the three most common envs you'll want to use: +module.exports = config; +``` -- `node` for NodeJS apps -- `browser` for browser apps -- `commonjs` for browser apps that are bundled using a bundler such as webpack +If you're using an `.eslintrc.js`, you want to use the +[`env`](https://eslint.org/docs/user-guide/configuring#specifying-environments) +toplevel property: + +```js +/** @type {import('eslint').Linter.LegacyConfig} */ +const config = { + extends: ['ackama'], + env: { + node: true, // for NodeJS apps + browser: true, // for browser apps + commonjs: true // for browser apps that are bundled using a bundler such as webpack + } +}; + +module.exports = config; +``` + +> [!NOTE] +> +> The legacy configuration sets the `es2017` env by default, since the majority +> of projects should be using ES2017 or higher. +> +> The equivalent to this in the flat configuration format is the +> `languageOptions.ecmaVersion` property, which defaults to `latest` meaning you +> don't need to set it unless you're using a different version of ECMAScript. You can also add a `lint` script to the `scripts` property in your apps `package.json` to make it easier for developers to run eslint against the app: +```json +{ + "scripts": { + "lint": "eslint" + } +} +``` + +If you're using the legacy configuration format, you will also need to specify a +file or directory along with the file extensions you want to lint: + ```json { "scripts": { @@ -92,13 +168,34 @@ base config already setups ignores for common folders, including `node_modules`, to ignore additional folders, or inversely might want to un-ignore a preset ignore. -This can be done using the +This can be done by +[including a configuration object with just an `ignores` key](https://eslint.org/docs/latest/use/configure/ignore), +making it act as a global ignore: + +```js +const configAckamaBase = require('eslint-config-ackama'); + +/** @type {import('eslint').Linter.FlatConfig[]} */ +const config = [ + { files: ['**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts}'] }, + { ignores: ['infra'] }, + /** @type {import('eslint').Linter.FlatConfig} */ (configAckamaBase) +]; + +module.exports = config; +``` + +> [!NOTE] +> +> The flat configuration system does not support `.eslintignore` + +For legacy configurations, this can be done using the [`ignorePatterns`](https://eslint.org/docs/user-guide/configuring#ignorepatterns-in-config-files) toplevel property, which is an array that accepts `.gitignore` glob-like strings: ```js -/** @type {import('eslint').Linter.Config} */ +/** @type {import('eslint').Linter.LegacyConfig} */ const config = { ignorePatterns: ['!public/', 'tmp/'] }; @@ -108,16 +205,58 @@ module.exports = config; ### Typical complete example +Here's what a typical `eslint.config.js` would look like for a TypeScript +project that uses `jest` & `react`: + +```js +const configAckamaBase = require('eslint-config-ackama'); +const configAckamaTypeScript = require('eslint-config-ackama/@typescript-eslint'); +const configAckamaJest = require('eslint-config-ackama/jest'); +const configAckamaReact = require('eslint-config-ackama/react'); +const globals = require('globals'); + +/** @type {import('eslint').Linter.FlatConfig[]} */ +const config = [ + { files: ['**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts}'] }, + { ignores: ['infra'] }, + /** @type {import('eslint').Linter.FlatConfig} */ (configAckamaBase), + /** @type {import('eslint').Linter.FlatConfig} */ (configAckamaTypeScript), + { + languageOptions: { + parserOptions: { project: true }, + globals: globals.commonjs + } + }, + /** @type {import('eslint').Linter.FlatConfig} */ (configAckamaReact), + ...[ + /** @type {import('eslint').Linter.FlatConfig} */ (configAckamaJest), + /** @type {import('eslint').Linter.FlatConfig} */ ({ + rules: { 'jest/prefer-expect-assertions': 'off' } + }) + ].map(c => ({ ...c, files: ['test/**'] })), + { + files: ['**/*.js'], + languageOptions: { sourceType: 'script' }, + rules: { + '@typescript-eslint/no-require-imports': 'off', + '@typescript-eslint/no-var-requires': 'off' + } + } +]; + +module.exports = config; +``` + Here's what a typical `.eslintrc.js` would look like for a TypeScript project that uses `jest` & `react`: ```js -/** @type {import('eslint').Linter.Config} */ +/** @type {import('eslint').Linter.LegacyConfig} */ const config = { root: true, parser: '@typescript-eslint/parser', parserOptions: { - project: 'tsconfig.json', + project: true, ecmaVersion: 2019, sourceType: 'module' }, diff --git a/configs.d.ts b/configs.d.ts index e1d7e6ca..62f3c0e0 100644 --- a/configs.d.ts +++ b/configs.d.ts @@ -1,7 +1,7 @@ declare module 'eslint-config-ackama' { import type { Linter } from 'eslint'; - const config: Linter.Config; + const config: Linter.LegacyConfig | Linter.FlatConfig; export = config; } @@ -9,7 +9,7 @@ declare module 'eslint-config-ackama' { declare module 'eslint-config-ackama/@typescript-eslint' { import type { Linter } from 'eslint'; - const config: Linter.Config; + const config: Linter.LegacyConfig | Linter.FlatConfig; export = config; } @@ -17,7 +17,7 @@ declare module 'eslint-config-ackama/@typescript-eslint' { declare module 'eslint-config-ackama/@typescript-eslint.js' { import type { Linter } from 'eslint'; - const config: Linter.Config; + const config: Linter.LegacyConfig | Linter.FlatConfig; export = config; } @@ -25,7 +25,7 @@ declare module 'eslint-config-ackama/@typescript-eslint.js' { declare module 'eslint-config-ackama/jest' { import type { Linter } from 'eslint'; - const config: Linter.Config; + const config: Linter.LegacyConfig | Linter.FlatConfig; export = config; } @@ -33,7 +33,7 @@ declare module 'eslint-config-ackama/jest' { declare module 'eslint-config-ackama/jest.js' { import type { Linter } from 'eslint'; - const config: Linter.Config; + const config: Linter.LegacyConfig | Linter.FlatConfig; export = config; } @@ -41,7 +41,7 @@ declare module 'eslint-config-ackama/jest.js' { declare module 'eslint-config-ackama/react' { import type { Linter } from 'eslint'; - const config: Linter.Config; + const config: Linter.LegacyConfig | Linter.FlatConfig; export = config; } @@ -49,7 +49,7 @@ declare module 'eslint-config-ackama/react' { declare module 'eslint-config-ackama/react.js' { import type { Linter } from 'eslint'; - const config: Linter.Config; + const config: Linter.LegacyConfig | Linter.FlatConfig; export = config; } diff --git a/eslint.config.js b/eslint.config.js index 4c4f5760..d09cf667 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,31 +1,26 @@ -const { FlatCompat } = require('@eslint/eslintrc'); -const js = require('@eslint/js'); const globals = require('globals'); const configAckamaTypeScript = require('./@typescript-eslint'); const configAckamaBase = require('./index'); const configAckamaJest = require('./jest'); -const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended -}); - /** @type {import('eslint').Linter.FlatConfig[]} */ const config = [ { files: ['**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts}'] }, - ...compat.config(configAckamaBase), - ...compat.config(configAckamaTypeScript), - { languageOptions: { parserOptions: { project: true } } }, + /** @type {import('eslint').Linter.FlatConfig} */ (configAckamaBase), + /** @type {import('eslint').Linter.FlatConfig} */ (configAckamaTypeScript), + { + languageOptions: { + parserOptions: { project: true }, + globals: globals.node + } + }, { - files: ['*.spec.*'], - ...compat.config(configAckamaJest) + files: ['**/*.spec.*'], + .../** @type {import('eslint').Linter.FlatConfig} */ (configAckamaJest) }, { files: ['**/*.js'], - languageOptions: { - sourceType: 'script', - globals: globals.node - }, + languageOptions: { sourceType: 'script' }, rules: { '@typescript-eslint/no-require-imports': 'off', '@typescript-eslint/no-var-requires': 'off' diff --git a/index.js b/index.js index 1e57ad63..fc58fcf0 100644 --- a/index.js +++ b/index.js @@ -1,208 +1,438 @@ -/** @type {import('eslint').Linter.Config} */ -const config = { - env: { es2017: true }, - plugins: [ - '@eslint-community/eslint-comments', - '@stylistic/js', - 'prettier', // - 'import', - 'n' - ], - extends: [ - 'eslint:recommended', - 'plugin:prettier/recommended', - 'plugin:@eslint-community/eslint-comments/recommended', - 'plugin:prettier/recommended' - ], - ignorePatterns: [ - '!.eslintrc.js', - 'node_modules/*', - 'coverage/*', - 'bundle/*', - 'public/*', - 'vendor/*', - 'dist/*', - 'lib/*', - 'out/*' - ], - rules: { - '@eslint-community/eslint-comments/disable-enable-pair': [ - 'error', - { allowWholeFile: true } - ], - '@eslint-community/eslint-comments/no-unused-disable': 'error', - 'import/export': 'error', - 'import/no-absolute-path': 'error', - 'import/no-anonymous-default-export': 'error', - 'import/no-mutable-exports': 'error', - 'import/no-self-import': 'error', - 'import/no-webpack-loader-syntax': 'error', - 'import/order': [ - 'error', - { - 'alphabetize': { order: 'asc' }, - 'groups': [ - ['builtin', 'external', 'internal', 'unknown'], - ['parent', 'sibling', 'index'] - ], - 'newlines-between': 'never' - } - ], - 'n/callback-return': 'warn', - 'n/global-require': 'error', - 'n/no-deprecated-api': 'error', - 'n/no-mixed-requires': 'error', - 'n/no-new-require': 'error', - 'n/no-path-concat': 'error', - 'n/no-process-exit': 'error', - 'n/no-sync': 'warn', - 'accessor-pairs': ['error', { enforceForClassMembers: true }], - 'array-callback-return': 'error', - 'block-scoped-var': 'warn', - 'camelcase': ['error', { allow: ['child_process'] }], - 'consistent-return': 'error', - 'consistent-this': 'error', - 'curly': 'error', - 'default-case': 'error', - 'default-param-last': 'error', - 'dot-notation': 'error', - 'eqeqeq': 'error', - 'func-names': ['error', 'as-needed'], - 'grouped-accessor-pairs': ['error', 'getBeforeSet'], - 'guard-for-in': 'error', - 'init-declarations': 'error', - '@stylistic/js/lines-between-class-members': [ - 'error', - 'always', - { exceptAfterSingleLine: true } - ], - 'max-classes-per-file': ['error', 1], - 'new-cap': ['error', { capIsNewExceptions: ['ESLintUtils.RuleCreator'] }], - 'no-alert': 'warn', - 'no-array-constructor': 'error', - 'no-await-in-loop': 'error', - 'no-bitwise': 'error', - 'no-caller': 'error', - 'no-constructor-return': 'error', - 'no-dupe-else-if': 'error', - 'no-else-return': 'error', - 'no-empty-function': 'error', - 'no-eq-null': 'error', - 'no-eval': 'error', - 'no-extend-native': 'error', - 'no-extra-bind': 'error', - 'no-extra-label': 'error', - 'no-implicit-globals': 'error', - 'no-implied-eval': 'error', - 'no-import-assign': 'error', - 'no-invalid-this': 'error', - 'no-iterator': 'error', - 'no-label-var': 'error', - 'no-labels': 'error', - 'no-lone-blocks': 'error', - 'no-lonely-if': 'error', - 'no-loop-func': 'error', - 'no-multi-assign': 'error', - 'no-multi-str': 'error', - 'no-nested-ternary': 'error', - 'no-new': 'error', - 'no-new-func': 'error', - 'no-new-object': 'error', - 'no-new-wrappers': 'error', - 'no-octal-escape': 'error', - 'no-param-reassign': 'error', - 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }], - 'no-promise-executor-return': 'error', - 'no-proto': 'error', - 'no-return-assign': 'error', - 'no-return-await': 'error', - 'no-script-url': 'error', - 'no-self-compare': 'error', - 'no-sequences': 'error', - 'no-setter-return': 'error', - 'no-shadow': 'warn', - 'no-template-curly-in-string': 'error', - 'no-throw-literal': 'error', - 'no-undef-init': 'error', - 'no-underscore-dangle': 'off', - 'no-unmodified-loop-condition': 'error', - 'no-unneeded-ternary': 'error', - 'no-unused-expressions': 'error', - 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], - 'no-use-before-define': 'error', - 'no-useless-call': 'error', - 'no-useless-computed-key': 'error', - 'no-useless-concat': 'error', - 'no-useless-constructor': 'error', - 'no-useless-rename': 'error', - 'no-useless-return': 'error', - 'no-var': 'error', - 'no-void': 'error', - 'object-shorthand': 'error', - 'one-var': ['error', 'never'], - 'operator-assignment': 'error', - '@stylistic/js/padding-line-between-statements': [ - 'error', - { - blankLine: 'always', - prev: '*', - next: 'return' - }, - { - blankLine: 'always', - prev: ['const', 'let', 'var'], - next: '*' - }, - { - blankLine: 'any', - prev: ['const', 'let', 'var'], - next: ['const', 'let', 'var'] - }, - { - blankLine: 'always', - prev: 'directive', - next: '*' +/** + * Generates an ESLint config for JavaScript, based on the Ackama style guide + * + * @return {import('eslint').Linter.FlatConfig|import('eslint').Linter.LegacyConfig} + */ +const generateConfig = () => { + if (process.env.ESLINT_USE_FLAT_CONFIG !== 'false') { + /* eslint-disable n/global-require */ + const js = require('@eslint/js'); + const pluginEslintCommentsConfigs = require('@eslint-community/eslint-plugin-eslint-comments/configs'); + const pluginStylisticJS = require('@stylistic/eslint-plugin-js'); + const pluginImport = require('eslint-plugin-import'); + const pluginN = require('eslint-plugin-n'); + const pluginPrettier = require('eslint-plugin-prettier'); + const pluginPrettierRecommended = require('eslint-plugin-prettier/recommended'); + /* eslint-enable n/global-require */ + + /** @type {import('eslint').Linter.FlatConfig} */ + const config = { + plugins: { + ...pluginEslintCommentsConfigs.recommended.plugins, + '@stylistic/js': pluginStylisticJS, + 'import': pluginImport, + 'n': pluginN, + 'prettier': pluginPrettier }, - { - blankLine: 'any', - prev: 'directive', - next: 'directive' + // ignorePatterns: [ + // '!.eslintrc.js', + // 'node_modules/*', + // 'coverage/*', + // 'bundle/*', + // 'public/*', + // 'vendor/*', + // 'dist/*', + // 'lib/*', + // 'out/*' + // ], + rules: { + ...js.configs.recommended.rules, + ...pluginEslintCommentsConfigs.recommended.rules, + ...pluginPrettierRecommended.rules, + + '@eslint-community/eslint-comments/disable-enable-pair': [ + 'error', + { allowWholeFile: true } + ], + '@eslint-community/eslint-comments/no-unused-disable': 'error', + 'import/export': 'error', + 'import/no-absolute-path': 'error', + 'import/no-anonymous-default-export': 'error', + 'import/no-mutable-exports': 'error', + 'import/no-self-import': 'error', + 'import/no-webpack-loader-syntax': 'error', + 'import/order': [ + 'error', + { + 'alphabetize': { order: 'asc' }, + 'groups': [ + ['builtin', 'external', 'internal', 'unknown'], + ['parent', 'sibling', 'index'] + ], + 'newlines-between': 'never' + } + ], + 'n/callback-return': 'warn', + 'n/global-require': 'error', + 'n/no-deprecated-api': 'error', + 'n/no-mixed-requires': 'error', + 'n/no-new-require': 'error', + 'n/no-path-concat': 'error', + 'n/no-process-exit': 'error', + 'n/no-sync': 'warn', + 'accessor-pairs': ['error', { enforceForClassMembers: true }], + 'array-callback-return': 'error', + 'block-scoped-var': 'warn', + 'camelcase': ['error', { allow: ['child_process'] }], + 'consistent-return': 'error', + 'consistent-this': 'error', + 'curly': 'error', + 'default-case': 'error', + 'default-param-last': 'error', + 'dot-notation': 'error', + 'eqeqeq': 'error', + 'func-names': ['error', 'as-needed'], + 'grouped-accessor-pairs': ['error', 'getBeforeSet'], + 'guard-for-in': 'error', + 'init-declarations': 'error', + '@stylistic/js/lines-between-class-members': [ + 'error', + 'always', + { exceptAfterSingleLine: true } + ], + 'max-classes-per-file': ['error', 1], + 'new-cap': [ + 'error', + { capIsNewExceptions: ['ESLintUtils.RuleCreator'] } + ], + 'no-alert': 'warn', + 'no-array-constructor': 'error', + 'no-await-in-loop': 'error', + 'no-bitwise': 'error', + 'no-caller': 'error', + 'no-constructor-return': 'error', + 'no-dupe-else-if': 'error', + 'no-else-return': 'error', + 'no-empty-function': 'error', + 'no-eq-null': 'error', + 'no-eval': 'error', + 'no-extend-native': 'error', + 'no-extra-bind': 'error', + 'no-extra-label': 'error', + 'no-implicit-globals': 'error', + 'no-implied-eval': 'error', + 'no-import-assign': 'error', + 'no-invalid-this': 'error', + 'no-iterator': 'error', + 'no-label-var': 'error', + 'no-labels': 'error', + 'no-lone-blocks': 'error', + 'no-lonely-if': 'error', + 'no-loop-func': 'error', + 'no-multi-assign': 'error', + 'no-multi-str': 'error', + 'no-nested-ternary': 'error', + 'no-new': 'error', + 'no-new-func': 'error', + 'no-new-object': 'error', + 'no-new-wrappers': 'error', + 'no-octal-escape': 'error', + 'no-param-reassign': 'error', + 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }], + 'no-promise-executor-return': 'error', + 'no-proto': 'error', + 'no-return-assign': 'error', + 'no-return-await': 'error', + 'no-script-url': 'error', + 'no-self-compare': 'error', + 'no-sequences': 'error', + 'no-setter-return': 'error', + 'no-shadow': 'warn', + 'no-template-curly-in-string': 'error', + 'no-throw-literal': 'error', + 'no-undef-init': 'error', + 'no-underscore-dangle': 'off', + 'no-unmodified-loop-condition': 'error', + 'no-unneeded-ternary': 'error', + 'no-unused-expressions': 'error', + 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + 'no-use-before-define': 'error', + 'no-useless-call': 'error', + 'no-useless-computed-key': 'error', + 'no-useless-concat': 'error', + 'no-useless-constructor': 'error', + 'no-useless-rename': 'error', + 'no-useless-return': 'error', + 'no-var': 'error', + 'no-void': 'error', + 'object-shorthand': 'error', + 'one-var': ['error', 'never'], + 'operator-assignment': 'error', + '@stylistic/js/padding-line-between-statements': [ + 'error', + { + blankLine: 'always', + prev: '*', + next: 'return' + }, + { + blankLine: 'always', + prev: ['const', 'let', 'var'], + next: '*' + }, + { + blankLine: 'any', + prev: ['const', 'let', 'var'], + next: ['const', 'let', 'var'] + }, + { + blankLine: 'always', + prev: 'directive', + next: '*' + }, + { + blankLine: 'any', + prev: 'directive', + next: 'directive' + } + ], + 'prefer-arrow-callback': 'warn', + 'prefer-const': 'error', + 'prefer-destructuring': [ + 'error', + { + AssignmentExpression: { array: true }, + VariableDeclarator: { array: true } + } + ], + 'prefer-exponentiation-operator': 'error', + 'prefer-numeric-literals': 'error', + 'prefer-object-spread': 'error', + 'prefer-promise-reject-errors': 'error', + 'prefer-regex-literals': 'error', + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + 'prefer-template': 'warn', + 'radix': ['error', 'as-needed'], + 'require-await': 'off', // never + 'require-unicode-regexp': 'error', + 'sort-imports': ['error', { ignoreDeclarationSort: true }], + 'sort-vars': 'error', + '@stylistic/js/spaced-comment': [ + 'warn', + 'always', + { + markers: ['=', '#region'], + exceptions: ['#endregion'] + } + ], + 'strict': 'error', + 'symbol-description': 'error', + 'yoda': 'error' } + }; + + return config; + } + + /** @type {import('eslint').Linter.LegacyConfig} */ + const config = { + env: { es2017: true }, + plugins: [ + '@eslint-community/eslint-comments', + '@stylistic/js', + 'prettier', // + 'import', + 'n' ], - 'prefer-arrow-callback': 'warn', - 'prefer-const': 'error', - 'prefer-destructuring': [ - 'error', - { - AssignmentExpression: { array: true }, - VariableDeclarator: { array: true } - } + extends: [ + 'eslint:recommended', + 'plugin:prettier/recommended', + 'plugin:@eslint-community/eslint-comments/recommended', + 'plugin:prettier/recommended' ], - 'prefer-exponentiation-operator': 'error', - 'prefer-numeric-literals': 'error', - 'prefer-object-spread': 'error', - 'prefer-promise-reject-errors': 'error', - 'prefer-regex-literals': 'error', - 'prefer-rest-params': 'error', - 'prefer-spread': 'error', - 'prefer-template': 'warn', - 'radix': ['error', 'as-needed'], - 'require-await': 'off', // never - 'require-unicode-regexp': 'error', - 'sort-imports': ['error', { ignoreDeclarationSort: true }], - 'sort-vars': 'error', - '@stylistic/js/spaced-comment': [ - 'warn', - 'always', - { - markers: ['=', '#region'], - exceptions: ['#endregion'] - } + ignorePatterns: [ + '!.eslintrc.js', + 'node_modules/*', + 'coverage/*', + 'bundle/*', + 'public/*', + 'vendor/*', + 'dist/*', + 'lib/*', + 'out/*' ], - 'strict': 'error', - 'symbol-description': 'error', - 'yoda': 'error' - } + rules: { + '@eslint-community/eslint-comments/disable-enable-pair': [ + 'error', + { allowWholeFile: true } + ], + '@eslint-community/eslint-comments/no-unused-disable': 'error', + 'import/export': 'error', + 'import/no-absolute-path': 'error', + 'import/no-anonymous-default-export': 'error', + 'import/no-mutable-exports': 'error', + 'import/no-self-import': 'error', + 'import/no-webpack-loader-syntax': 'error', + 'import/order': [ + 'error', + { + 'alphabetize': { order: 'asc' }, + 'groups': [ + ['builtin', 'external', 'internal', 'unknown'], + ['parent', 'sibling', 'index'] + ], + 'newlines-between': 'never' + } + ], + 'n/callback-return': 'warn', + 'n/global-require': 'error', + 'n/no-deprecated-api': 'error', + 'n/no-mixed-requires': 'error', + 'n/no-new-require': 'error', + 'n/no-path-concat': 'error', + 'n/no-process-exit': 'error', + 'n/no-sync': 'warn', + 'accessor-pairs': ['error', { enforceForClassMembers: true }], + 'array-callback-return': 'error', + 'block-scoped-var': 'warn', + 'camelcase': ['error', { allow: ['child_process'] }], + 'consistent-return': 'error', + 'consistent-this': 'error', + 'curly': 'error', + 'default-case': 'error', + 'default-param-last': 'error', + 'dot-notation': 'error', + 'eqeqeq': 'error', + 'func-names': ['error', 'as-needed'], + 'grouped-accessor-pairs': ['error', 'getBeforeSet'], + 'guard-for-in': 'error', + 'init-declarations': 'error', + '@stylistic/js/lines-between-class-members': [ + 'error', + 'always', + { exceptAfterSingleLine: true } + ], + 'max-classes-per-file': ['error', 1], + 'new-cap': ['error', { capIsNewExceptions: ['ESLintUtils.RuleCreator'] }], + 'no-alert': 'warn', + 'no-array-constructor': 'error', + 'no-await-in-loop': 'error', + 'no-bitwise': 'error', + 'no-caller': 'error', + 'no-constructor-return': 'error', + 'no-dupe-else-if': 'error', + 'no-else-return': 'error', + 'no-empty-function': 'error', + 'no-eq-null': 'error', + 'no-eval': 'error', + 'no-extend-native': 'error', + 'no-extra-bind': 'error', + 'no-extra-label': 'error', + 'no-implicit-globals': 'error', + 'no-implied-eval': 'error', + 'no-import-assign': 'error', + 'no-invalid-this': 'error', + 'no-iterator': 'error', + 'no-label-var': 'error', + 'no-labels': 'error', + 'no-lone-blocks': 'error', + 'no-lonely-if': 'error', + 'no-loop-func': 'error', + 'no-multi-assign': 'error', + 'no-multi-str': 'error', + 'no-nested-ternary': 'error', + 'no-new': 'error', + 'no-new-func': 'error', + 'no-new-object': 'error', + 'no-new-wrappers': 'error', + 'no-octal-escape': 'error', + 'no-param-reassign': 'error', + 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }], + 'no-promise-executor-return': 'error', + 'no-proto': 'error', + 'no-return-assign': 'error', + 'no-return-await': 'error', + 'no-script-url': 'error', + 'no-self-compare': 'error', + 'no-sequences': 'error', + 'no-setter-return': 'error', + 'no-shadow': 'warn', + 'no-template-curly-in-string': 'error', + 'no-throw-literal': 'error', + 'no-undef-init': 'error', + 'no-underscore-dangle': 'off', + 'no-unmodified-loop-condition': 'error', + 'no-unneeded-ternary': 'error', + 'no-unused-expressions': 'error', + 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + 'no-use-before-define': 'error', + 'no-useless-call': 'error', + 'no-useless-computed-key': 'error', + 'no-useless-concat': 'error', + 'no-useless-constructor': 'error', + 'no-useless-rename': 'error', + 'no-useless-return': 'error', + 'no-var': 'error', + 'no-void': 'error', + 'object-shorthand': 'error', + 'one-var': ['error', 'never'], + 'operator-assignment': 'error', + '@stylistic/js/padding-line-between-statements': [ + 'error', + { + blankLine: 'always', + prev: '*', + next: 'return' + }, + { + blankLine: 'always', + prev: ['const', 'let', 'var'], + next: '*' + }, + { + blankLine: 'any', + prev: ['const', 'let', 'var'], + next: ['const', 'let', 'var'] + }, + { + blankLine: 'always', + prev: 'directive', + next: '*' + }, + { + blankLine: 'any', + prev: 'directive', + next: 'directive' + } + ], + 'prefer-arrow-callback': 'warn', + 'prefer-const': 'error', + 'prefer-destructuring': [ + 'error', + { + AssignmentExpression: { array: true }, + VariableDeclarator: { array: true } + } + ], + 'prefer-exponentiation-operator': 'error', + 'prefer-numeric-literals': 'error', + 'prefer-object-spread': 'error', + 'prefer-promise-reject-errors': 'error', + 'prefer-regex-literals': 'error', + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + 'prefer-template': 'warn', + 'radix': ['error', 'as-needed'], + 'require-await': 'off', // never + 'require-unicode-regexp': 'error', + 'sort-imports': ['error', { ignoreDeclarationSort: true }], + 'sort-vars': 'error', + '@stylistic/js/spaced-comment': [ + 'warn', + 'always', + { + markers: ['=', '#region'], + exceptions: ['#endregion'] + } + ], + 'strict': 'error', + 'symbol-description': 'error', + 'yoda': 'error' + } + }; + + return config; }; -module.exports = config; +module.exports = generateConfig(); diff --git a/jest.js b/jest.js index 621dfbe5..8e069484 100644 --- a/jest.js +++ b/jest.js @@ -18,55 +18,130 @@ const banMatchers = matchers => { ); }; -/** @type {import('eslint').Linter.Config} */ -const config = { - plugins: ['jest'], - extends: ['plugin:jest/recommended', 'plugin:jest/style'], - rules: { - '@typescript-eslint/unbound-method': 'off', - 'jest/consistent-test-it': 'error', - 'jest/expect-expect': [ - 'error', - // todo: TBD - this will need adjusting for react-testing-library - { assertFunctionNames: ['expect'] } - ], - 'jest/no-conditional-expect': 'error', - 'jest/no-conditional-in-test': 'error', - 'jest/no-deprecated-functions': 'error', - 'jest/no-large-snapshots': 'warn', - 'jest/no-restricted-matchers': [ - 'error', - banMatchers({ - toThrowErrorMatchingSnapshot: - 'Use `toThrowErrorMatchingInlineSnapshot()` instead', - toMatchSnapshot: 'Use `toMatchInlineSnapshot()` instead', - toBeTruthy: 'Avoid `toBeTruthy`', - toBeFalsy: 'Avoid `toBeFalsy`' - }) - ], - 'jest/no-test-return-statement': 'error', - 'jest/prefer-called-with': 'error', - // you can disable this if you use a `beforeEach` setup script, - 'jest/prefer-expect-assertions': 'warn', - 'jest/prefer-expect-resolves': 'error', - 'jest/prefer-hooks-on-top': 'error', - 'jest/prefer-lowercase-title': ['error', { ignoreTopLevelDescribe: true }], - 'jest/prefer-spy-on': 'error', - 'jest/prefer-strict-equal': 'error', - 'jest/prefer-todo': 'error', - 'jest/require-hook': 'error', - 'jest/require-to-throw-message': 'error', - 'jest/require-top-level-describe': 'error', - 'jest/unbound-method': 'error', - 'jest/valid-title': 'error', +/** + * Generates an ESLint config for Jest, based on the Ackama style guide + * + * @return {import('eslint').Linter.FlatConfig|import('eslint').Linter.LegacyConfig} + */ +const generateConfig = () => { + if (process.env.ESLINT_USE_FLAT_CONFIG !== 'false') { + // eslint-disable-next-line n/global-require + const pluginJest = require('eslint-plugin-jest'); + + /** @type {import('eslint').Linter.FlatConfig} */ + const config = { + ...pluginJest.configs['flat/recommended'], + plugins: { jest: pluginJest }, + rules: { + ...pluginJest.configs['flat/recommended'].rules, + ...pluginJest.configs['flat/style'].rules, + '@typescript-eslint/unbound-method': 'off', + 'jest/consistent-test-it': 'error', + 'jest/expect-expect': [ + 'error', + // todo: TBD - this will need adjusting for react-testing-library + { assertFunctionNames: ['expect'] } + ], + 'jest/no-conditional-expect': 'error', + 'jest/no-conditional-in-test': 'error', + 'jest/no-deprecated-functions': 'error', + 'jest/no-large-snapshots': 'warn', + 'jest/no-restricted-matchers': [ + 'error', + banMatchers({ + toThrowErrorMatchingSnapshot: + 'Use `toThrowErrorMatchingInlineSnapshot()` instead', + toMatchSnapshot: 'Use `toMatchInlineSnapshot()` instead', + toBeTruthy: 'Avoid `toBeTruthy`', + toBeFalsy: 'Avoid `toBeFalsy`' + }) + ], + 'jest/no-test-return-statement': 'error', + 'jest/prefer-called-with': 'error', + // you can disable this if you use a `beforeEach` setup script, + 'jest/prefer-expect-assertions': 'warn', + 'jest/prefer-expect-resolves': 'error', + 'jest/prefer-hooks-on-top': 'error', + 'jest/prefer-lowercase-title': [ + 'error', + { ignoreTopLevelDescribe: true } + ], + 'jest/prefer-spy-on': 'error', + 'jest/prefer-strict-equal': 'error', + 'jest/prefer-todo': 'error', + 'jest/require-hook': 'error', + 'jest/require-to-throw-message': 'error', + 'jest/require-top-level-describe': 'error', + 'jest/unbound-method': 'error', + 'jest/valid-title': 'error', - 'jest/padding-around-after-all-blocks': 'error', - 'jest/padding-around-after-each-blocks': 'error', - 'jest/padding-around-before-all-blocks': 'error', - 'jest/padding-around-before-each-blocks': 'error', - 'jest/padding-around-describe-blocks': 'error', - 'jest/padding-around-test-blocks': 'error' + 'jest/padding-around-after-all-blocks': 'error', + 'jest/padding-around-after-each-blocks': 'error', + 'jest/padding-around-before-all-blocks': 'error', + 'jest/padding-around-before-each-blocks': 'error', + 'jest/padding-around-describe-blocks': 'error', + 'jest/padding-around-test-blocks': 'error' + } + }; + + return config; } + + /** @type {import('eslint').Linter.LegacyConfig} */ + const config = { + plugins: ['jest'], + extends: ['plugin:jest/recommended', 'plugin:jest/style'], + rules: { + '@typescript-eslint/unbound-method': 'off', + 'jest/consistent-test-it': 'error', + 'jest/expect-expect': [ + 'error', + // todo: TBD - this will need adjusting for react-testing-library + { assertFunctionNames: ['expect'] } + ], + 'jest/no-conditional-expect': 'error', + 'jest/no-conditional-in-test': 'error', + 'jest/no-deprecated-functions': 'error', + 'jest/no-large-snapshots': 'warn', + 'jest/no-restricted-matchers': [ + 'error', + banMatchers({ + toThrowErrorMatchingSnapshot: + 'Use `toThrowErrorMatchingInlineSnapshot()` instead', + toMatchSnapshot: 'Use `toMatchInlineSnapshot()` instead', + toBeTruthy: 'Avoid `toBeTruthy`', + toBeFalsy: 'Avoid `toBeFalsy`' + }) + ], + 'jest/no-test-return-statement': 'error', + 'jest/prefer-called-with': 'error', + // you can disable this if you use a `beforeEach` setup script, + 'jest/prefer-expect-assertions': 'warn', + 'jest/prefer-expect-resolves': 'error', + 'jest/prefer-hooks-on-top': 'error', + 'jest/prefer-lowercase-title': [ + 'error', + { ignoreTopLevelDescribe: true } + ], + 'jest/prefer-spy-on': 'error', + 'jest/prefer-strict-equal': 'error', + 'jest/prefer-todo': 'error', + 'jest/require-hook': 'error', + 'jest/require-to-throw-message': 'error', + 'jest/require-top-level-describe': 'error', + 'jest/unbound-method': 'error', + 'jest/valid-title': 'error', + + 'jest/padding-around-after-all-blocks': 'error', + 'jest/padding-around-after-each-blocks': 'error', + 'jest/padding-around-before-all-blocks': 'error', + 'jest/padding-around-before-each-blocks': 'error', + 'jest/padding-around-describe-blocks': 'error', + 'jest/padding-around-test-blocks': 'error' + } + }; + + return config; }; -module.exports = config; +module.exports = generateConfig(); diff --git a/package-lock.json b/package-lock.json index be58a1f6..78c0f5ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "3.4.0", "license": "ISC", "dependencies": { + "@eslint/js": "^8.57.1", "eslint-config-prettier": "^8.0.0" }, "devDependencies": { @@ -17,15 +18,13 @@ "@commitlint/cli": "^19.0.0", "@commitlint/config-conventional": "^19.0.0", "@eslint-community/eslint-plugin-eslint-comments": "^4.0.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "^8.57.1", "@semantic-release/changelog": "^6.0.0", "@semantic-release/git": "^10.0.0", "@stylistic/eslint-plugin-js": "^2.6.1", "@stylistic/eslint-plugin-ts": "^2.6.1", "@types/eslint": "^8.0.0", - "@types/eslint__eslintrc": "^2.1.2", "@types/eslint__js": "^8.42.3", + "@types/eslint-plugin-jsx-a11y": "^6.9.0", "@types/jest": "^29.0.0", "@types/node": "^20.0.0", "@types/semver": "^7.3.8", @@ -1029,122 +1028,6 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@eslint/js": { "version": "8.57.1", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", @@ -3054,20 +2937,20 @@ "@types/json-schema": "*" } }, - "node_modules/@types/eslint__eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@types/eslint__eslintrc/-/eslint__eslintrc-2.1.2.tgz", - "integrity": "sha512-qXvzPFY7Rz05xD8ZApXJ3S8xStQD2Ibzu3EFIF0UMNOAfLY5xUu3H61q0JrHo2OXD6rcFG75yUxNQbkKtFKBSw==", + "node_modules/@types/eslint__js": { + "version": "8.42.3", + "resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-8.42.3.tgz", + "integrity": "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==", "dev": true, "license": "MIT", "dependencies": { "@types/eslint": "*" } }, - "node_modules/@types/eslint__js": { - "version": "8.42.3", - "resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-8.42.3.tgz", - "integrity": "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==", + "node_modules/@types/eslint-plugin-jsx-a11y": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@types/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.9.0.tgz", + "integrity": "sha512-5nw0sPyYGCsFibwjOXftxends8Nrh/JLgDtBWj6aJVcN14kHwy1yIy0o1MGLKfCcR27pvUFGgYG+hX2HSX16uA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index e1910f51..f43c6515 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ } }, "dependencies": { + "@eslint/js": "^8.57.1", "eslint-config-prettier": "^8.0.0" }, "devDependencies": { @@ -74,15 +75,13 @@ "@commitlint/cli": "^19.0.0", "@commitlint/config-conventional": "^19.0.0", "@eslint-community/eslint-plugin-eslint-comments": "^4.0.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "^8.57.1", "@semantic-release/changelog": "^6.0.0", "@semantic-release/git": "^10.0.0", "@stylistic/eslint-plugin-js": "^2.6.1", "@stylistic/eslint-plugin-ts": "^2.6.1", "@types/eslint": "^8.0.0", - "@types/eslint__eslintrc": "^2.1.2", "@types/eslint__js": "^8.42.3", + "@types/eslint-plugin-jsx-a11y": "^6.9.0", "@types/jest": "^29.0.0", "@types/node": "^20.0.0", "@types/semver": "^7.3.8", diff --git a/react.js b/react.js index 9fb96c00..e18e444b 100644 --- a/react.js +++ b/react.js @@ -1,82 +1,205 @@ -/** @type {import('eslint').Linter.Config} */ -const config = { - env: { es2017: true }, - parserOptions: { - ecmaFeatures: { - jsx: true - } - }, - settings: { - react: { - version: 'detect' - } - }, - plugins: ['prettier', 'react', 'react-hooks', 'jsx-a11y'], - extends: [ - 'plugin:jsx-a11y/recommended', - 'plugin:jsx-a11y/strict', - 'plugin:react/recommended', - 'plugin:react-hooks/recommended', - 'plugin:prettier/recommended' - ], - overrides: [ - { - files: ['*.tsx'], +/** + * Generates an ESLint config for React, based on the Ackama style guide + * + * @return {import('eslint').Linter.FlatConfig|import('eslint').Linter.LegacyConfig} + */ +const generateConfig = () => { + if (process.env.ESLINT_USE_FLAT_CONFIG !== 'false') { + /* eslint-disable n/global-require */ + const pluginJsxA11y = require('eslint-plugin-jsx-a11y'); + const pluginPrettier = require('eslint-plugin-prettier'); + const pluginPrettierRecommended = require('eslint-plugin-prettier/recommended'); + const pluginReact = require('eslint-plugin-react'); + const pluginReactHooks = require('eslint-plugin-react-hooks'); + /* eslint-enable n/global-require */ + + /** @type {import('eslint').Linter.FlatConfig} */ + const config = { + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true + } + } + }, + settings: { + react: { + version: 'detect' + } + }, + plugins: { + 'jsx-a11y': pluginJsxA11y, + 'prettier': pluginPrettier, + 'react': pluginReact, + 'react-hooks': pluginReactHooks + }, + // todo: need to think about what to do for this + // overrides: [ + // { + // files: ['*.tsx'], + // rules: { + // 'react/no-unknown-property': 'off', + // 'react/prop-types': 'off', + // 'react/require-render-return': 'off' + // } + // } + // ], rules: { - 'react/no-unknown-property': 'off', - 'react/prop-types': 'off', - 'react/require-render-return': 'off' + ...pluginJsxA11y.flatConfigs.recommended.rules, + ...pluginJsxA11y.flatConfigs.strict.rules, + ...pluginReact.configs.flat.recommended.rules, + ...pluginPrettierRecommended.rules, + + // explicitly (re)enable this as it's disabled by eslint-config-prettier + // and its likely our standard JS config will be used alongside this one + 'curly': 'error', + + // todo: react-hooks does not export a flat config (yet) + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + + 'react/button-has-type': 'warn', + 'react/default-props-match-prop-types': 'warn', + 'react/display-name': 'off', // todo: re-look into + 'react/forbid-foreign-prop-types': 'error', + 'react/forbid-prop-types': 'warn', + + 'react/function-component-definition': [ + 'error', + { + namedComponents: 'arrow-function', + unnamedComponents: 'arrow-function' + } + ], + 'react/jsx-boolean-value': 'warn', + 'react/jsx-curly-brace-presence': 'warn', + 'react/jsx-filename-extension': [ + 'warn', + { extensions: ['.tsx', '.jsx'] } + ], + 'react/jsx-fragments': 'error', + 'react/jsx-handler-names': 'error', + 'react/jsx-no-bind': 'warn', + 'react/jsx-no-script-url': 'error', + 'react/jsx-no-useless-fragment': 'error', + 'react/jsx-pascal-case': 'error', + 'react/no-access-state-in-setstate': 'error', + 'react/no-array-index-key': 'warn', + 'react/no-danger': 'error', + 'react/no-did-mount-set-state': 'error', + 'react/no-did-update-set-state': 'error', + 'react/no-multi-comp': ['warn', { ignoreStateless: true }], + 'react/no-redundant-should-component-update': 'error', + 'react/no-this-in-sfc': 'error', + 'react/no-typos': 'error', + 'react/no-unused-prop-types': 'error', + 'react/no-unused-state': 'warn', + 'react/no-will-update-set-state': 'warn', + 'react/prefer-es6-class': 'error', + 'react/prefer-read-only-props': 'error', + 'react/prefer-stateless-function': 'warn', + 'react/require-default-props': [ + 'error', + { ignoreFunctionalComponents: true } + ], + 'react/self-closing-comp': 'warn', + 'react/state-in-constructor': ['error', 'never'], + 'react/style-prop-object': 'warn', + 'react/void-dom-elements-no-children': 'warn' } - } - ], - rules: { - // explicitly (re)enable this as it's disabled by eslint-config-prettier - // and its likely our standard JS config will be used alongside this one - 'curly': 'error', + }; - 'react/button-has-type': 'warn', - 'react/default-props-match-prop-types': 'warn', - 'react/display-name': 'off', // todo: re-look into - 'react/forbid-foreign-prop-types': 'error', - 'react/forbid-prop-types': 'warn', + return config; + } - 'react/function-component-definition': [ - 'error', - { namedComponents: 'arrow-function', unnamedComponents: 'arrow-function' } + /** @type {import('eslint').Linter.LegacyConfig} */ + const config = { + env: { es2017: true }, + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + settings: { + react: { + version: 'detect' + } + }, + plugins: ['prettier', 'react', 'react-hooks', 'jsx-a11y'], + extends: [ + 'plugin:jsx-a11y/recommended', + 'plugin:jsx-a11y/strict', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:prettier/recommended' ], - 'react/jsx-boolean-value': 'warn', - 'react/jsx-curly-brace-presence': 'warn', - 'react/jsx-filename-extension': ['warn', { extensions: ['.tsx', '.jsx'] }], - 'react/jsx-fragments': 'error', - 'react/jsx-handler-names': 'error', - 'react/jsx-no-bind': 'warn', - 'react/jsx-no-script-url': 'error', - 'react/jsx-no-useless-fragment': 'error', - 'react/jsx-pascal-case': 'error', - 'react/no-access-state-in-setstate': 'error', - 'react/no-array-index-key': 'warn', - 'react/no-danger': 'error', - 'react/no-did-mount-set-state': 'error', - 'react/no-did-update-set-state': 'error', - 'react/no-multi-comp': ['warn', { ignoreStateless: true }], - 'react/no-redundant-should-component-update': 'error', - 'react/no-this-in-sfc': 'error', - 'react/no-typos': 'error', - 'react/no-unused-prop-types': 'error', - 'react/no-unused-state': 'warn', - 'react/no-will-update-set-state': 'warn', - 'react/prefer-es6-class': 'error', - 'react/prefer-read-only-props': 'error', - 'react/prefer-stateless-function': 'warn', - 'react/require-default-props': [ - 'error', - { ignoreFunctionalComponents: true } + overrides: [ + { + files: ['*.tsx'], + rules: { + 'react/no-unknown-property': 'off', + 'react/prop-types': 'off', + 'react/require-render-return': 'off' + } + } ], - 'react/self-closing-comp': 'warn', - 'react/state-in-constructor': ['error', 'never'], - 'react/style-prop-object': 'warn', - 'react/void-dom-elements-no-children': 'warn' - } + rules: { + // explicitly (re)enable this as it's disabled by eslint-config-prettier + // and its likely our standard JS config will be used alongside this one + 'curly': 'error', + + 'react/button-has-type': 'warn', + 'react/default-props-match-prop-types': 'warn', + 'react/display-name': 'off', // todo: re-look into + 'react/forbid-foreign-prop-types': 'error', + 'react/forbid-prop-types': 'warn', + + 'react/function-component-definition': [ + 'error', + { + namedComponents: 'arrow-function', + unnamedComponents: 'arrow-function' + } + ], + 'react/jsx-boolean-value': 'warn', + 'react/jsx-curly-brace-presence': 'warn', + 'react/jsx-filename-extension': [ + 'warn', + { extensions: ['.tsx', '.jsx'] } + ], + 'react/jsx-fragments': 'error', + 'react/jsx-handler-names': 'error', + 'react/jsx-no-bind': 'warn', + 'react/jsx-no-script-url': 'error', + 'react/jsx-no-useless-fragment': 'error', + 'react/jsx-pascal-case': 'error', + 'react/no-access-state-in-setstate': 'error', + 'react/no-array-index-key': 'warn', + 'react/no-danger': 'error', + 'react/no-did-mount-set-state': 'error', + 'react/no-did-update-set-state': 'error', + 'react/no-multi-comp': ['warn', { ignoreStateless: true }], + 'react/no-redundant-should-component-update': 'error', + 'react/no-this-in-sfc': 'error', + 'react/no-typos': 'error', + 'react/no-unused-prop-types': 'error', + 'react/no-unused-state': 'warn', + 'react/no-will-update-set-state': 'warn', + 'react/prefer-es6-class': 'error', + 'react/prefer-read-only-props': 'error', + 'react/prefer-stateless-function': 'warn', + 'react/require-default-props': [ + 'error', + { ignoreFunctionalComponents: true } + ], + 'react/self-closing-comp': 'warn', + 'react/state-in-constructor': ['error', 'never'], + 'react/style-prop-object': 'warn', + 'react/void-dom-elements-no-children': 'warn' + } + }; + + return config; }; -module.exports = config; +module.exports = generateConfig(); diff --git a/test/configs.spec.ts b/test/configs.spec.ts index f64f395f..9720222c 100644 --- a/test/configs.spec.ts +++ b/test/configs.spec.ts @@ -3,6 +3,8 @@ import * as fs from 'fs'; import semver from 'semver/preload'; import packageJson from '../package.json'; +process.env.ESLINT_USE_FLAT_CONFIG = 'false'; + // eslint-disable-next-line n/no-sync const configFiles = fs .readdirSync('.', { withFileTypes: true }) @@ -49,13 +51,15 @@ const determinePluginPackageName = (plugin: string): string => { const requireConfig = ( config: string -): ESLint.Linter.Config & - Required> => ({ +): ESLint.Linter.LegacyConfig & + Required< + Pick + > => ({ plugins: [], extends: [], rules: {}, // eslint-disable-next-line n/global-require,@typescript-eslint/no-require-imports,@typescript-eslint/no-var-requires - ...(require(config) as ESLint.Linter.Config) + ...(require(config) as ESLint.Linter.LegacyConfig) }); describe('package.json', () => { @@ -125,7 +129,7 @@ describe('for each config file', () => { it('is valid', async () => { expect.hasAssertions(); - const baseConfig: ESLint.Linter.Config = { + const baseConfig: ESLint.Linter.LegacyConfig = { // default to using the @typescript-eslint/parser in case we have any // rules that can use the type services, like `jest/unbound-method` parser: '@typescript-eslint/parser', diff --git a/tools/generate-configs-list.ts b/tools/generate-configs-list.ts index 0f4e1e60..1e6dfa6b 100644 --- a/tools/generate-configs-list.ts +++ b/tools/generate-configs-list.ts @@ -11,12 +11,16 @@ import { prettier as prettierConfigPackage } from '../package.json'; +process.env.ESLINT_USE_FLAT_CONFIG = 'false'; + // eslint-disable-next-line @typescript-eslint/no-require-imports,n/global-require,@typescript-eslint/no-var-requires const prettierConfig = require(prettierConfigPackage) as Options; -const requireConfig = (config: string): Required => { +const requireConfig = ( + config: string +): Required => { // eslint-disable-next-line @typescript-eslint/no-require-imports,n/global-require,@typescript-eslint/no-var-requires - const requiredConfig = require(config) as ESLint.Linter.Config; + const requiredConfig = require(config) as ESLint.Linter.LegacyConfig; return { $schema: '', diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 00000000..374363e2 --- /dev/null +++ b/types.d.ts @@ -0,0 +1,122 @@ +// todo: see https://github.com/eslint-community/eslint-plugin-eslint-comments/pull/246 +declare module '@eslint-community/eslint-plugin-eslint-comments/configs' { + import * as ESLint from 'eslint'; + + const configs: { + recommended: ESLint.Linter.FlatConfig & { + plugins: { + '@eslint-community/eslint-plugin-eslint-comments': ESLint.ESLint.Plugin; + }; + }; + }; + export = configs; +} + +// todo: see https://github.com/import-js/eslint-plugin-import/pull/3097 +declare module 'eslint-plugin-import' { + import * as ESLint from 'eslint'; + + const plugin: ESLint.ESLint.Plugin & { + configs: { + 'recommended': ESLint.Linter.LegacyConfig; + 'errors': ESLint.Linter.LegacyConfig; + 'warnings': ESLint.Linter.LegacyConfig; + 'stage-0': ESLint.Linter.LegacyConfig; + 'react': ESLint.Linter.LegacyConfig; + 'react-native': ESLint.Linter.LegacyConfig; + 'electron': ESLint.Linter.LegacyConfig; + 'typescript': ESLint.Linter.LegacyConfig; + }; + + flatConfigs: { + 'recommended': ESLint.Linter.FlatConfig; + 'errors': ESLint.Linter.FlatConfig; + 'warnings': ESLint.Linter.FlatConfig; + 'stage-0': ESLint.Linter.FlatConfig; + 'react': ESLint.Linter.FlatConfig; + 'react-native': ESLint.Linter.FlatConfig; + 'electron': ESLint.Linter.FlatConfig; + 'typescript': ESLint.Linter.FlatConfig; + }; + }; + export = plugin; +} + +// todo: while @stylistic/eslint-plugin-js provides its own types, they error for some reason +// see https://github.com/eslint-stylistic/eslint-stylistic/issues/481 +declare module '@stylistic/eslint-plugin-js' { + import * as ESLint from 'eslint'; + + const plugin: ESLint.ESLint.Plugin; + export = plugin; +} + +// todo: while @stylistic/eslint-plugin-ts provides its own types, they error for some reason +// see https://github.com/eslint-stylistic/eslint-stylistic/issues/481 +declare module '@stylistic/eslint-plugin-ts' { + import * as ESLint from 'eslint'; + + const plugin: ESLint.ESLint.Plugin; + export = plugin; +} + +// todo: while eslint-plugin-react provides its own types, they are very broken +// see https://github.com/jsx-eslint/eslint-plugin-react/issues/3838 +declare module 'eslint-plugin-react' { + import * as ESLint from 'eslint'; + + const plugin: ESLint.ESLint.Plugin & { + configs: { + flat: Record< + 'all' | 'jsx-runtime' | 'recommended', + ESLint.Linter.FlatConfig + >; + }; + }; + export = plugin; +} + +declare module 'eslint-plugin-react-hooks' { + import * as ESLint from 'eslint'; + + const plugin: ESLint.ESLint.Plugin & { + configs: { + recommended: ESLint.Linter.LegacyConfig; + }; + }; + export = plugin; +} + +// todo: has its own types, but requires `node16` module resolution which breaks other things +declare module '@typescript-eslint/eslint-plugin' { + import * as ESLint from 'eslint'; + + const plugin: ESLint.ESLint.Plugin & { + configs: { + 'all': ESLint.Linter.LegacyConfig; + 'base': ESLint.Linter.LegacyConfig; + 'disable-type-checked': ESLint.Linter.LegacyConfig; + 'eslint-recommended': ESLint.Linter.LegacyConfig; + 'recommended': ESLint.Linter.LegacyConfig; + /** @deprecated - please use "recommended-type-checked" instead. */ + 'recommended-requiring-type-checking': ESLint.Linter.LegacyConfig; + 'recommended-type-checked': ESLint.Linter.LegacyConfig; + 'recommended-type-checked-only': ESLint.Linter.LegacyConfig; + 'strict': ESLint.Linter.LegacyConfig; + 'strict-type-checked': ESLint.Linter.LegacyConfig; + 'strict-type-checked-only': ESLint.Linter.LegacyConfig; + 'stylistic': ESLint.Linter.LegacyConfig; + 'stylistic-type-checked': ESLint.Linter.LegacyConfig; + 'stylistic-type-checked-only': ESLint.Linter.LegacyConfig; + }; + }; + export = plugin; +} + +// todo: doesn't get its own types until v8 +declare module '@typescript-eslint/parser' { + import * as ESLint from 'eslint'; + + const parser: ESLint.Linter.ParserModule; + export = parser; +}