diff --git a/.yarnrc.yml b/.yarnrc.yml index a46d337..d450c61 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -33,5 +33,7 @@ packageExtensions: eslint-plugin-storybook@*: peerDependencies: typescript: ">=4.2.0" - + typescript-eslint@*: + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 pnpEnableEsmLoader: true diff --git a/cspell.config.yml b/cspell.config.yml index abf8fd5..f1fa540 100644 --- a/cspell.config.yml +++ b/cspell.config.yml @@ -7,3 +7,5 @@ import: - '@kurone-kito/cspell-config' usePnP: true version: '0.2' +words: + - TSES diff --git a/packages/eslint-config-base/README.md b/packages/eslint-config-base/README.md index be152dc..054d352 100644 --- a/packages/eslint-config-base/README.md +++ b/packages/eslint-config-base/README.md @@ -4,29 +4,39 @@ My ESLint configuration for general Node.js projects. ## Usage +### Use the flat config (recommended) + +First, install this package and its peer dependencies: + +```sh +npm install --save-dev @kurone-kito/eslint-config-base eslint typescript +``` + +Then, create a `eslint.config.mjs` file. +If exists, merge the following configuration into it: + +```js +export { default } from '@kurone-kito/eslint-config-base'; +``` + +### for legacy configuration (deprecated) + +⚠️ **DEPRECATED**: The legacy configuration is no longer maintained. + First, install this package and its peer dependencies: ```sh npm install --save-dev \ @kurone-kito/eslint-config-base \ @cspell/eslint-plugin \ - @typescript-eslint/eslint-plugin \ - @typescript-eslint/parser \ eslint \ eslint-config-airbnb-typescript \ - eslint-config-prettier \ - eslint-formatter-codeframe \ eslint-import-resolver-node \ eslint-import-resolver-typescript \ eslint-plugin-editorconfig \ eslint-plugin-import \ - eslint-plugin-jsdoc \ - eslint-plugin-json \ - eslint-plugin-markdown \ eslint-plugin-markdownlint \ - eslint-plugin-n \ - eslint-plugin-oxlint \ - eslint-plugin-yaml + eslint-plugin-n ``` Then, create a `.eslintrc.yml` file. diff --git a/packages/eslint-config-base/package.json b/packages/eslint-config-base/package.json index 62e79b6..6ceeef3 100644 --- a/packages/eslint-config-base/package.json +++ b/packages/eslint-config-base/package.json @@ -40,26 +40,15 @@ "start": "tsc --watch" }, "dependencies": { + "@cspell/eslint-plugin": "^8.13.1", "@eslint/compat": "^1.1.1", "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.8.0", - "eslint-plugin-yaml": "^1.0.3" - }, - "devDependencies": { - "@cspell/eslint-plugin": "^8.13.1", - "@kurone-kito/typescript-config": "workspace:^", - "@types/eslint": "^9.6.0", - "@types/eslint__eslintrc": "^2.1.2", - "@types/eslint__js": "^8.42.3", - "@types/node": "^22.1.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", - "concurrently": "^8.2.2", - "cpy-cli": "^5.0.0", - "eslint": "^9.8.0", + "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^18.0.0", "eslint-config-prettier": "^9.1.0", - "eslint-formatter-codeframe": "^7.32.1", "eslint-import-resolver-node": "^0.3.9", "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-editorconfig": "^4.0.3", @@ -70,80 +59,36 @@ "eslint-plugin-markdownlint": "^0.6.0", "eslint-plugin-n": "^17.10.2", "eslint-plugin-oxlint": "^0.5.0", + "eslint-plugin-yaml": "^1.0.3", + "globals": "^15.9.0", + "typescript-eslint": "^8.0.0" + }, + "devDependencies": { + "@kurone-kito/typescript-config": "workspace:^", + "@types/eslint": "^9.6.0", + "@types/eslint-config-prettier": "^6.11.3", + "@types/eslint-plugin-markdown": "^2.0.2", + "@types/eslint__eslintrc": "^2.1.2", + "@types/eslint__js": "^8.42.3", + "@types/node": "^22.1.0", + "@typescript-eslint/utils": "^8.0.0", + "concurrently": "^8.2.2", + "cpy-cli": "^5.0.0", + "eslint": "^9.8.0", + "eslint-formatter-codeframe": "^7.32.1", "js-yaml": "^4.1.0", - "prettier": "^3.3.3", "rimraf": "^5.0.10", "typescript": "~5.5.4" }, "peerDependencies": { - "@cspell/eslint-plugin": ">=6.x.x", - "@typescript-eslint/eslint-plugin": ">=8.0.x", - "@typescript-eslint/parser": ">=8.0.x", "eslint": ">=9.x.x", - "eslint-config-airbnb-typescript": ">=17.x.x", - "eslint-config-prettier": ">=8.x.x", - "eslint-formatter-codeframe": ">=7.x.x", - "eslint-import-resolver-node": ">=0.3.x", - "eslint-import-resolver-typescript": ">=3.x.x", - "eslint-plugin-editorconfig": ">=4.x.x", - "eslint-plugin-import": ">=2.27.x", - "eslint-plugin-jsdoc": ">=46.x.x", - "eslint-plugin-json": ">=4.x.x", - "eslint-plugin-markdown": ">=3.x.x", - "eslint-plugin-markdownlint": ">=0.5.x", - "eslint-plugin-n": ">=17.x.x", - "eslint-plugin-oxlint": ">=0.2.x" + "typescript": ">=5.5.x" }, "peerDependenciesMeta": { - "@cspell/eslint-plugin": { - "optional": true - }, - "@typescript-eslint/eslint-plugin": { - "optional": true - }, - "@typescript-eslint/parser": { - "optional": true - }, "eslint": { "optional": true }, - "eslint-config-airbnb-typescript": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - }, - "eslint-formatter-codeframe": { - "optional": true - }, - "eslint-import-resolver-node": { - "optional": true - }, - "eslint-import-resolver-typescript": { - "optional": true - }, - "eslint-plugin-editorconfig": { - "optional": true - }, - "eslint-plugin-import": { - "optional": true - }, - "eslint-plugin-jsdoc": { - "optional": true - }, - "eslint-plugin-json": { - "optional": true - }, - "eslint-plugin-markdown": { - "optional": true - }, - "eslint-plugin-markdownlint": { - "optional": true - }, - "eslint-plugin-n": { - "optional": true - }, - "eslint-plugin-oxlint": { + "typescript": { "optional": true } }, diff --git a/packages/eslint-config-base/src/airbnb.mts b/packages/eslint-config-base/src/airbnb.mts new file mode 100644 index 0000000..9d6b786 --- /dev/null +++ b/packages/eslint-config-base/src/airbnb.mts @@ -0,0 +1,13 @@ +import type { Linter } from 'eslint'; +import { compat } from './utils.mjs'; + +/** The configuration for ESLint to use the Airbnb style guide. */ +export const airbnbConfig: readonly Linter.Config[] = [ + ...compat.extends('airbnb-base'), + // ...compat.extends('airbnb-typescript/base'), + { + languageOptions: { + parserOptions: { project: true, tsconfigRootDir: process.cwd() }, + }, + }, +]; diff --git a/packages/eslint-config-base/src/data.mts b/packages/eslint-config-base/src/data.mts index b8e08eb..cd646b0 100644 --- a/packages/eslint-config-base/src/data.mts +++ b/packages/eslint-config-base/src/data.mts @@ -1,7 +1,10 @@ import type { Linter } from 'eslint'; +// @ts-ignore +import json from 'eslint-plugin-json'; import pluginYaml from 'eslint-plugin-yaml'; /** The ESLint configuration for data language files. */ export const dataConfig: readonly Linter.Config[] = [ + { files: ['**/*.json'], ...json.configs['recommended'] }, (pluginYaml as any).configs.recommended, ]; diff --git a/packages/eslint-config-base/src/import.mts b/packages/eslint-config-base/src/import.mts new file mode 100644 index 0000000..b9a94b4 --- /dev/null +++ b/packages/eslint-config-base/src/import.mts @@ -0,0 +1,107 @@ +import type { Linter } from 'eslint'; +import { compat } from './utils.mjs'; + +/** Additional configuration for the `import` plugin. */ +export const additionalImportConfig: readonly Linter.Config[] = [ + { + files: ['*.?(c)js?(x)'], + rules: { + /** + * Allow `require` syntax only for JavaScript. By default, it is + * completely prohibited. + * + * There are many situations where pure JavaScript is used outside + * the scope of transpiling, such as in various configuration files, + * making the `import` syntax challenging. + */ + '@typescript-eslint/no-var-requires': 'off', + }, + }, + { + files: ['*.?([cm])js?(x)'], + rules: { + /** + * Allow type inference of return values of exported functions, etc., + * only for JavaScript. By default, it is allowed with a warning. + * + * There are many situations where CommonJS is used outside the scope + * of transpiling, such as in various configuration files, + * where it is challenging to make type definitions mandatory. + */ + '@typescript-eslint/explicit-module-boundary-types': 'off', + }, + }, + { + rules: { + /** + * Forces import without extensions for internal modules only. + * The default is full enforcement. + * + * Because it interferes with imports such as `lodash-es` + * in the ES modules environment. + */ + 'import/extensions': [ + 'error', + 'never', + { js: 'ignorePackages', json: 'ignorePackages' }, + ], + /** + * Prohibit dependencies on `devDependencies`, except for specific + * files. By default, this is a total ban. + * + * There is no need for strict separation of dependent packages since + * they are internally tree shaken by bundlers. Still, some packages + * are separated into `devDependencies` to make it easier to organize. + */ + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: [ + '**/*.config.?([cm])[jt]s?(x)', + '**/*.spec.?([cm])[jt]s?(x)', + '**/*.test.?([cm])[jt]s?(x)', + ], + }, + ], + /** + * Allow with a warning that the arbitrary reordering in the + * import syntax. The default is to allow it unconditionally. + * + * in order to deal with the snowballing problem of the import part. + */ + 'import/order': [ + 'warn', + { + alphabetize: { caseInsensitive: true, order: 'asc' }, + groups: [ + 'builtin', + 'external', + 'internal', + 'parent', + 'sibling', + 'index', + 'object', + ], + 'newlines-between': 'never', + }, + ], + /** + * Allow with a warning that the arbitrary reordering in the + * import syntax. The default is to allow it unconditionally. + * + * To deal with the snowballing problem of the import part. + */ + 'sort-imports': [ + 'warn', + { ignoreCase: true, ignoreDeclarationSort: true }, + ], + }, + }, +]; + +/** The ESLint configuration for the `import` plugin. */ +export const importConfig: readonly Linter.Config[] = [ + ...compat.extends('plugin:import/recommended'), + ...compat.extends('plugin:import/typescript'), + ...additionalImportConfig, +]; diff --git a/packages/eslint-config-base/src/index.mts b/packages/eslint-config-base/src/index.mts index bcf7672..6419909 100644 --- a/packages/eslint-config-base/src/index.mts +++ b/packages/eslint-config-base/src/index.mts @@ -1,8 +1,29 @@ -import type { Linter } from 'eslint'; +import tsEslint from 'typescript-eslint'; +import { airbnbConfig } from './airbnb.mjs'; import { dataConfig } from './data.mjs'; import { ignoreConfig } from './ignore.mjs'; +import { importConfig, additionalImportConfig } from './import.mjs'; +import { jsdocConfig, additionalJsdocConfig } from './jsdoc.mjs'; +import { markdownConfig } from './markdown.mjs'; +import { additionalNodeConfig, nodeConfig } from './node.mjs'; +import { additionalStyleConfig, styleConfig } from './style.mjs'; +import { additionalTsConfig, tsConfig } from './ts.mjs'; -/** ESLint configuration for generic TypeScript projects. */ -const config: readonly Linter.Config[] = [...ignoreConfig, ...dataConfig]; - -export default config; +export default tsEslint.config( + ...([ + ...ignoreConfig, + ...markdownConfig, + ...dataConfig, + ...styleConfig, + ...jsdocConfig, + ...importConfig, + ...nodeConfig, + ...tsConfig, + ...airbnbConfig, + ...additionalStyleConfig, + ...additionalJsdocConfig, + ...additionalImportConfig, + ...additionalNodeConfig, + ...additionalTsConfig, + ] as tsEslint.ConfigWithExtends[]), +); diff --git a/packages/eslint-config-base/src/jsdoc.mts b/packages/eslint-config-base/src/jsdoc.mts new file mode 100644 index 0000000..ce39908 --- /dev/null +++ b/packages/eslint-config-base/src/jsdoc.mts @@ -0,0 +1,41 @@ +import type { Linter } from 'eslint'; +import jsdoc from 'eslint-plugin-jsdoc'; + +/** Additional ESLint configurations for JSDoc comments. */ +export const additionalJsdocConfig: readonly Linter.Config[] = [ + { + rules: { + /** + * Allow unconditional type specification of the arguments in JSDoc. + * The default is a blanket ban. + * + * Because in TypeScript projects, it can be inferred from the type + * definitions in the code. + * + * TODO: + * If there are any inconveniences in document generation, consider + * removing this rule. + */ + 'jsdoc/require-param-type': 'off', + + /** + * Allow unconditional type specification of the return value in JSDoc. + * The default is a blanket ban. + * + * Because in TypeScript projects, it can be inferred from the type + * definitions in the code. + * + * TODO: + * If there are any inconveniences in document generation, consider + * removing this rule. + */ + 'jsdoc/require-returns-type': 'off', + }, + }, +]; + +/** The ESLint configurations for JSDoc comments. */ +export const jsdocConfig: readonly Linter.Config[] = [ + jsdoc.configs['flat/recommended'], + ...additionalJsdocConfig, +]; diff --git a/packages/eslint-config-base/src/markdown.mts b/packages/eslint-config-base/src/markdown.mts new file mode 100644 index 0000000..05b3b93 --- /dev/null +++ b/packages/eslint-config-base/src/markdown.mts @@ -0,0 +1,17 @@ +import type { Linter } from 'eslint'; +import markdown from 'eslint-plugin-markdown'; +import { compat } from './utils.mjs'; + +/** The ESLint configuration for Markdown files. */ +export const markdownConfig: readonly Linter.Config[] = [ + ...(markdown.configs.recommended as Linter.Config[]), + ...compat.config({ + overrides: [ + { + extends: ['plugin:markdownlint/recommended'], + files: ['*.md'], + parser: 'eslint-plugin-markdownlint/parser', + }, + ], + }), +]; diff --git a/packages/eslint-config-base/src/node.mts b/packages/eslint-config-base/src/node.mts new file mode 100644 index 0000000..b9bf9c9 --- /dev/null +++ b/packages/eslint-config-base/src/node.mts @@ -0,0 +1,41 @@ +import type { Linter } from 'eslint'; +import nodePlugin from 'eslint-plugin-n'; + +/** Additional ESLint configurations for Node.js code. */ +export const additionalNodeConfig: readonly Linter.Config[] = [ + { + rules: { + /** + * Unconditional permission to import modules that do not exist. + * The default is total prohibition. + * + * A temporary measure to support the ES module resolution in + * TypeScript. + * + * @see {@link https://github.com/mysticatea/eslint-plugin-node/issues/248#issuecomment-1052550467} + */ + 'n/no-missing-import': 'off', + }, + settings: { 'import/resolver': { typescript: { alwaysTryTypes: true } } }, + }, + { + rules: { + /** + * Allows the use of unsupported Node.js notation. + * Unconditional prohibition by default. + * + * All Node.js versions prohibit dynamic imports, and the rule is + * already obsoleted. + * + * TODO: Remove this rule in the future. + */ + 'n/no-unsupported-features/es-syntax': 'off', + }, + }, +]; + +/** The ESLint configuration for Node.js code. */ +export const nodeConfig: readonly Linter.Config[] = [ + nodePlugin.configs['flat/recommended-script'], + ...additionalNodeConfig, +]; diff --git a/packages/eslint-config-base/src/style.mts b/packages/eslint-config-base/src/style.mts new file mode 100644 index 0000000..96a1be9 --- /dev/null +++ b/packages/eslint-config-base/src/style.mts @@ -0,0 +1,28 @@ +import cspell from '@cspell/eslint-plugin/recommended'; +import type { Linter } from 'eslint'; +import prettier from 'eslint-config-prettier'; +import { compat } from './utils.mjs'; + +/** Additional configuration for the style language files. */ +export const additionalStyleConfig: readonly Linter.Config[] = [ + ...compat.extends('plugin:editorconfig/noconflict'), + prettier, + { + rules: { + /** + * Permit explicit statements in Arrow functions, even when the block + * is optional, with a warning. The default is unconditional allow. + * + * The intent is to reduce the amount of code and keep it simple, + * thereby improving readability. + */ + 'arrow-body-style': 'warn', + }, + }, +]; + +/** The ESLint configuration for style language files. */ +export const styleConfig: readonly Linter.Config[] = [ + { plugins: cspell.plugins ?? {}, rules: cspell.rules ?? {} }, + ...additionalStyleConfig, +]; diff --git a/packages/eslint-config-base/src/ts.mts b/packages/eslint-config-base/src/ts.mts new file mode 100644 index 0000000..6701d64 --- /dev/null +++ b/packages/eslint-config-base/src/ts.mts @@ -0,0 +1,47 @@ +import eslint from '@eslint/js'; +import type { Linter } from 'eslint'; +import oxlint from 'eslint-plugin-oxlint'; +import globals from 'globals'; +import type { ConfigWithExtends } from 'typescript-eslint'; +import tsesLint from 'typescript-eslint'; + +/** Additional ESLint configurations for TypeScript. */ +export const additionalTsConfig: readonly Linter.Config[] = [ + { + languageOptions: { + globals: { ...globals.browser, ...globals.es2025, ...globals.node }, + parserOptions: { ecmaVersion: 'latest', projectService: true }, + }, + linterOptions: { reportUnusedDisableDirectives: 'warn' }, + rules: { + /** + * With a warning, allow use standard import syntax to import for + * type-only. The default is to enable it unconditionally. + * + * Tree shaking at build time needs to work correctly to reduce + * bundle size, and active use of the type import syntax can + * contribute significantly to this. + */ + '@typescript-eslint/consistent-type-imports': 'warn', + /** + * With a warning, allow type inference for return values of functions + * and methods referenced from other files, including public methods + * in exported functions and classes. Default is unconditional allow. + * + * Allowing this may cause TypeScript to chain type inference from the + * wrong type if it does not read the type definition correctly, + * resulting in type errors in the wrong places. + */ + '@typescript-eslint/explicit-module-boundary-types': 'warn', + }, + }, +]; + +/** The ESLint configurations for TypeScript. */ +export const tsConfig: readonly ConfigWithExtends[] = [ + eslint.configs.recommended, + ...tsesLint.configs.recommended, + ...tsesLint.configs.stylistic, + oxlint.configs['flat/recommended'] as Pick, + ...additionalTsConfig, +]; diff --git a/packages/eslint-config-react/README.md b/packages/eslint-config-react/README.md index 675994e..8722c05 100644 --- a/packages/eslint-config-react/README.md +++ b/packages/eslint-config-react/README.md @@ -4,6 +4,25 @@ My ESLint configuration for React projects. ## Usage +### Use the flat config (recommended) + +First, install this package and its peer dependencies: + +```sh +npm install --save-dev @kurone-kito/eslint-config-react eslint +``` + +Then, create a `eslint.config.mjs` file. +If exists, merge the following configuration into it: + +```js +export { default } from '@kurone-kito/eslint-config-react'; +``` + +### for legacy configuration (deprecated) + +⚠️ **DEPRECATED**: The legacy configuration is no longer maintained. + First, install this package and its peer dependencies: ```sh @@ -14,23 +33,17 @@ npm install --save-dev \ @typescript-eslint/parser \ eslint \ eslint-config-airbnb-typescript \ - eslint-config-prettier \ eslint-formatter-codeframe \ eslint-import-resolver-node \ eslint-import-resolver-typescript \ eslint-plugin-editorconfig \ eslint-plugin-import \ - eslint-plugin-jsdoc \ - eslint-plugin-json \ eslint-plugin-jsx-a11y \ - eslint-plugin-markdown \ eslint-plugin-markdownlint \ eslint-plugin-n \ - eslint-plugin-oxlint \ eslint-plugin-react \ eslint-plugin-react-hooks \ - eslint-plugin-storybook \ - eslint-plugin-yaml + eslint-plugin-storybook ``` Then, create a `.eslintrc.yml` file. diff --git a/packages/eslint-config-react/package.json b/packages/eslint-config-react/package.json index 8cd5173..33b028a 100644 --- a/packages/eslint-config-react/package.json +++ b/packages/eslint-config-react/package.json @@ -40,135 +40,42 @@ "start": "tsc --watch" }, "dependencies": { + "@cspell/eslint-plugin": "^8.13.1", "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.8.0", - "@kurone-kito/eslint-config-base": "workspace:^" + "@kurone-kito/eslint-config-base": "workspace:^", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-airbnb-typescript": "^18.0.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jsx-a11y": "^6.9.0", + "eslint-plugin-react": "^7.35.0", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-storybook": "^0.8.0", + "typescript-eslint": "^8.0.0" }, "devDependencies": { - "@cspell/eslint-plugin": "^8.13.1", "@kurone-kito/typescript-config": "workspace:^", "@types/eslint": "^9.6.0", "@types/eslint__eslintrc": "^2.1.2", "@types/eslint__js": "^8.42.3", "@types/node": "^22.1.0", - "@typescript-eslint/eslint-plugin": "^8.0.0", - "@typescript-eslint/parser": "^8.0.0", + "@typescript-eslint/utils": "^8.0.0", "concurrently": "^8.2.2", "cpy-cli": "^5.0.0", "eslint": "^9.8.0", - "eslint-config-airbnb-typescript": "^18.0.0", - "eslint-config-prettier": "^9.1.0", "eslint-formatter-codeframe": "^7.32.1", - "eslint-import-resolver-node": "^0.3.9", - "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-editorconfig": "^4.0.3", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jsdoc": "^48.11.0", - "eslint-plugin-json": "^4.0.0", - "eslint-plugin-jsx-a11y": "^6.9.0", - "eslint-plugin-markdown": "^5.1.0", - "eslint-plugin-markdownlint": "^0.6.0", - "eslint-plugin-n": "^17.10.2", - "eslint-plugin-oxlint": "^0.5.0", - "eslint-plugin-react": "^7.35.0", - "eslint-plugin-react-hooks": "^4.6.2", - "eslint-plugin-storybook": "^0.8.0", - "eslint-plugin-yaml": "^1.0.3", "js-yaml": "^4.1.0", - "prettier": "^3.3.3", "rimraf": "^5.0.10", "typescript": "~5.5.4" }, "peerDependencies": { - "@cspell/eslint-plugin": ">=6.x.x", - "@typescript-eslint/eslint-plugin": ">=8.0.x", - "@typescript-eslint/parser": ">=8.0.x", - "eslint": ">=9.x.x", - "eslint-config-airbnb-typescript": ">=17.x.x", - "eslint-config-prettier": ">=8.x.x", - "eslint-formatter-codeframe": ">=7.x.x", - "eslint-import-resolver-node": ">=0.3.x", - "eslint-import-resolver-typescript": ">=3.x.x", - "eslint-plugin-editorconfig": ">=4.x.x", - "eslint-plugin-import": ">=2.27.x", - "eslint-plugin-jsdoc": ">=46.x.x", - "eslint-plugin-json": ">=4.x.x", - "eslint-plugin-jsx-a11y": ">=6.x.x", - "eslint-plugin-markdown": ">=3.x.x", - "eslint-plugin-markdownlint": ">=0.5.x", - "eslint-plugin-n": ">=17.x.x", - "eslint-plugin-oxlint": ">=0.2.x", - "eslint-plugin-react": ">=7.2.x", - "eslint-plugin-react-hooks": ">=4.3.x", - "eslint-plugin-storybook": ">=0.6.x", - "eslint-plugin-yaml": ">=0.5.x" + "eslint": ">=9.x.x" }, "peerDependenciesMeta": { - "@cspell/eslint-plugin": { - "optional": true - }, - "@typescript-eslint/eslint-plugin": { - "optional": true - }, - "@typescript-eslint/parser": { - "optional": true - }, "eslint": { "optional": true - }, - "eslint-config-airbnb-typescript": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - }, - "eslint-formatter-codeframe": { - "optional": true - }, - "eslint-import-resolver-node": { - "optional": true - }, - "eslint-import-resolver-typescript": { - "optional": true - }, - "eslint-plugin-editorconfig": { - "optional": true - }, - "eslint-plugin-import": { - "optional": true - }, - "eslint-plugin-jsdoc": { - "optional": true - }, - "eslint-plugin-json": { - "optional": true - }, - "eslint-plugin-jsx-a11y": { - "optional": true - }, - "eslint-plugin-markdown": { - "optional": true - }, - "eslint-plugin-markdownlint": { - "optional": true - }, - "eslint-plugin-n": { - "optional": true - }, - "eslint-plugin-oxlint": { - "optional": true - }, - "eslint-plugin-react": { - "optional": true - }, - "eslint-plugin-react-hooks": { - "optional": true - }, - "eslint-plugin-storybook": { - "optional": true - }, - "eslint-plugin-yaml": { - "optional": true } }, "engines": { diff --git a/packages/eslint-config-react/src/index.mts b/packages/eslint-config-react/src/index.mts index d281076..8bc7c75 100644 --- a/packages/eslint-config-react/src/index.mts +++ b/packages/eslint-config-react/src/index.mts @@ -1,7 +1,17 @@ import baseConfig from '@kurone-kito/eslint-config-base'; -import type { Linter } from 'eslint'; +import tsEslint from 'typescript-eslint'; +import { additionalReactConfig, reactConfig } from './react.mjs'; +import { storybookConfig } from './storybook.mjs'; +import { compat } from './utils.mjs'; -/** ESLint configuration for generic React projects. */ -const config: readonly Linter.Config[] = [...baseConfig]; - -export default config; +export default tsEslint.config( + ...([ + ...reactConfig, + ...compat.extends('airbnb'), + ...compat.extends('airbnb-typescript'), + ...baseConfig, + ...additionalReactConfig, + ...storybookConfig, + { languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } } }, + ] as tsEslint.ConfigWithExtends[]), +); diff --git a/packages/eslint-config-react/src/react.mts b/packages/eslint-config-react/src/react.mts new file mode 100644 index 0000000..675d494 --- /dev/null +++ b/packages/eslint-config-react/src/react.mts @@ -0,0 +1,48 @@ +import type { Linter } from 'eslint'; +import { compat } from './utils.mjs'; + +/** Additional configuration for ESLint to React */ +export const additionalReactConfig: readonly Linter.Config[] = [ + { + rules: { + /** + * Allows explicit notation of `true` in JSX notation with a warning. + * It's unconditionally permitted by default. + * + * Because `true` is optional, the intent is to improve readability + * by reducing the amount of code and keeping it simple. + */ + 'react/jsx-boolean-value': 'warn', + /** + * Allows explicit string value notation in JSX notation with a + * warning. The default is unconditional permission. + * + * String values can be described simply as constant attributes + * intended to reduce code and improve readability. + */ + 'react/jsx-curly-brace-presence': 'warn', + /** + * Starting with React 17, the `React` import in JSX/TSX is no longer + * required, and this rule is no longer necessary. + * + * The default is to force it in JSX/TSX. + * @see {@link https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html} + */ + 'react/react-in-jsx-scope': 'off', + /** + * Allows explicit notation of the absence of child elements in JSX + * notation with a warning. The default is unconditional permission. + * + * Self-contained tags are intended to reduce the amount of code, + * keep things more straightforward, and improve readability. + */ + 'react/self-closing-comp': 'warn', + }, + }, +]; + +/** Configuration for ESLint to React */ +export const reactConfig: readonly Linter.Config[] = [ + ...compat.extends('plugin:react-hooks/recommended'), + ...additionalReactConfig, +]; diff --git a/packages/eslint-config-react/src/storybook.mts b/packages/eslint-config-react/src/storybook.mts new file mode 100644 index 0000000..c37d243 --- /dev/null +++ b/packages/eslint-config-react/src/storybook.mts @@ -0,0 +1,33 @@ +import type { Linter } from 'eslint'; + +/** The configuration for ESLint to Storybook */ +export const storybookConfig: readonly Linter.Config[] = [ + { + files: ['**/*.stories.?([cm])[jt]s?(x)'], + rules: { + /** + * Unconditionally allow export to `default`. + * + * The default is unknown, but since Storybook component definitions + * are structurally dependent on export to `default`, this permission + * is explicitly stated to prevent influence by later configuration + * updates. + */ + 'import/no-anonymous-default-export': 'off', + /** + * Exporting code that depends `on devDependencies is` allowed only + * for specific packages on an exception basis, and it is prohibited + * by default. + * + * To organize dependencies, only packages that directly depend on + * the product code should be listed in the dependencies list, so + * Storybook component definitions not included in the product code + * are exceptionally allowed to rely on `devDependencies`. + */ + 'n/no-unpublished-import': [ + 'error', + { allow: ['@storybook/react', '@storybook/testing-library'] }, + ], + }, + }, +]; diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json index 079beb9..4896192 100644 --- a/packages/typescript-config/package.json +++ b/packages/typescript-config/package.json @@ -40,7 +40,7 @@ "typescript-eslint-language-service": "^5.0.5" }, "peerDependencies": { - "@typescript-eslint/parser": ">=6.x.x", + "@typescript-eslint/parser": ">=7.17.x", "typescript": ">=5.x.x", "typescript-eslint-language-service": ">=5.x.x" }, diff --git a/yarn.lock b/yarn.lock index 12be2c4..581558a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -947,14 +947,18 @@ __metadata: "@eslint/js": "npm:^9.8.0" "@kurone-kito/typescript-config": "workspace:^" "@types/eslint": "npm:^9.6.0" + "@types/eslint-config-prettier": "npm:^6.11.3" + "@types/eslint-plugin-markdown": "npm:^2.0.2" "@types/eslint__eslintrc": "npm:^2.1.2" "@types/eslint__js": "npm:^8.42.3" "@types/node": "npm:^22.1.0" "@typescript-eslint/eslint-plugin": "npm:^8.0.0" "@typescript-eslint/parser": "npm:^8.0.0" + "@typescript-eslint/utils": "npm:^8.0.0" concurrently: "npm:^8.2.2" cpy-cli: "npm:^5.0.0" eslint: "npm:^9.8.0" + eslint-config-airbnb-base: "npm:^15.0.0" eslint-config-airbnb-typescript: "npm:^18.0.0" eslint-config-prettier: "npm:^9.1.0" eslint-formatter-codeframe: "npm:^7.32.1" @@ -969,62 +973,18 @@ __metadata: eslint-plugin-n: "npm:^17.10.2" eslint-plugin-oxlint: "npm:^0.5.0" eslint-plugin-yaml: "npm:^1.0.3" + globals: "npm:^15.9.0" js-yaml: "npm:^4.1.0" - prettier: "npm:^3.3.3" rimraf: "npm:^5.0.10" typescript: "npm:~5.5.4" + typescript-eslint: "npm:^8.0.0" peerDependencies: - "@cspell/eslint-plugin": ">=6.x.x" - "@typescript-eslint/eslint-plugin": ">=8.0.x" - "@typescript-eslint/parser": ">=8.0.x" eslint: ">=9.x.x" - eslint-config-airbnb-typescript: ">=17.x.x" - eslint-config-prettier: ">=8.x.x" - eslint-formatter-codeframe: ">=7.x.x" - eslint-import-resolver-node: ">=0.3.x" - eslint-import-resolver-typescript: ">=3.x.x" - eslint-plugin-editorconfig: ">=4.x.x" - eslint-plugin-import: ">=2.27.x" - eslint-plugin-jsdoc: ">=46.x.x" - eslint-plugin-json: ">=4.x.x" - eslint-plugin-markdown: ">=3.x.x" - eslint-plugin-markdownlint: ">=0.5.x" - eslint-plugin-n: ">=17.x.x" - eslint-plugin-oxlint: ">=0.2.x" + typescript: ">=5.5.x" peerDependenciesMeta: - "@cspell/eslint-plugin": - optional: true - "@typescript-eslint/eslint-plugin": - optional: true - "@typescript-eslint/parser": - optional: true eslint: optional: true - eslint-config-airbnb-typescript: - optional: true - eslint-config-prettier: - optional: true - eslint-formatter-codeframe: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-plugin-editorconfig: - optional: true - eslint-plugin-import: - optional: true - eslint-plugin-jsdoc: - optional: true - eslint-plugin-json: - optional: true - eslint-plugin-markdown: - optional: true - eslint-plugin-markdownlint: - optional: true - eslint-plugin-n: - optional: true - eslint-plugin-oxlint: + typescript: optional: true languageName: unknown linkType: soft @@ -1044,99 +1004,27 @@ __metadata: "@types/node": "npm:^22.1.0" "@typescript-eslint/eslint-plugin": "npm:^8.0.0" "@typescript-eslint/parser": "npm:^8.0.0" + "@typescript-eslint/utils": "npm:^8.0.0" concurrently: "npm:^8.2.2" cpy-cli: "npm:^5.0.0" eslint: "npm:^9.8.0" + eslint-config-airbnb: "npm:^19.0.4" eslint-config-airbnb-typescript: "npm:^18.0.0" - eslint-config-prettier: "npm:^9.1.0" eslint-formatter-codeframe: "npm:^7.32.1" - eslint-import-resolver-node: "npm:^0.3.9" - eslint-import-resolver-typescript: "npm:^3.6.1" - eslint-plugin-editorconfig: "npm:^4.0.3" eslint-plugin-import: "npm:^2.29.1" - eslint-plugin-jsdoc: "npm:^48.11.0" - eslint-plugin-json: "npm:^4.0.0" eslint-plugin-jsx-a11y: "npm:^6.9.0" - eslint-plugin-markdown: "npm:^5.1.0" - eslint-plugin-markdownlint: "npm:^0.6.0" - eslint-plugin-n: "npm:^17.10.2" - eslint-plugin-oxlint: "npm:^0.5.0" eslint-plugin-react: "npm:^7.35.0" eslint-plugin-react-hooks: "npm:^4.6.2" eslint-plugin-storybook: "npm:^0.8.0" - eslint-plugin-yaml: "npm:^1.0.3" js-yaml: "npm:^4.1.0" - prettier: "npm:^3.3.3" rimraf: "npm:^5.0.10" typescript: "npm:~5.5.4" + typescript-eslint: "npm:^8.0.0" peerDependencies: - "@cspell/eslint-plugin": ">=6.x.x" - "@typescript-eslint/eslint-plugin": ">=8.0.x" - "@typescript-eslint/parser": ">=8.0.x" eslint: ">=9.x.x" - eslint-config-airbnb-typescript: ">=17.x.x" - eslint-config-prettier: ">=8.x.x" - eslint-formatter-codeframe: ">=7.x.x" - eslint-import-resolver-node: ">=0.3.x" - eslint-import-resolver-typescript: ">=3.x.x" - eslint-plugin-editorconfig: ">=4.x.x" - eslint-plugin-import: ">=2.27.x" - eslint-plugin-jsdoc: ">=46.x.x" - eslint-plugin-json: ">=4.x.x" - eslint-plugin-jsx-a11y: ">=6.x.x" - eslint-plugin-markdown: ">=3.x.x" - eslint-plugin-markdownlint: ">=0.5.x" - eslint-plugin-n: ">=17.x.x" - eslint-plugin-oxlint: ">=0.2.x" - eslint-plugin-react: ">=7.2.x" - eslint-plugin-react-hooks: ">=4.3.x" - eslint-plugin-storybook: ">=0.6.x" - eslint-plugin-yaml: ">=0.5.x" peerDependenciesMeta: - "@cspell/eslint-plugin": - optional: true - "@typescript-eslint/eslint-plugin": - optional: true - "@typescript-eslint/parser": - optional: true eslint: optional: true - eslint-config-airbnb-typescript: - optional: true - eslint-config-prettier: - optional: true - eslint-formatter-codeframe: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-plugin-editorconfig: - optional: true - eslint-plugin-import: - optional: true - eslint-plugin-jsdoc: - optional: true - eslint-plugin-json: - optional: true - eslint-plugin-jsx-a11y: - optional: true - eslint-plugin-markdown: - optional: true - eslint-plugin-markdownlint: - optional: true - eslint-plugin-n: - optional: true - eslint-plugin-oxlint: - optional: true - eslint-plugin-react: - optional: true - eslint-plugin-react-hooks: - optional: true - eslint-plugin-storybook: - optional: true - eslint-plugin-yaml: - optional: true languageName: unknown linkType: soft @@ -1223,7 +1111,7 @@ __metadata: typescript: "npm:~5.5.4" typescript-eslint-language-service: "npm:^5.0.5" peerDependencies: - "@typescript-eslint/parser": ">=6.x.x" + "@typescript-eslint/parser": ">=7.17.x" typescript: ">=5.x.x" typescript-eslint-language-service: ">=5.x.x" peerDependenciesMeta: @@ -1400,6 +1288,23 @@ __metadata: languageName: node linkType: hard +"@types/eslint-config-prettier@npm:^6.11.3": + version: 6.11.3 + resolution: "@types/eslint-config-prettier@npm:6.11.3" + checksum: 10/b69ad5d7452f614450fcaf78b4055cfb11afb632f1ef292a3229cb5ac9a7041106a85cf634c570fbd3bb9db59c8fee7ca0e32a059e6fcad2477e22d81d5c3ef3 + languageName: node + linkType: hard + +"@types/eslint-plugin-markdown@npm:^2.0.2": + version: 2.0.2 + resolution: "@types/eslint-plugin-markdown@npm:2.0.2" + dependencies: + "@types/eslint": "npm:*" + "@types/unist": "npm:*" + checksum: 10/7711de5489e9fc859552a11e2afdf680a4585c6e4cacb497f7c51e51dc5331daf3e8e3c967d483d23897fd4825e71a27e53e892e086d94e4e643825ff9f9170f + languageName: node + linkType: hard + "@types/eslint@npm:*, @types/eslint@npm:^9.6.0": version: 9.6.0 resolution: "@types/eslint@npm:9.6.0" @@ -1506,6 +1411,13 @@ __metadata: languageName: node linkType: hard +"@types/unist@npm:*": + version: 3.0.2 + resolution: "@types/unist@npm:3.0.2" + checksum: 10/3d04d0be69316e5f14599a0d993a208606c12818cf631fd399243d1dc7a9bd8a3917d6066baa6abc290814afbd744621484756803c80cba892c39cd4b4a85616 + languageName: node + linkType: hard + "@types/unist@npm:^2, @types/unist@npm:^2.0.2": version: 2.0.10 resolution: "@types/unist@npm:2.0.10" @@ -1513,7 +1425,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^8.0.0": +"@typescript-eslint/eslint-plugin@npm:8.0.0, @typescript-eslint/eslint-plugin@npm:^8.0.0": version: 8.0.0 resolution: "@typescript-eslint/eslint-plugin@npm:8.0.0" dependencies: @@ -1536,7 +1448,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:^8.0.0": +"@typescript-eslint/parser@npm:8.0.0, @typescript-eslint/parser@npm:^8.0.0": version: 8.0.0 resolution: "@typescript-eslint/parser@npm:8.0.0" dependencies: @@ -1640,7 +1552,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.0.0": +"@typescript-eslint/utils@npm:8.0.0, @typescript-eslint/utils@npm:^8.0.0": version: 8.0.0 resolution: "@typescript-eslint/utils@npm:8.0.0" dependencies: @@ -3336,6 +3248,23 @@ __metadata: languageName: node linkType: hard +"eslint-config-airbnb@npm:^19.0.4": + version: 19.0.4 + resolution: "eslint-config-airbnb@npm:19.0.4" + dependencies: + eslint-config-airbnb-base: "npm:^15.0.0" + object.assign: "npm:^4.1.2" + object.entries: "npm:^1.1.5" + peerDependencies: + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.3 + eslint-plugin-jsx-a11y: ^6.5.1 + eslint-plugin-react: ^7.28.0 + eslint-plugin-react-hooks: ^4.3.0 + checksum: 10/f2086523cfd20c42fd620c757281bd028aa8ce9dadc7293c5c23ea60947a2d3ca04404ede77b40f5a65250fe3c04502acafc4f2f6946819fe6c257d76d9644e5 + languageName: node + linkType: hard + "eslint-config-prettier@npm:^9.1.0": version: 9.1.0 resolution: "eslint-config-prettier@npm:9.1.0" @@ -4237,7 +4166,7 @@ __metadata: languageName: node linkType: hard -"globals@npm:^15.8.0": +"globals@npm:^15.8.0, globals@npm:^15.9.0": version: 15.9.0 resolution: "globals@npm:15.9.0" checksum: 10/19bca70131c5d3e0d4171deed0f8ae16adda19f18d39b67421056f1eaa160b4433c3ffc8eb69b8b19adebbbdad4834d8a0494c5fe1ae295f0f769a5c0331d794 @@ -6933,6 +6862,20 @@ __metadata: languageName: node linkType: hard +"typescript-eslint@npm:^8.0.0": + version: 8.0.0 + resolution: "typescript-eslint@npm:8.0.0" + dependencies: + "@typescript-eslint/eslint-plugin": "npm:8.0.0" + "@typescript-eslint/parser": "npm:8.0.0" + "@typescript-eslint/utils": "npm:8.0.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/128c1ff75dc6e6e79fbd93aff615a0c2a7abeeca507bc667689d805a1498e8c671277dbd3f31396fafad633ed82b6f7300686a6dea355e6f99f9c7dfb8918a21 + languageName: node + linkType: hard + "typescript@npm:~5.5.4": version: 5.5.4 resolution: "typescript@npm:5.5.4"