如何基于ESLint扁平配置构建自定义规则包并用于Next.js项目
问题描述
我有一个名为K的TypeScript Next.js项目,当前使用内置Next.js ESLint规则并继承包含Canonical规则的第三方包。我希望实现两个目标:
- 用TypeScript创建一个采用ESLint扁平配置的第三方包,整合Next.js ESLint规则与部分Canonical等规则;
- 将K项目切换为ESLint扁平配置,本地不定义任何规则,完全导入新包的规则。
我已经查阅了ESLint新配置系统、Next.js ESLint配置相关文档,但仍未找到解决方案。以下是我编译为eslint.config.js的src/eslint.config.ts代码:
import path from 'path'; import { fileURLToPath } from 'url'; import { FlatCompat } from '@eslint/eslintrc'; import typeScriptEslintPlugin from '@typescript-eslint/eslint-plugin'; import tsParser from '@typescript-eslint/parser'; import { type Linter } from 'eslint'; import functional from 'eslint-plugin-functional'; import globals from 'globals'; // import eslintPluginImport from 'eslint-plugin-import'; // Seems unnecessary. Maybe it's included in the 'canonical'? // mimic CommonJS variables -- not needed if using CommonJS const filename = fileURLToPath(import.meta.url); const dirname = path.dirname(filename); const compat = new FlatCompat({ baseDirectory: dirname, }); const customRules = { '@babel/object-curly-spacing': 'off', 'array-bracket-newline': 'off', 'array-element-newline': 'off', 'func-style': 'off', 'line-comment-position': 'off', // https://eslint.org/docs/latest/rules/max-len 'max-len': ['warn', { code: 150, ignoreComments: false, ignoreRegExpLiterals: true, ignoreStrings: true, ignoreTemplateLiterals: true }], // https://eslint.org/docs/rules/max-lines 'max-lines': ['error', { max: 300, skipBlankLines: true, skipComments: true }], // https://eslint.org/docs/rules/max-lines-per-function 'max-lines-per-function': ['error', { max: 30, skipBlankLines: true, skipComments: true }], // https://eslint.org/docs/latest/rules/no-console // 'no-console': ['error', { allow: ['warn', 'error'] }], 'no-console': ['warn'], 'no-inline-comments': 'off', 'no-warning-comments': 'off', 'object-curly-newline': 'off', 'object-property-newline': 'off', 'react-hooks/exhaustive-deps': [ 'warn', { additionalHooks: '(useRecoilCallback|useRecoilTransaction_UNSTABLE)', // https://recoiljs.org/docs/introduction/installation#eslint }, ], }; const config: Linter.Config = [ ...compat.extends('@next/next/recommended', 'canonical/prettier'), // TODO: eslint rules for jest, if the Next.js rules don't handle it. ...compat.extends('canonical', 'canonical/prettier'), ...compat.extends('canonical/json', 'canonical/prettier').map((item: Linter.Config) => { return { ...item, files: ['*.json'], }; }), ...compat.extends('canonical/yaml', 'canonical/prettier').map((item: Linter.Config) => { return { ...item, files: ['*.yaml'], }; }), // TODO: This section seems to conflict with the `tsParser` section. What's the best way to combine them or eliminate whatever isn't necessary? // ...compat.extends('canonical/typescript', 'canonical/prettier').map((item: Linter.Config) => { // return { // ...item, // files: ['*.ts'], // languageOptions: { // parser: tsParser, // Is there a way to find this within `item` instead of importing it separately? // parserOptions: { // project: './tsconfig.json', // }, // }, // }; // }), { files: ['{**/{index,_app}.tsx,next.config.js,**/eslint.config.js,**/eslint.config.ts,pages/*.tsx}'], rules: { 'canonical/filename-match-exported': 'off', }, }, { rules: { 'canonical/destructuring-property-newline': 'off', 'canonical/import-specifier-newline': 'off', 'import/extensions': [ 'error', { css: 'always', js: 'never', json: 'always', jsx: 'never', mjs: 'never', ts: 'never', tsx: 'never', }, ], 'import/no-unassigned-import': [ 'error', { allow: ['**/*.css'], }, ], 'import/order': [ 'error', { alphabetize: { caseInsensitive: false, order: 'asc', }, groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 'newlines-between': 'always', warnOnUnassignedImports: false, }, ], 'jsdoc/valid-types': 'off', }, }, { rules: { 'import/extensions': [ // https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/extensions.md#importextensions 'off', { 'nextauth]': 'never', }, ], }, }, { ignores: ['.next/*'], }, { files: ['**/*.ts', '**/*.tsx'], languageOptions: { parser: tsParser, parserOptions: { ecmaFeatures: { modules: true, }, ecmaVersion: 'latest', project: './tsconfig.json', }, }, plugins: { '@typescript-eslint': typeScriptEslintPlugin, functional, // import: eslintPluginImport, ts: typeScriptEslintPlugin, }, rules: { ...typeScriptEslintPlugin.configs['eslint-recommended'].rules, ...typeScriptEslintPlugin.configs.recommended.rules, 'ts/return-await': 'error', // https://typescript-eslint.io/rules/return-await/ }, }, { files: ['*.json'], rules: { 'jsonc/sort-keys': 'off', }, }, { files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], languageOptions: { globals: { ...globals.browser, ...globals.node, }, }, rules: { ...customRules, }, }, ]; const innerConfigs: Linter.Config = Object.values(config); for (const innerConfig of innerConfigs) { console.log(innerConfig.rules); } export default config;
解决方案
一、创建ESLint扁平配置第三方包
1. 初始化包结构
新建一个npm包项目,比如命名为eslint-config-k-custom:
mkdir eslint-config-k-custom cd eslint-config-k-custom npm init -y
2. 安装核心依赖
npm install eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-functional globals @eslint/eslintrc eslint-config-canonical @next/eslint-plugin-next typescript --save-dev
3. 编写TypeScript扁平配置
在src/eslint.config.ts中编写优化后的配置:
import type { Linter } from 'eslint'; import { FlatCompat } from '@eslint/eslintrc'; import path from 'path'; import { fileURLToPath } from 'url'; import globals from 'globals'; import tsParser from '@typescript-eslint/parser'; import typeScriptEslintPlugin from '@typescript-eslint/eslint-plugin'; import functional from 'eslint-plugin-functional'; import nextPlugin from '@next/eslint-plugin-next'; // 处理模块路径 const filename = fileURLToPath(import.meta.url); const dirname = path.dirname(filename); const compat = new FlatCompat({ baseDirectory: dirname }); // 自定义规则集合 const customRules = { '@babel/object-curly-spacing': 'off', 'array-bracket-newline': 'off', 'array-element-newline': 'off', 'func-style': 'off', 'line-comment-position': 'off', 'max-len': ['warn', { code: 150, ignoreComments: false, ignoreRegExpLiterals: true, ignoreStrings: true, ignoreTemplateLiterals: true }], 'max-lines': ['error', { max: 300, skipBlankLines: true, skipComments: true }], 'max-lines-per-function': ['error', { max: 30, skipBlankLines: true, skipComments: true }], 'no-console': ['warn'], 'no-inline-comments': 'off', 'no-warning-comments': 'off', 'object-curly-newline': 'off', 'object-property-newline': 'off', 'react-hooks/exhaustive-deps': [ 'warn', { additionalHooks: '(useRecoilCallback|useRecoilTransaction_UNSTABLE)' } ], 'canonical/filename-match-exported': 'off', 'canonical/destructuring-property-newline': 'off', 'canonical/import-specifier-newline': 'off', 'import/extensions': [ 'error', { css: 'always', js: 'never', json: 'always', jsx: 'never', mjs: 'never', ts: 'never', tsx: 'never' } ], 'import/no-unassigned-import': ['error', { allow: ['**/*.css'] }], 'import/order': [ 'error', { alphabetize: { caseInsensitive: false, order: 'asc' }, groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 'newlines-between': 'always', warnOnUnassignedImports: false } ], 'jsdoc/valid-types': 'off', 'jsonc/sort-keys': 'off', '@typescript-eslint/return-await': 'error' }; const config: Linter.FlatConfig[] = [ // 全局环境配置 { files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], languageOptions: { globals: { ...globals.browser, ...globals.node }, ecmaVersion: 'latest', sourceType: 'module' } }, // Next.js规则(直接使用官方插件) { files: ['**/*.tsx', '**/*.jsx'], plugins: { '@next/next': nextPlugin }, rules: { ...nextPlugin.configs.recommended.rules, ...nextPlugin.configs['core-web-vitals'].rules } }, // Canonical基础规则(通过FlatCompat转换传统配置) ...compat.extends('canonical', 'canonical/prettier'), // Canonical JSON规则 ...compat.extends('canonical/json', 'canonical/prettier').map(item => ({ ...item, files: ['*.json'] })), // Canonical YAML规则 ...compat.extends('canonical/yaml', 'canonical/prettier').map(item => ({ ...item, files: ['*.yaml'] })), // TypeScript规则配置 { files: ['**/*.ts', '**/*.tsx'], languageOptions: { parser: tsParser, parserOptions: { projectService: true, // 支持多tsconfig场景,替代固定project路径 tsconfigRootDir: process.cwd() } }, plugins: { '@typescript-eslint': typeScriptEslintPlugin, functional }, rules: { ...typeScriptEslintPlugin.configs['eslint-recommended'].rules, ...typeScriptEslintPlugin.configs.recommended.rules, ...typeScriptEslintPlugin.configs['strict-type-checked'].rules, ...functional.configs.recommended.rules } }, // 忽略文件 { ignores: ['.next/*', 'node_modules/*'] }, // 自定义规则覆盖 { rules: customRules }, // 特定文件规则例外 { files: ['{**/{index,_app}.tsx,next.config.js,**/eslint.config.js,**/eslint.config.ts,pages/*.tsx}'], rules: { 'canonical/filename-match-exported': 'off' } } ]; export default config;
4. 配置打包与发布
- 在
package.json中添加以下配置:
{ "name": "eslint-config-k-custom", "version": "1.0.0", "type": "module", "main": "./dist/eslint.config.js", "types": "./dist/eslint.config.d.ts", "scripts": { "build": "tsc", "prepublishOnly": "npm run build" }, "files": ["dist"], "peerDependencies": { "eslint": "^8.0.0", "typescript": "^5.0.0" } }
- 创建
tsconfig.json:
{ "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", "declaration": true, "outDir": "./dist", "strict": true, "esModuleInterop": true }, "include": ["src/**/*"] }
- 执行
npm run build编译代码,之后可发布到npm或本地link测试。
二、迁移K项目到扁平配置
1. 安装自定义配置包
本地测试可使用link:
cd ../K npm link ../eslint-config-k-custom
已发布到npm则直接安装:
npm install eslint-config-k-custom --save-dev
2. 替换ESLint配置文件
删除项目中原有的.eslintrc系列文件,创建eslint.config.js:
import kCustomConfig from 'eslint-config-k-custom'; export default kCustomConfig;
3. 更新lint脚本(可选)
在项目package.json中确保脚本正确:
{ "scripts": { "lint": "eslint .", "lint:fix": "eslint . --fix" } }
4. 验证配置
执行npm run lint,确认规则正常生效,且本地无自定义规则。
关键优化说明
- Next.js规则适配:扁平配置下直接使用
@next/eslint-plugin-next的规则集合,无需通过compat.extends转换传统配置。 - TypeScript解析优化:使用
projectService: true替代固定project路径,支持项目中多tsconfig场景。 - 规则合并顺序:自定义规则放在最后,确保能覆盖Canonical和Next.js的默认规则。
- 冗余代码清理:移除原代码中重复规则定义与调试用的
console.log。
内容的提问来源于stack exchange,提问作者Ryan
相关产品推荐
相关产品推荐

