You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于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,确认规则正常生效,且本地无自定义规则。


关键优化说明

  1. Next.js规则适配:扁平配置下直接使用@next/eslint-plugin-next的规则集合,无需通过compat.extends转换传统配置。
  2. TypeScript解析优化:使用projectService: true替代固定project路径,支持项目中多tsconfig场景。
  3. 规则合并顺序:自定义规则放在最后,确保能覆盖Canonical和Next.js的默认规则。
  4. 冗余代码清理:移除原代码中重复规则定义与调试用的console.log。

内容的提问来源于stack exchange,提问作者Ryan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 09:37:02