ESLint 工程化实践指南
更新: 8/27/2025 字数: 0 字 时长: 0 分钟
深入探讨 ESLint 在现代前端工程化中的应用,从基础配置到高级定制,打造团队级代码质量保障体系。
ESLint 9+ 升级指南
重大变更概览
配置文件迁移
从 .eslintrc.js 迁移到 eslint.config.js
javascript
// 旧配置 (.eslintrc.js)
module.exports = {
env: {
browser: true,
es2021: true,
node: true
},
extends: [
'eslint:recommended',
'@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended'
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true
},
ecmaVersion: 'latest',
sourceType: 'module'
},
plugins: [
'react',
'@typescript-eslint',
'react-hooks'
],
rules: {
'indent': ['error', 2],
'linebreak-style': ['error', 'unix'],
'quotes': ['error', 'single'],
'semi': ['error', 'always']
},
settings: {
react: {
version: 'detect'
}
}
};
javascript
// 新配置 (eslint.config.js)
import js from '@eslint/js';
import typescript from '@typescript-eslint/eslint-plugin';
import typescriptParser from '@typescript-eslint/parser';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import globals from 'globals';
export default [
// 基础 JavaScript 推荐配置
js.configs.recommended,
// 全局配置
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
...globals.es2021
},
ecmaVersion: 'latest',
sourceType: 'module'
}
},
// TypeScript 配置
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaFeatures: {
jsx: true
}
}
},
plugins: {
'@typescript-eslint': typescript
},
rules: {
...typescript.configs.recommended.rules,
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/no-explicit-any': 'warn'
}
},
// React 配置
{
files: ['**/*.{jsx,tsx}'],
plugins: {
react,
'react-hooks': reactHooks
},
rules: {
...react.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'react/react-in-jsx-scope': 'off', // React 17+
'react/prop-types': 'off' // 使用 TypeScript
},
settings: {
react: {
version: 'detect'
}
}
},
// 通用规则
{
rules: {
'indent': ['error', 2],
'linebreak-style': ['error', 'unix'],
'quotes': ['error', 'single'],
'semi': ['error', 'always'],
'no-console': 'warn',
'no-debugger': 'error'
}
},
// 忽略文件
{
ignores: [
'dist/**',
'build/**',
'node_modules/**',
'*.min.js',
'coverage/**'
]
}
];
迁移工具和脚本
javascript
// migrate-eslint.js - 自动迁移脚本
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { join } from 'path';
class ESLintMigrator {
constructor(projectPath) {
this.projectPath = projectPath;
this.oldConfigFiles = [
'.eslintrc.js',
'.eslintrc.json',
'.eslintrc.yml',
'.eslintrc.yaml'
];
}
findOldConfig() {
for (const file of this.oldConfigFiles) {
const filePath = join(this.projectPath, file);
if (existsSync(filePath)) {
return { file, path: filePath };
}
}
return null;
}
parseOldConfig(configPath) {
const content = readFileSync(configPath, 'utf8');
if (configPath.endsWith('.js')) {
// 动态导入旧配置
return require(configPath);
} else if (configPath.endsWith('.json')) {
return JSON.parse(content);
}
// 处理 YAML 格式...
}
convertToFlatConfig(oldConfig) {
const flatConfig = [];
// 转换基础配置
if (oldConfig.extends) {
flatConfig.push(this.convertExtends(oldConfig.extends));
}
// 转换环境配置
if (oldConfig.env) {
flatConfig.push(this.convertEnv(oldConfig.env));
}
// 转换解析器配置
if (oldConfig.parser || oldConfig.parserOptions) {
flatConfig.push(this.convertParser(oldConfig));
}
// 转换插件和规则
if (oldConfig.plugins || oldConfig.rules) {
flatConfig.push(this.convertPluginsAndRules(oldConfig));
}
return flatConfig;
}
convertExtends(extendsConfig) {
const extends = Array.isArray(extendsConfig) ? extendsConfig : [extendsConfig];
const imports = [];
const configs = [];
extends.forEach(ext => {
if (ext === 'eslint:recommended') {
imports.push("import js from '@eslint/js';");
configs.push('js.configs.recommended');
} else if (ext.startsWith('@typescript-eslint')) {
imports.push("import typescript from '@typescript-eslint/eslint-plugin';");
configs.push('typescript.configs.recommended');
}
// 处理其他 extends...
});
return { imports, configs };
}
generateNewConfig(flatConfig) {
return `// ESLint 9+ Flat Config
// 自动从旧配置迁移生成
${flatConfig.imports.join('\n')}
export default [
${flatConfig.configs.map(config => ` ${config},`).join('\n')}
// 自定义规则
{
rules: {
${Object.entries(flatConfig.rules || {})
.map(([rule, value]) => ` '${rule}': ${JSON.stringify(value)}`)
.join(',\n')}
}
}
];
`;
}
migrate() {
const oldConfig = this.findOldConfig();
if (!oldConfig) {
console.log('未找到旧的 ESLint 配置文件');
return;
}
console.log(`发现旧配置文件: ${oldConfig.file}`);
const parsedConfig = this.parseOldConfig(oldConfig.path);
const flatConfig = this.convertToFlatConfig(parsedConfig);
const newConfigContent = this.generateNewConfig(flatConfig);
const newConfigPath = join(this.projectPath, 'eslint.config.js');
writeFileSync(newConfigPath, newConfigContent);
console.log('✅ 迁移完成!新配置文件: eslint.config.js');
console.log('⚠️ 请手动检查并调整配置,然后删除旧配置文件');
}
}
// 使用示例
const migrator = new ESLintMigrator(process.cwd());
migrator.migrate();
前端工程规范体系
代码规范
1. JavaScript/TypeScript 规范
javascript
// eslint.config.js - JavaScript/TypeScript 规范配置
export default [
{
name: 'javascript-standards',
files: ['**/*.{js,mjs,cjs}'],
rules: {
// 代码风格
'indent': ['error', 2, {
SwitchCase: 1,
VariableDeclarator: 1,
outerIIFEBody: 1,
FunctionDeclaration: { parameters: 1, body: 1 },
FunctionExpression: { parameters: 1, body: 1 },
CallExpression: { arguments: 1 },
ArrayExpression: 1,
ObjectExpression: 1,
ImportDeclaration: 1,
flatTernaryExpressions: false,
ignoreComments: false
}],
'quotes': ['error', 'single', {
avoidEscape: true,
allowTemplateLiterals: true
}],
'semi': ['error', 'always'],
'comma-dangle': ['error', 'never'],
'comma-spacing': ['error', { before: false, after: true }],
'comma-style': ['error', 'last'],
'key-spacing': ['error', { beforeColon: false, afterColon: true }],
'object-curly-spacing': ['error', 'always'],
'array-bracket-spacing': ['error', 'never'],
'space-before-blocks': ['error', 'always'],
'space-before-function-paren': ['error', {
anonymous: 'always',
named: 'never',
asyncArrow: 'always'
}],
// 变量声明
'no-var': 'error',
'prefer-const': 'error',
'no-unused-vars': ['error', {
vars: 'all',
args: 'after-used',
ignoreRestSiblings: true
}],
'no-undef': 'error',
'no-redeclare': 'error',
// 函数
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
'prefer-arrow-callback': 'error',
'arrow-spacing': ['error', { before: true, after: true }],
'arrow-parens': ['error', 'as-needed'],
// 对象和数组
'object-shorthand': ['error', 'always'],
'prefer-destructuring': ['error', {
array: true,
object: true
}, {
enforceForRenamedProperties: false
}],
// 字符串
'prefer-template': 'error',
'template-curly-spacing': ['error', 'never'],
// 最佳实践
'eqeqeq': ['error', 'always'],
'no-eval': 'error',
'no-implied-eval': 'error',
'no-new-func': 'error',
'no-return-assign': 'error',
'no-sequences': 'error',
'no-throw-literal': 'error',
'no-unmodified-loop-condition': 'error',
'no-unused-expressions': 'error',
'no-useless-call': 'error',
'no-useless-concat': 'error',
'no-useless-return': 'error',
'prefer-promise-reject-errors': 'error',
'require-await': 'error',
// ES6+
'no-duplicate-imports': 'error',
'no-useless-computed-key': 'error',
'no-useless-constructor': 'error',
'no-useless-rename': 'error',
'rest-spread-spacing': ['error', 'never'],
'symbol-description': 'error'
}
},
{
name: 'typescript-standards',
files: ['**/*.{ts,tsx}'],
languageOptions: {
parser: typescriptParser
},
plugins: {
'@typescript-eslint': typescript
},
rules: {
// TypeScript 特定规则
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/prefer-nullish-coalescing': 'error',
'@typescript-eslint/prefer-optional-chain': 'error',
'@typescript-eslint/prefer-as-const': 'error',
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/no-unnecessary-type-constraint': 'error',
'@typescript-eslint/ban-ts-comment': ['error', {
'ts-expect-error': 'allow-with-description',
'ts-ignore': false,
'ts-nocheck': false,
'ts-check': false
}],
// 命名约定
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'variableLike',
format: ['camelCase', 'PascalCase', 'UPPER_CASE']
},
{
selector: 'function',
format: ['camelCase', 'PascalCase']
},
{
selector: 'typeLike',
format: ['PascalCase']
},
{
selector: 'interface',
format: ['PascalCase'],
prefix: ['I']
},
{
selector: 'enum',
format: ['PascalCase']
},
{
selector: 'enumMember',
format: ['UPPER_CASE']
}
],
// 类型定义
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
'@typescript-eslint/consistent-type-imports': ['error', {
prefer: 'type-imports',
disallowTypeAnnotations: false
}],
'@typescript-eslint/explicit-function-return-type': ['warn', {
allowExpressions: true,
allowTypedFunctionExpressions: true
}],
'@typescript-eslint/explicit-module-boundary-types': 'warn'
}
}
];
2. React 组件规范
javascript
// React 专用配置
export const reactConfig = {
name: 'react-standards',
files: ['**/*.{jsx,tsx}'],
plugins: {
react,
'react-hooks': reactHooks,
'jsx-a11y': jsxA11y
},
rules: {
// React 基础规则
'react/react-in-jsx-scope': 'off', // React 17+
'react/prop-types': 'off', // 使用 TypeScript
'react/display-name': 'error',
'react/no-array-index-key': 'warn',
'react/no-danger': 'warn',
'react/no-deprecated': 'error',
'react/no-direct-mutation-state': 'error',
'react/no-find-dom-node': 'error',
'react/no-is-mounted': 'error',
'react/no-render-return-value': 'error',
'react/no-string-refs': 'error',
'react/no-unescaped-entities': 'error',
'react/no-unknown-property': 'error',
'react/no-unsafe': 'error',
'react/require-render-return': 'error',
// JSX 规则
'react/jsx-boolean-value': ['error', 'never'],
'react/jsx-closing-bracket-location': ['error', 'line-aligned'],
'react/jsx-closing-tag-location': 'error',
'react/jsx-curly-spacing': ['error', 'never'],
'react/jsx-equals-spacing': ['error', 'never'],
'react/jsx-first-prop-new-line': ['error', 'multiline-multiprop'],
'react/jsx-indent': ['error', 2],
'react/jsx-indent-props': ['error', 2],
'react/jsx-key': ['error', { checkFragmentShorthand: true }],
'react/jsx-max-props-per-line': ['error', { maximum: 1, when: 'multiline' }],
'react/jsx-no-bind': ['error', {
ignoreRefs: true,
allowArrowFunctions: true,
allowFunctions: false,
allowBind: false,
ignoreDOMComponents: true
}],
'react/jsx-no-duplicate-props': 'error',
'react/jsx-no-literals': 'off',
'react/jsx-no-undef': 'error',
'react/jsx-pascal-case': 'error',
'react/jsx-props-no-spreading': ['warn', {
html: 'enforce',
custom: 'enforce',
explicitSpread: 'ignore'
}],
'react/jsx-tag-spacing': ['error', {
closingSlash: 'never',
beforeSelfClosing: 'always',
afterOpening: 'never',
beforeClosing: 'never'
}],
'react/jsx-uses-react': 'error',
'react/jsx-uses-vars': 'error',
'react/jsx-wrap-multilines': ['error', {
declaration: 'parens-new-line',
assignment: 'parens-new-line',
return: 'parens-new-line',
arrow: 'parens-new-line',
condition: 'parens-new-line',
logical: 'parens-new-line',
prop: 'parens-new-line'
}],
// Hooks 规则
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// 无障碍性规则
'jsx-a11y/alt-text': 'error',
'jsx-a11y/anchor-has-content': 'error',
'jsx-a11y/anchor-is-valid': 'error',
'jsx-a11y/aria-activedescendant-has-tabindex': 'error',
'jsx-a11y/aria-props': 'error',
'jsx-a11y/aria-proptypes': 'error',
'jsx-a11y/aria-role': 'error',
'jsx-a11y/aria-unsupported-elements': 'error',
'jsx-a11y/click-events-have-key-events': 'error',
'jsx-a11y/heading-has-content': 'error',
'jsx-a11y/html-has-lang': 'error',
'jsx-a11y/img-redundant-alt': 'error',
'jsx-a11y/interactive-supports-focus': 'error',
'jsx-a11y/label-has-associated-control': 'error',
'jsx-a11y/mouse-events-have-key-events': 'error',
'jsx-a11y/no-access-key': 'error',
'jsx-a11y/no-autofocus': 'error',
'jsx-a11y/no-distracting-elements': 'error',
'jsx-a11y/no-interactive-element-to-noninteractive-role': 'error',
'jsx-a11y/no-noninteractive-element-interactions': 'error',
'jsx-a11y/no-noninteractive-element-to-interactive-role': 'error',
'jsx-a11y/no-redundant-roles': 'error',
'jsx-a11y/no-static-element-interactions': 'error',
'jsx-a11y/role-has-required-aria-props': 'error',
'jsx-a11y/role-supports-aria-props': 'error',
'jsx-a11y/scope': 'error',
'jsx-a11y/tabindex-no-positive': 'error'
}
};
文件结构规范
javascript
// file-structure.config.js - 文件结构规范配置
export default [
{
name: 'file-structure-rules',
plugins: {
'file-structure': fileStructurePlugin
},
rules: {
// 文件命名规范
'file-structure/naming-convention': ['error', {
// 组件文件使用 PascalCase
'src/components/**/*.{tsx,jsx}': 'PascalCase',
// 页面文件使用 PascalCase
'src/pages/**/*.{tsx,jsx}': 'PascalCase',
// Hook 文件使用 camelCase,以 use 开头
'src/hooks/**/*.{ts,tsx}': '^use[A-Z].*',
// 工具函数使用 camelCase
'src/utils/**/*.{ts,js}': 'camelCase',
// 类型定义文件使用 camelCase
'src/types/**/*.{ts}': 'camelCase',
// 常量文件使用 UPPER_CASE
'src/constants/**/*.{ts,js}': 'UPPER_CASE',
// 配置文件使用 kebab-case
'config/**/*.{ts,js,json}': 'kebab-case',
// 测试文件
'**/*.{test,spec}.{ts,tsx,js,jsx}': 'camelCase'
}],
// 目录结构规范
'file-structure/folder-structure': ['error', {
// 强制的目录结构
required: [
'src/',
'src/components/',
'src/pages/',
'src/hooks/',
'src/utils/',
'src/types/',
'src/constants/',
'src/assets/',
'src/styles/',
'tests/',
'docs/'
],
// 禁止的目录结构
forbidden: [
'src/component/', // 应该是 components
'src/page/', // 应该是 pages
'src/util/', // 应该是 utils
'src/type/' // 应该是 types
]
}],
// 文件大小限制
'file-structure/max-file-size': ['warn', {
maxSize: 500, // 500 行
exclude: [
'**/*.config.{js,ts}',
'**/*.d.ts',
'**/index.{js,ts}'
]
}],
// 导入路径规范
'file-structure/import-path': ['error', {
// 强制使用绝对路径导入
'src/**/*.{ts,tsx,js,jsx}': {
// 同级目录可以使用相对路径
allowRelative: 'same-folder',
// 其他情况使用绝对路径
preferAbsolute: true,
// 路径别名配置
aliases: {
'@': 'src',
'@components': 'src/components',
'@pages': 'src/pages',
'@hooks': 'src/hooks',
'@utils': 'src/utils',
'@types': 'src/types',
'@constants': 'src/constants',
'@assets': 'src/assets',
'@styles': 'src/styles'
}
}
}]
}
},
// 组件文件结构规范
{
name: 'component-structure',
files: ['src/components/**/*.{tsx,jsx}'],
rules: {
// 组件导出规范
'file-structure/component-export': ['error', {
// 强制默认导出
defaultExport: true,
// 组件名必须与文件名一致
matchFileName: true,
// 允许的导出模式
allowedPatterns: [
// 默认导出 + 命名导出类型
'export default Component; export type { ComponentProps };',
// 仅默认导出
'export default Component;'
]
}),
// 组件内部结构顺序
'file-structure/component-order': ['error', {
order: [
'imports',
'types',
'interfaces',
'constants',
'styled-components',
'component',
'default-export'
]
}]
}
}
];
提交规范
Conventional Commits 规范
bash
# 提交消息格式
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
类型说明:
feat
: 新功能fix
: 修复 bugdocs
: 文档更新style
: 代码格式(不影响代码运行的变动)refactor
: 重构(既不是新增功能,也不是修改 bug 的代码变动)perf
: 性能优化test
: 增加测试chore
: 构建过程或辅助工具的变动ci
: CI 配置文件和脚本的变动build
: 影响构建系统或外部依赖的更改revert
: 回滚之前的提交
示例:
bash
feat(auth): add user login functionality
Implement JWT-based authentication with:
- Login form validation
- Token storage in localStorage
- Auto-redirect after successful login
Closes #123
Commitlint 配置
安装依赖:
bash
npm install --save-dev @commitlint/config-conventional @commitlint/cli
commitlint.config.js:
javascript
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat',
'fix',
'docs',
'style',
'refactor',
'perf',
'test',
'chore',
'ci',
'build',
'revert'
]
],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'scope-case': [2, 'always', 'lower-case'],
'subject-case': [2, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
'header-max-length': [2, 'always', 72],
'body-leading-blank': [1, 'always'],
'body-max-line-length': [2, 'always', 100],
'footer-leading-blank': [1, 'always'],
'footer-max-line-length': [2, 'always', 100]
}
};
Husky 工作流配置
安装和初始化
bash
# 安装 Husky
npm install --save-dev husky
# 初始化 Husky
npx husky install
# 添加 prepare 脚本(确保团队成员安装依赖时自动启用 hooks)
npm pkg set scripts.prepare="husky install"
Git Hooks 配置
pre-commit Hook
bash
# 创建 pre-commit hook
npx husky add .husky/pre-commit "npm run lint-staged"
lint-staged 配置(package.json):
json
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{css,scss,less}": [
"stylelint --fix",
"prettier --write"
],
"*.{json,md,yml,yaml}": [
"prettier --write"
]
}
}
commit-msg Hook
bash
# 创建 commit-msg hook
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit ${1}'
pre-push Hook
bash
# 创建 pre-push hook
npx husky add .husky/pre-push "npm run test && npm run build"
完整的 Husky 工作流
.husky/pre-commit:
bash
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# 运行 lint-staged
npm run lint-staged
# 类型检查
npm run type-check
# 运行单元测试(仅针对暂存文件)
npm run test:staged
.husky/commit-msg:
bash
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# 验证提交消息格式
npx --no -- commitlint --edit ${1}
# 检查提交消息长度和格式
node scripts/validate-commit-msg.js ${1}
.husky/pre-push:
bash
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# 运行完整测试套件
npm run test:ci
# 构建检查
npm run build
# 安全检查
npm audit --audit-level moderate
# 依赖检查
npm run check-deps
高级配置
条件性 Hook 执行
scripts/conditional-hooks.js:
javascript
const { execSync } = require('child_process');
const fs = require('fs');
class ConditionalHooks {
static shouldRunTests() {
try {
// 检查是否有测试文件变更
const changedFiles = execSync('git diff --cached --name-only', { encoding: 'utf8' });
return changedFiles.includes('.test.') || changedFiles.includes('.spec.');
} catch (error) {
return true; // 出错时默认运行
}
}
static shouldRunBuild() {
try {
const changedFiles = execSync('git diff --cached --name-only', { encoding: 'utf8' });
const buildRelatedFiles = ['.js', '.ts', '.jsx', '.tsx', '.json', '.yml', '.yaml'];
return buildRelatedFiles.some(ext => changedFiles.includes(ext));
} catch (error) {
return true;
}
}
static getBranchName() {
try {
return execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
} catch (error) {
return 'unknown';
}
}
static isProtectedBranch() {
const protectedBranches = ['main', 'master', 'develop', 'release/*'];
const currentBranch = this.getBranchName();
return protectedBranches.some(pattern => {
if (pattern.includes('*')) {
const regex = new RegExp(pattern.replace('*', '.*'));
return regex.test(currentBranch);
}
return currentBranch === pattern;
});
}
}
module.exports = ConditionalHooks;
智能 lint-staged 配置
scripts/smart-lint-staged.js:
javascript
const { execSync } = require('child_process');
const path = require('path');
class SmartLintStaged {
static getChangedFiles() {
try {
const output = execSync('git diff --cached --name-only', { encoding: 'utf8' });
return output.trim().split('\n').filter(Boolean);
} catch (error) {
return [];
}
}
static categorizeFiles(files) {
const categories = {
typescript: [],
javascript: [],
react: [],
styles: [],
configs: [],
tests: [],
docs: []
};
files.forEach(file => {
const ext = path.extname(file);
const basename = path.basename(file);
if (ext === '.ts' || ext === '.tsx') {
categories.typescript.push(file);
if (ext === '.tsx' || file.includes('component') || file.includes('Component')) {
categories.react.push(file);
}
} else if (ext === '.js' || ext === '.jsx') {
categories.javascript.push(file);
if (ext === '.jsx' || file.includes('component') || file.includes('Component')) {
categories.react.push(file);
}
} else if (['.css', '.scss', '.less', '.sass'].includes(ext)) {
categories.styles.push(file);
} else if (basename.includes('config') || basename.includes('.config.')) {
categories.configs.push(file);
} else if (file.includes('.test.') || file.includes('.spec.')) {
categories.tests.push(file);
} else if (['.md', '.mdx'].includes(ext)) {
categories.docs.push(file);
}
});
return categories;
}
static generateLintCommands(categories) {
const commands = [];
if (categories.typescript.length > 0) {
commands.push(`npx eslint ${categories.typescript.join(' ')} --fix`);
commands.push(`npx tsc --noEmit`);
}
if (categories.javascript.length > 0) {
commands.push(`npx eslint ${categories.javascript.join(' ')} --fix`);
}
if (categories.react.length > 0) {
commands.push(`npx eslint ${categories.react.join(' ')} --fix --config .eslintrc.react.js`);
}
if (categories.styles.length > 0) {
commands.push(`npx stylelint ${categories.styles.join(' ')} --fix`);
}
if (categories.tests.length > 0) {
commands.push(`npm run test -- --findRelatedTests ${categories.tests.join(' ')}`);
}
return commands;
}
static run() {
const changedFiles = this.getChangedFiles();
if (changedFiles.length === 0) {
console.log('No files to lint');
return;
}
const categories = this.categorizeFiles(changedFiles);
const commands = this.generateLintCommands(categories);
console.log(`Linting ${changedFiles.length} changed files...`);
commands.forEach(command => {
try {
console.log(`Running: ${command}`);
execSync(command, { stdio: 'inherit' });
} catch (error) {
console.error(`Command failed: ${command}`);
process.exit(1);
}
});
console.log('All lint checks passed!');
}
}
if (require.main === module) {
SmartLintStaged.run();
}
module.exports = SmartLintStaged;
javascript
// commitlint.config.js - 提交信息规范
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
// 类型枚举
'type-enum': [
2,
'always',
[
'feat', // 新功能
'fix', // 修复 bug
'docs', // 文档更新
'style', // 代码格式调整
'refactor', // 重构
'perf', // 性能优化
'test', // 测试相关
'build', // 构建相关
'ci', // CI/CD 相关
'chore', // 其他杂项
'revert' // 回滚
]
],
// 主题长度限制
'subject-max-length': [2, 'always', 50],
'subject-min-length': [2, 'always', 10],
// 主题格式
'subject-case': [2, 'always', 'lower-case'],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
// 类型格式
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
// 范围格式
'scope-case': [2, 'always', 'lower-case'],
// 正文格式
'body-leading-blank': [2, 'always'],
'body-max-line-length': [2, 'always', 100],
// 脚注格式
'footer-leading-blank': [2, 'always'],
'footer-max-line-length': [2, 'always', 100]
},
// 自定义解析器
parserPreset: {
parserOpts: {
headerPattern: /^(\w*)(?:\(([\w\$\.\-\*\s]*)\))?\:\s(.*)$/,
headerCorrespondence: ['type', 'scope', 'subject']
}
},
// 忽略规则
ignores: [
commit => commit.includes('WIP'),
commit => commit.includes('Merge')
],
// 默认忽略
defaultIgnores: true
};
javascript
// commit-msg-validator.js - 提交信息验证器
import { execSync } from 'child_process';
import chalk from 'chalk';
class CommitValidator {
constructor() {
this.types = {
feat: '✨ 新功能',
fix: '🐛 修复 bug',
docs: '📚 文档更新',
style: '💄 代码格式调整',
refactor: '♻️ 重构',
perf: '⚡ 性能优化',
test: '✅ 测试相关',
build: '📦 构建相关',
ci: '👷 CI/CD 相关',
chore: '🔧 其他杂项',
revert: '⏪ 回滚'
};
this.scopes = [
'components',
'pages',
'hooks',
'utils',
'types',
'styles',
'config',
'deps',
'release'
];
}
validateFormat(message) {
const pattern = /^(\w+)(\([\w-]+\))?: .{10,50}$/;
return pattern.test(message);
}
validateType(type) {
return Object.keys(this.types).includes(type);
}
validateScope(scope) {
if (!scope) return true; // scope 是可选的
return this.scopes.includes(scope);
}
parseCommitMessage(message) {
const match = message.match(/^(\w+)(?:\(([\w-]+)\))?: (.+)$/);
if (!match) return null;
return {
type: match[1],
scope: match[2],
subject: match[3]
};
}
validate(message) {
const errors = [];
// 基本格式验证
if (!this.validateFormat(message)) {
errors.push('提交信息格式不正确');
return { valid: false, errors };
}
const parsed = this.parseCommitMessage(message);
if (!parsed) {
errors.push('无法解析提交信息');
return { valid: false, errors };
}
// 类型验证
if (!this.validateType(parsed.type)) {
errors.push(`无效的提交类型: ${parsed.type}`);
}
// 范围验证
if (!this.validateScope(parsed.scope)) {
errors.push(`无效的提交范围: ${parsed.scope}`);
}
// 主题长度验证
if (parsed.subject.length < 10) {
errors.push('提交主题太短,至少需要 10 个字符');
}
if (parsed.subject.length > 50) {
errors.push('提交主题太长,最多 50 个字符');
}
return {
valid: errors.length === 0,
errors,
parsed
};
}
showHelp() {
console.log(chalk.blue('\n📝 提交信息格式规范:'));
console.log(chalk.gray('type(scope): subject\n'));
console.log(chalk.blue('🏷️ 可用的提交类型:'));
Object.entries(this.types).forEach(([type, desc]) => {
console.log(chalk.green(` ${type.padEnd(10)} - ${desc}`));
});
console.log(chalk.blue('\n🎯 可用的提交范围:'));
this.scopes.forEach(scope => {
console.log(chalk.green(` ${scope}`));
});
console.log(chalk.blue('\n✅ 示例:'));
console.log(chalk.green(' feat(components): add new button component'));
console.log(chalk.green(' fix(hooks): resolve memory leak in useEffect'));
console.log(chalk.green(' docs: update installation guide'));
console.log(chalk.green(' style(pages): format code with prettier'));
}
interactiveCommit() {
// 交互式提交助手
const inquirer = require('inquirer');
return inquirer.prompt([
{
type: 'list',
name: 'type',
message: '选择提交类型:',
choices: Object.entries(this.types).map(([type, desc]) => ({
name: `${type.padEnd(10)} - ${desc}`,
value: type
}))
},
{
type: 'list',
name: 'scope',
message: '选择提交范围 (可选):',
choices: [
{ name: '无', value: '' },
...this.scopes.map(scope => ({ name: scope, value: scope }))
]
},
{
type: 'input',
name: 'subject',
message: '输入提交主题 (10-50 字符):',
validate: input => {
if (input.length < 10) return '主题太短,至少需要 10 个字符';
if (input.length > 50) return '主题太长,最多 50 个字符';
return true;
}
},
{
type: 'input',
name: 'body',
message: '输入提交正文 (可选):'
}
]).then(answers => {
const { type, scope, subject, body } = answers;
const scopeStr = scope ? `(${scope})` : '';
const commitMessage = `${type}${scopeStr}: ${subject}`;
if (body) {
return `${commitMessage}\n\n${body}`;
}
return commitMessage;
});
}
}
// Git hooks 集成
function setupGitHooks() {
const validator = new CommitValidator();
// commit-msg hook
const commitMsgHook = `#!/bin/sh
# Commit message validation
node -e "
const fs = require('fs');
const { CommitValidator } = require('./scripts/commit-validator');
const message = fs.readFileSync(process.argv[1], 'utf8').trim();
const validator = new CommitValidator();
const result = validator.validate(message);
if (!result.valid) {
console.error('❌ 提交信息验证失败:');
result.errors.forEach(error => console.error(' -', error));
validator.showHelp();
process.exit(1);
}
console.log('✅ 提交信息验证通过');
"`;
// 写入 git hook
require('fs').writeFileSync('.git/hooks/commit-msg', commitMsgHook, { mode: 0o755 });
console.log('✅ Git commit-msg hook 已安装');
}
export { CommitValidator, setupGitHooks };
流程规范
javascript
// workflow.config.js - 开发流程规范
export const workflowConfig = {
// 分支管理规范
branches: {
main: {
protection: true,
requiredReviews: 2,
dismissStaleReviews: true,
requireCodeOwnerReviews: true,
requiredStatusChecks: [
'ci/lint',
'ci/test',
'ci/build',
'ci/security-scan'
]
},
develop: {
protection: true,
requiredReviews: 1,
requiredStatusChecks: [
'ci/lint',
'ci/test'
]
},
feature: {
namingPattern: /^feature\/[a-z0-9-]+$/,
baseBranch: 'develop',
autoDelete: true
},
hotfix: {
namingPattern: /^hotfix\/[a-z0-9-]+$/,
baseBranch: 'main',
autoDelete: true
},
release: {
namingPattern: /^release\/v\d+\.\d+\.\d+$/,
baseBranch: 'develop',
autoDelete: false
}
},
// PR/MR 规范
pullRequest: {
template: `## 📝 变更描述
<!-- 简要描述本次变更的内容 -->
## 🎯 变更类型
- [ ] 新功能 (feature)
- [ ] 修复 bug (fix)
- [ ] 文档更新 (docs)
- [ ] 代码重构 (refactor)
- [ ] 性能优化 (perf)
- [ ] 测试相关 (test)
- [ ] 构建相关 (build)
- [ ] 其他 (chore)
## 🧪 测试
- [ ] 单元测试已通过
- [ ] 集成测试已通过
- [ ] 手动测试已完成
- [ ] 无需测试
## 📋 检查清单
- [ ] 代码符合团队规范
- [ ] 已添加必要的注释
- [ ] 已更新相关文档
- [ ] 已考虑向后兼容性
- [ ] 已进行自测
## 🔗 相关链接
<!-- 相关的 issue、文档或其他 PR 链接 -->
## 📸 截图 (如适用)
<!-- 如果是 UI 变更,请提供截图 -->
`,
rules: {
titlePattern: /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore)(\([\w-]+\))?: .{10,50}$/,
minDescriptionLength: 50,
requiredLabels: ['type', 'priority'],
blockedLabels: ['wip', 'do-not-merge'],
requiredFiles: {
'src/**/*.{ts,tsx}': ['**/*.test.{ts,tsx}'], // 源码需要测试
'docs/**/*.md': [], // 文档不需要额外文件
'package.json': ['CHANGELOG.md'] // 依赖变更需要更新日志
}
}
},
// 代码审查规范
codeReview: {
reviewers: {
automatic: true,
algorithm: 'round-robin', // 轮询分配
minReviewers: 1,
maxReviewers: 3,
codeOwners: {
'src/components/**': ['@frontend-team'],
'src/pages/**': ['@frontend-team'],
'src/utils/**': ['@frontend-team', '@backend-team'],
'docs/**': ['@tech-writers'],
'package.json': ['@tech-leads'],
'.github/**': ['@devops-team']
}
},
guidelines: {
// 审查要点
checklist: [
'代码逻辑是否正确',
'是否遵循团队编码规范',
'是否有适当的错误处理',
'是否有性能问题',
'是否有安全隐患',
'测试覆盖率是否足够',
'文档是否需要更新',
'是否影响现有功能'
],
// 审查标准
criteria: {
functionality: '功能是否按预期工作',
readability: '代码是否易于理解',
maintainability: '代码是否易于维护',
performance: '是否有性能影响',
security: '是否存在安全问题',
testing: '测试是否充分'
}
}
},
// 发布流程
release: {
strategy: 'semantic-versioning',
branches: {
main: 'production',
develop: 'staging',
'feature/*': 'development'
},
pipeline: [
{
stage: 'pre-release',
steps: [
'lint',
'test',
'build',
'security-scan',
'dependency-check'
]
},
{
stage: 'release',
steps: [
'version-bump',
'changelog-update',
'tag-creation',
'artifact-build',
'deployment'
]
},
{
stage: 'post-release',
steps: [
'smoke-test',
'monitoring-setup',
'notification',
'documentation-update'
]
}
],
automation: {
versionBump: true,
changelogGeneration: true,
tagCreation: true,
releaseNotes: true,
notification: {
slack: true,
email: true,
webhook: true
}
}
}
};
ESLint 配置实践
基础配置 (eslint.config.js)
javascript
// eslint.config.js - ESLint v9+ 推荐配置
import js from '@eslint/js';
import typescript from '@typescript-eslint/eslint-plugin';
import typescriptParser from '@typescript-eslint/parser';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import importPlugin from 'eslint-plugin-import';
import prettier from 'eslint-plugin-prettier';
import globals from 'globals';
// 基础配置
const baseConfig = {
languageOptions: {
globals: {
...globals.browser,
...globals.node,
...globals.es2021
},
ecmaVersion: 'latest',
sourceType: 'module'
},
plugins: {
import: importPlugin,
prettier
},
rules: {
// 基础规则
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}],
// 导入规则
'import/order': ['error', {
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index'
],
'newlines-between': 'always',
alphabetize: {
order: 'asc',
caseInsensitive: true
}
}],
'import/no-unresolved': 'error',
'import/no-duplicates': 'error',
'import/no-unused-modules': 'warn',
// Prettier 集成
'prettier/prettier': ['error', {
singleQuote: true,
trailingComma: 'es5',
tabWidth: 2,
semi: true,
printWidth: 80,
bracketSpacing: true,
arrowParens: 'avoid'
}]
},
settings: {
'import/resolver': {
typescript: {
alwaysTryTypes: true,
project: './tsconfig.json'
},
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx']
}
}
}
};
// TypeScript 配置
const typescriptConfig = {
files: ['**/*.{ts,tsx}'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaFeatures: {
jsx: true
},
project: './tsconfig.json'
}
},
plugins: {
'@typescript-eslint': typescript
},
rules: {
// 覆盖基础规则
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}],
// TypeScript 特定规则
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/prefer-nullish-coalescing': 'error',
'@typescript-eslint/prefer-optional-chain': 'error',
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/consistent-type-imports': ['error', {
prefer: 'type-imports',
disallowTypeAnnotations: false
}],
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
// 严格模式规则
'@typescript-eslint/strict-boolean-expressions': ['error', {
allowString: false,
allowNumber: false,
allowNullableObject: false
}],
'@typescript-eslint/prefer-readonly': 'error',
'@typescript-eslint/prefer-readonly-parameter-types': 'off', // 太严格
'@typescript-eslint/explicit-function-return-type': ['warn', {
allowExpressions: true,
allowTypedFunctionExpressions: true
}]
}
};
// React 配置
const reactConfig = {
files: ['**/*.{jsx,tsx}'],
languageOptions: {
parserOptions: {
ecmaFeatures: {
jsx: true
}
}
},
plugins: {
react,
'react-hooks': reactHooks,
'jsx-a11y': jsxA11y
},
rules: {
// React 规则
'react/react-in-jsx-scope': 'off', // React 17+
'react/prop-types': 'off', // 使用 TypeScript
'react/jsx-uses-react': 'off', // React 17+
'react/jsx-uses-vars': 'error',
'react/jsx-key': ['error', { checkFragmentShorthand: true }],
'react/jsx-no-bind': ['error', {
allowArrowFunctions: true,
allowBind: false,
ignoreRefs: true
}],
'react/jsx-curly-brace-presence': ['error', {
props: 'never',
children: 'never'
}],
'react/self-closing-comp': 'error',
'react/jsx-boolean-value': ['error', 'never'],
// Hooks 规则
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// 无障碍性规则
'jsx-a11y/alt-text': 'error',
'jsx-a11y/anchor-has-content': 'error',
'jsx-a11y/anchor-is-valid': 'error',
'jsx-a11y/aria-props': 'error',
'jsx-a11y/heading-has-content': 'error',
'jsx-a11y/label-has-associated-control': 'error',
'jsx-a11y/no-autofocus': 'warn',
'jsx-a11y/no-static-element-interactions': 'error',
'jsx-a11y/click-events-have-key-events': 'error'
},
settings: {
react: {
version: 'detect'
}
}
};
// 测试文件配置
const testConfig = {
files: ['**/*.{test,spec}.{js,jsx,ts,tsx}', '**/__tests__/**/*.{js,jsx,ts,tsx}'],
languageOptions: {
globals: {
...globals.jest,
...globals.node
}
},
rules: {
// 测试文件中允许的规则放宽
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'no-console': 'off'
}
};
// 配置文件配置
const configConfig = {
files: ['**/*.config.{js,ts,mjs}', '**/.*rc.{js,ts}'],
languageOptions: {
globals: {
...globals.node
}
},
rules: {
// 配置文件中允许 require
'@typescript-eslint/no-var-requires': 'off',
'import/no-commonjs': 'off'
}
};
// 忽略配置
const ignoreConfig = {
ignores: [
'dist/**',
'build/**',
'coverage/**',
'node_modules/**',
'*.min.js',
'*.min.css',
'public/**',
'.next/**',
'.nuxt/**',
'.output/**',
'.vitepress/cache/**',
'.vitepress/dist/**'
]
};
// 导出配置
export default [
js.configs.recommended,
baseConfig,
typescriptConfig,
reactConfig,
testConfig,
configConfig,
ignoreConfig
];
环境特定配置
javascript
// eslint.config.development.js - 开发环境配置
import baseConfig from './eslint.config.js';
export default [
...baseConfig,
{
name: 'development-overrides',
rules: {
// 开发环境放宽的规则
'no-console': 'warn',
'no-debugger': 'warn',
'@typescript-eslint/no-unused-vars': 'warn',
// 开发时有用的规则
'no-warning-comments': ['warn', {
terms: ['TODO', 'FIXME', 'XXX', 'HACK'],
location: 'start'
}],
// 性能相关警告
'react/jsx-no-bind': 'warn',
'react/jsx-no-literals': 'off' // 开发时允许字面量
}
}
];
javascript
// eslint.config.production.js - 生产环境配置
import baseConfig from './eslint.config.js';
export default [
...baseConfig,
{
name: 'production-overrides',
rules: {
// 生产环境严格规则
'no-console': 'error',
'no-debugger': 'error',
'no-alert': 'error',
'no-warning-comments': 'error',
// 性能相关错误
'react/jsx-no-bind': 'error',
'react/jsx-no-literals': 'warn',
// 安全相关
'no-eval': 'error',
'no-implied-eval': 'error',
'no-new-func': 'error',
// TypeScript 严格模式
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/prefer-readonly': 'error'
}
}
];
封装与共享:打造团队的 eslint-config
创建共享配置包
json
// packages/eslint-config-team/package.json
{
"name": "@company/eslint-config",
"version": "1.0.0",
"description": "团队共享的 ESLint 配置",
"main": "index.js",
"exports": {
".": "./index.js",
"./react": "./react.js",
"./typescript": "./typescript.js",
"./node": "./node.js",
"./vue": "./vue.js"
},
"files": [
"index.js",
"react.js",
"typescript.js",
"node.js",
"vue.js",
"rules/",
"README.md"
],
"keywords": [
"eslint",
"eslintconfig",
"javascript",
"typescript",
"react",
"vue",
"code-quality"
],
"peerDependencies": {
"eslint": "^9.0.0"
},
"dependencies": {
"@eslint/js": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"eslint-plugin-react": "^7.34.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-prettier": "^5.1.0",
"eslint-plugin-vue": "^9.20.0",
"globals": "^14.0.0"
},
"devDependencies": {
"@types/eslint": "^8.56.0",
"typescript": "^5.3.0"
},
"engines": {
"node": ">=18.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/company/eslint-config.git"
},
"bugs": {
"url": "https://github.com/company/eslint-config/issues"
},
"homepage": "https://github.com/company/eslint-config#readme"
}
基础配置模块
javascript
// packages/eslint-config-team/index.js - 基础配置
import js from '@eslint/js';
import globals from 'globals';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
// 基础规则集
const baseRules = {
// 代码质量
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'no-alert': 'warn',
'no-eval': 'error',
'no-implied-eval': 'error',
'no-new-func': 'error',
'no-script-url': 'error',
// 变量
'no-unused-vars': ['error', {
vars: 'all',
args: 'after-used',
ignoreRestSiblings: true,
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}],
'no-undef': 'error',
'no-redeclare': 'error',
'no-shadow': 'warn',
// 代码风格
'indent': ['error', 2, {
SwitchCase: 1,
VariableDeclarator: 1,
outerIIFEBody: 1
}],
'quotes': ['error', 'single', {
avoidEscape: true,
allowTemplateLiterals: true
}],
'semi': ['error', 'always'],
'comma-dangle': ['error', 'never'],
'object-curly-spacing': ['error', 'always'],
'array-bracket-spacing': ['error', 'never'],
// 最佳实践
'eqeqeq': ['error', 'always'],
'curly': ['error', 'all'],
'dot-notation': 'error',
'no-else-return': 'error',
'no-empty-function': 'warn',
'no-magic-numbers': ['warn', {
ignore: [-1, 0, 1, 2],
ignoreArrayIndexes: true,
enforceConst: true
}],
'prefer-const': 'error',
'prefer-template': 'error'
};
// 导出基础配置
export default [
js.configs.recommended,
{
name: '@company/eslint-config/base',
languageOptions: {
globals: {
...globals.browser,
...globals.node,
...globals.es2021
},
ecmaVersion: 'latest',
sourceType: 'module'
},
rules: baseRules
}
];
// 导出规则集供其他配置使用
export { baseRules };
TypeScript 配置模块
javascript
// packages/eslint-config-team/typescript.js
import typescript from '@typescript-eslint/eslint-plugin';
import typescriptParser from '@typescript-eslint/parser';
import baseConfig, { baseRules } from './index.js';
const typescriptRules = {
// 覆盖基础规则
'no-unused-vars': 'off',
'no-redeclare': 'off',
'no-shadow': 'off',
'indent': 'off',
// TypeScript 特定规则
'@typescript-eslint/no-unused-vars': ['error', {
vars: 'all',
args: 'after-used',
ignoreRestSiblings: true,
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}],
'@typescript-eslint/no-redeclare': 'error',
'@typescript-eslint/no-shadow': 'warn',
'@typescript-eslint/indent': ['error', 2],
// 类型相关
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/prefer-nullish-coalescing': 'error',
'@typescript-eslint/prefer-optional-chain': 'error',
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/consistent-type-imports': ['error', {
prefer: 'type-imports',
disallowTypeAnnotations: false
}],
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
// 命名约定
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'variableLike',
format: ['camelCase', 'PascalCase', 'UPPER_CASE']
},
{
selector: 'function',
format: ['camelCase', 'PascalCase']
},
{
selector: 'typeLike',
format: ['PascalCase']
}
],
// 函数相关
'@typescript-eslint/explicit-function-return-type': ['warn', {
allowExpressions: true,
allowTypedFunctionExpressions: true
}],
'@typescript-eslint/explicit-module-boundary-types': 'warn',
// 严格模式
'@typescript-eslint/strict-boolean-expressions': ['error', {
allowString: false,
allowNumber: false,
allowNullableObject: false
}],
'@typescript-eslint/prefer-readonly': 'error'
};
export default [
...baseConfig,
{
name: '@company/eslint-config/typescript',
files: ['**/*.{ts,tsx}'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaFeatures: {
jsx: true
},
project: './tsconfig.json'
}
},
plugins: {
'@typescript-eslint': typescript
},
rules: {
...baseRules,
...typescriptRules
}
}
];
export { typescriptRules };
React 配置模块
javascript
// packages/eslint-config-team/react.js
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import typescriptConfig, { typescriptRules } from './typescript.js';
const reactRules = {
// React 基础规则
'react/react-in-jsx-scope': 'off', // React 17+
'react/prop-types': 'off', // 使用 TypeScript
'react/display-name': 'error',
'react/jsx-key': ['error', { checkFragmentShorthand: true }],
'react/jsx-no-bind': ['error', {
allowArrowFunctions: true,
allowBind: false,
ignoreRefs: true
}],
'react/jsx-curly-brace-presence': ['error', {
props: 'never',
children: 'never'
}],
'react/self-closing-comp': 'error',
'react/jsx-boolean-value': ['error', 'never'],
'react/no-array-index-key': 'warn',
'react/no-danger': 'warn',
'react/jsx-pascal-case': 'error',
// JSX 格式
'react/jsx-closing-bracket-location': ['error', 'line-aligned'],
'react/jsx-closing-tag-location': 'error',
'react/jsx-curly-spacing': ['error', 'never'],
'react/jsx-equals-spacing': ['error', 'never'],
'react/jsx-indent': ['error', 2],
'react/jsx-indent-props': ['error', 2],
'react/jsx-max-props-per-line': ['error', { maximum: 1, when: 'multiline' }],
'react/jsx-tag-spacing': ['error', {
closingSlash: 'never',
beforeSelfClosing: 'always',
afterOpening: 'never',
beforeClosing: 'never'
}],
// Hooks 规则
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// 无障碍性规则
'jsx-a11y/alt-text': 'error',
'jsx-a11y/anchor-has-content': 'error',
'jsx-a11y/anchor-is-valid': 'error',
'jsx-a11y/aria-props': 'error',
'jsx-a11y/heading-has-content': 'error',
'jsx-a11y/label-has-associated-control': 'error',
'jsx-a11y/no-autofocus': 'warn',
'jsx-a11y/click-events-have-key-events': 'error'
};
export default [
...typescriptConfig,
{
name: '@company/eslint-config/react',
files: ['**/*.{jsx,tsx}'],
plugins: {
react,
'react-hooks': reactHooks,
'jsx-a11y': jsxA11y
},
rules: {
...typescriptRules,
...reactRules
},
settings: {
react: {
version: 'detect'
}
}
}
];
export { reactRules };
配置发布和使用
javascript
// packages/eslint-config-team/scripts/build.js - 构建脚本
import { writeFileSync, readFileSync } from 'fs';
import { join } from 'path';
import { execSync } from 'child_process';
class ConfigBuilder {
constructor() {
this.packagePath = join(process.cwd(), 'package.json');
this.distPath = join(process.cwd(), 'dist');
}
validateConfig() {
console.log('🔍 验证配置文件...');
try {
// 验证基础配置
const baseConfig = require('./index.js');
console.log('✅ 基础配置验证通过');
// 验证 TypeScript 配置
const tsConfig = require('./typescript.js');
console.log('✅ TypeScript 配置验证通过');
// 验证 React 配置
const reactConfig = require('./react.js');
console.log('✅ React 配置验证通过');
} catch (error) {
console.error('❌ 配置验证失败:', error.message);
process.exit(1);
}
}
generateDocs() {
console.log('📚 生成文档...');
const readme = `# @company/eslint-config
团队共享的 ESLint 配置包,支持 JavaScript、TypeScript、React 等技术栈。
## 安装
\`\`\`bash
npm install --save-dev @company/eslint-config eslint
\`\`\`
## 使用
### 基础 JavaScript 项目
\`\`\`javascript
// eslint.config.js
import config from '@company/eslint-config';
export default config;
\`\`\`
### TypeScript 项目
\`\`\`javascript
// eslint.config.js
import config from '@company/eslint-config/typescript';
export default config;
\`\`\`
### React + TypeScript 项目
\`\`\`javascript
// eslint.config.js
import config from '@company/eslint-config/react';
export default config;
\`\`\`
## 规则说明
### 代码质量规则
- 禁止使用 \`console\` 和 \`debugger\` (生产环境)
- 禁止未使用的变量
- 强制使用 \`===\` 和 \`!==\`
### 代码风格规则
- 使用 2 空格缩进
- 使用单引号
- 行末必须有分号
- 对象花括号内必须有空格
### TypeScript 规则
- 优先使用类型导入
- 禁止使用 \`any\` 类型 (警告)
- 强制函数返回类型声明
### React 规则
- 强制 JSX 中的 key 属性
- 禁止在 JSX 中使用 bind
- 遵循 Hooks 规则
- 支持无障碍性检查
## 自定义配置
\`\`\`javascript
// eslint.config.js
import baseConfig from '@company/eslint-config/react';
export default [
...baseConfig,
{
// 你的自定义规则
rules: {
'no-console': 'off'
}
}
];
\`\`\`
## 版本更新
查看 [CHANGELOG.md](./CHANGELOG.md) 了解版本更新内容。
`;
writeFileSync(join(process.cwd(), 'README.md'), readme);
console.log('✅ README.md 生成完成');
}
updateVersion() {
const pkg = JSON.parse(readFileSync(this.packagePath, 'utf8'));
const currentVersion = pkg.version;
// 自动递增版本号
const versionParts = currentVersion.split('.');
versionParts[2] = String(parseInt(versionParts[2]) + 1);
const newVersion = versionParts.join('.');
pkg.version = newVersion;
writeFileSync(this.packagePath, JSON.stringify(pkg, null, 2));
console.log(`📦 版本更新: ${currentVersion} -> ${newVersion}`);
return newVersion;
}
publish() {
console.log('🚀 发布配置包...');
try {
// 构建
execSync('npm run build', { stdio: 'inherit' });
// 测试
execSync('npm test', { stdio: 'inherit' });
// 发布
execSync('npm publish', { stdio: 'inherit' });
console.log('✅ 发布成功');
} catch (error) {
console.error('❌ 发布失败:', error.message);
process.exit(1);
}
}
build() {
this.validateConfig();
this.generateDocs();
const newVersion = this.updateVersion();
console.log(`\n🎉 构建完成!版本: ${newVersion}`);
console.log('运行 npm run publish 发布到 npm');
}
}
// 运行构建
const builder = new ConfigBuilder();
builder.build();
深入原理:解构 ESLint 的"微内核"架构
架构概览
核心组件分析
1. Linter 核心引擎
javascript
// eslint/lib/linter/linter.js - 简化版核心逻辑
class Linter {
constructor(options = {}) {
this.version = version;
this.rules = new Map();
this.environments = new Map();
this.processors = new Map();
// 加载内置规则
this.loadBuiltInRules();
}
/**
* 验证代码
* @param {string} text - 源代码
* @param {Object} config - 配置对象
* @param {Object} options - 选项
* @returns {Array} 问题列表
*/
verifyAndFix(text, config, options = {}) {
const results = [];
let currentText = text;
let fixed = false;
// 最多修复 10 次,防止无限循环
for (let i = 0; i < 10; i++) {
const result = this.verify(currentText, config, options);
if (result.length === 0) {
break;
}
// 应用修复
const fixResult = this.applyFixes(currentText, result);
if (fixResult.fixed) {
currentText = fixResult.output;
fixed = true;
} else {
results.push(...result);
break;
}
}
return {
results,
output: currentText,
fixed
};
}
verify(text, config, options = {}) {
// 1. 解析配置
const resolvedConfig = this.resolveConfig(config, options);
// 2. 预处理代码
const processedText = this.preprocess(text, resolvedConfig);
// 3. 解析 AST
const ast = this.parse(processedText, resolvedConfig);
// 4. 遍历 AST 并应用规则
const problems = this.runRules(ast, resolvedConfig, options);
// 5. 后处理问题
return this.postprocess(problems, resolvedConfig);
}
parse(text, config) {
const parser = this.getParser(config.parser);
const parserOptions = config.parserOptions || {};
try {
return parser.parse(text, {
...parserOptions,
range: true,
loc: true,
tokens: true,
comments: true
});
} catch (error) {
throw new Error(`解析错误: ${error.message}`);
}
}
runRules(ast, config, options) {
const problems = [];
const ruleContext = this.createRuleContext(config, options);
// 遍历每个启用的规则
for (const [ruleId, ruleConfig] of Object.entries(config.rules)) {
if (this.isRuleEnabled(ruleConfig)) {
const rule = this.rules.get(ruleId);
if (rule) {
const ruleProblems = this.executeRule(rule, ast, ruleContext, ruleConfig);
problems.push(...ruleProblems);
}
}
}
return problems;
}
executeRule(rule, ast, context, config) {
const problems = [];
const ruleListener = rule.create({
...context,
report: (problem) => {
problems.push({
...problem,
ruleId: rule.meta?.name || 'unknown',
severity: this.getSeverity(config)
});
}
});
// 遍历 AST 节点
this.traverseAST(ast, ruleListener);
return problems;
}
traverseAST(ast, listener) {
const nodeQueue = [ast];
while (nodeQueue.length > 0) {
const node = nodeQueue.shift();
// 调用节点监听器
if (listener[node.type]) {
listener[node.type](node);
}
// 添加子节点到队列
for (const key of Object.keys(node)) {
const child = node[key];
if (this.isASTNode(child)) {
nodeQueue.push(child);
} else if (Array.isArray(child)) {
nodeQueue.push(...child.filter(this.isASTNode));
}
}
// 调用退出监听器
if (listener[`${node.type}:exit`]) {
listener[`${node.type}:exit`](node);
}
}
}
}
实战:开发你的第一个 ESLint 插件
插件项目结构
eslint-plugin-custom/
├── package.json
├── README.md
├── lib/
│ ├── index.js # 插件入口
│ ├── rules/ # 规则目录
│ │ ├── no-console-log.js # 示例规则
│ │ ├── prefer-const.js # 示例规则
│ │ └── index.js # 规则导出
│ ├── configs/ # 配置目录
│ │ ├── recommended.js # 推荐配置
│ │ └── strict.js # 严格配置
│ └── utils/ # 工具函数
│ ├── ast-utils.js # AST 工具
│ └── rule-utils.js # 规则工具
├── tests/ # 测试目录
│ ├── lib/
│ │ └── rules/
│ │ ├── no-console-log.test.js
│ │ └── prefer-const.test.js
│ └── fixtures/ # 测试用例
└── docs/ # 文档目录
├── rules/
│ ├── no-console-log.md
│ └── prefer-const.md
└── README.md
1. 插件入口文件
javascript
// lib/index.js
const rules = require('./rules');
const configs = require('./configs');
module.exports = {
// 插件元信息
meta: {
name: 'eslint-plugin-custom',
version: '1.0.0'
},
// 导出规则
rules,
// 导出配置
configs,
// 处理器(可选)
processors: {
// 自定义文件处理器
'.custom': {
preprocess: function(text, filename) {
// 预处理逻辑
return [text];
},
postprocess: function(messages, filename) {
// 后处理逻辑
return messages[0];
},
supportsAutofix: true
}
},
// 环境定义(可选)
environments: {
custom: {
globals: {
customGlobal: 'readonly',
customFunction: 'writable'
},
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module'
}
}
}
};
2. 规则开发实战
规则 1:禁止使用 console.log
javascript
// lib/rules/no-console-log.js
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: '禁止使用 console.log',
category: 'Best Practices',
recommended: true,
url: 'https://github.com/your-org/eslint-plugin-custom/blob/main/docs/rules/no-console-log.md'
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
allowInDevelopment: {
type: 'boolean',
default: false
},
allowedMethods: {
type: 'array',
items: {
type: 'string'
},
default: []
}
},
additionalProperties: false
}
],
messages: {
unexpected: '不应该使用 console.log,请使用适当的日志库',
unexpectedMethod: '不应该使用 console.{{method}},请使用适当的日志库',
developmentOnly: 'console.log 只应在开发环境中使用'
}
},
create(context) {
const options = context.options[0] || {};
const allowInDevelopment = options.allowInDevelopment || false;
const allowedMethods = new Set(options.allowedMethods || []);
const sourceCode = context.getSourceCode();
/**
* 检查是否在开发环境
*/
function isInDevelopment() {
// 检查环境变量或其他开发环境标识
const program = sourceCode.ast;
const comments = sourceCode.getAllComments();
// 检查是否有 @dev 注释
return comments.some(comment =>
comment.value.includes('@dev') ||
comment.value.includes('development only')
);
}
/**
* 生成修复建议
*/
function getFixer(node) {
return function(fixer) {
// 简单的修复:注释掉 console.log
const range = node.range;
const text = sourceCode.getText(node);
return fixer.replaceText(node, `// ${text}`);
};
}
/**
* 报告问题
*/
function reportConsoleUsage(node, method = 'log') {
const messageId = method === 'log' ? 'unexpected' : 'unexpectedMethod';
context.report({
node,
messageId,
data: { method },
fix: getFixer(node)
});
}
return {
// 监听成员表达式节点
MemberExpression(node) {
// 检查是否是 console.xxx 调用
if (
node.object &&
node.object.type === 'Identifier' &&
node.object.name === 'console' &&
node.property &&
node.property.type === 'Identifier'
) {
const method = node.property.name;
// 检查是否是允许的方法
if (allowedMethods.has(method)) {
return;
}
// 检查开发环境设置
if (allowInDevelopment && isInDevelopment()) {
return;
}
// 检查父节点是否是调用表达式
if (
node.parent &&
node.parent.type === 'CallExpression' &&
node.parent.callee === node
) {
reportConsoleUsage(node.parent, method);
}
}
},
// 监听调用表达式(处理解构赋值的情况)
CallExpression(node) {
// 处理 const { log } = console; log() 的情况
if (
node.callee &&
node.callee.type === 'Identifier'
) {
const scope = context.getScope();
const variable = scope.set.get(node.callee.name);
if (variable && variable.defs.length > 0) {
const def = variable.defs[0];
// 检查是否来自 console 对象的解构
if (
def.type === 'Variable' &&
def.node.id.type === 'ObjectPattern' &&
def.node.init &&
def.node.init.type === 'Identifier' &&
def.node.init.name === 'console'
) {
reportConsoleUsage(node, node.callee.name);
}
}
}
}
};
}
};
规则 2:优先使用 const
javascript
// lib/rules/prefer-const.js
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: '要求使用 const 声明那些声明后不再被修改的变量',
category: 'ECMAScript 6',
recommended: true,
url: 'https://github.com/your-org/eslint-plugin-custom/blob/main/docs/rules/prefer-const.md'
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
destructuring: {
enum: ['any', 'all'],
default: 'any'
},
ignoreReadBeforeAssign: {
type: 'boolean',
default: false
}
},
additionalProperties: false
}
],
messages: {
useConst: "'{{name}}' 从未被重新赋值,应该使用 'const'",
useConstDestructuring: '解构赋值中的所有变量都从未被重新赋值,应该使用 const'
}
},
create(context) {
const options = context.options[0] || {};
const destructuring = options.destructuring || 'any';
const ignoreReadBeforeAssign = options.ignoreReadBeforeAssign || false;
const sourceCode = context.getSourceCode();
/**
* 检查变量是否被重新赋值
*/
function isReassigned(variable) {
return variable.references.some(ref => {
return ref.isWrite() && ref.init !== true;
});
}
/**
* 检查变量是否在声明前被读取
*/
function isReadBeforeAssign(variable) {
const def = variable.defs[0];
if (!def || def.type !== 'Variable') {
return false;
}
const declarationRange = def.node.range;
return variable.references.some(ref => {
return ref.isRead() && ref.identifier.range[0] < declarationRange[0];
});
}
/**
* 生成修复函数
*/
function getFixer(node) {
return function(fixer) {
const letToken = sourceCode.getFirstToken(node, token =>
token.type === 'Keyword' && token.value === 'let'
);
if (letToken) {
return fixer.replaceText(letToken, 'const');
}
return null;
};
}
/**
* 检查变量声明
*/
function checkVariableDeclaration(node) {
if (node.kind !== 'let') {
return;
}
const scope = context.getScope();
const variablesToCheck = [];
// 收集需要检查的变量
for (const declarator of node.declarations) {
if (declarator.id.type === 'Identifier') {
const variable = scope.set.get(declarator.id.name);
if (variable) {
variablesToCheck.push({
variable,
declarator,
name: declarator.id.name
});
}
} else if (declarator.id.type === 'ObjectPattern' || declarator.id.type === 'ArrayPattern') {
// 处理解构赋值
const names = [];
function collectNames(pattern) {
if (pattern.type === 'Identifier') {
names.push(pattern.name);
} else if (pattern.type === 'ObjectPattern') {
pattern.properties.forEach(prop => {
if (prop.type === 'Property') {
collectNames(prop.value);
} else if (prop.type === 'RestElement') {
collectNames(prop.argument);
}
});
} else if (pattern.type === 'ArrayPattern') {
pattern.elements.forEach(element => {
if (element) {
collectNames(element);
}
});
}
}
collectNames(declarator.id);
for (const name of names) {
const variable = scope.set.get(name);
if (variable) {
variablesToCheck.push({
variable,
declarator,
name,
isDestructuring: true
});
}
}
}
}
// 检查每个变量
const shouldUseConst = [];
for (const { variable, declarator, name, isDestructuring } of variablesToCheck) {
// 跳过在声明前被读取的变量(如果设置了忽略选项)
if (ignoreReadBeforeAssign && isReadBeforeAssign(variable)) {
continue;
}
// 检查是否被重新赋值
if (!isReassigned(variable)) {
shouldUseConst.push({ variable, declarator, name, isDestructuring });
}
}
// 报告问题
if (shouldUseConst.length > 0) {
if (shouldUseConst.some(item => item.isDestructuring)) {
// 处理解构赋值
const destructuringItems = shouldUseConst.filter(item => item.isDestructuring);
const nonDestructuringItems = shouldUseConst.filter(item => !item.isDestructuring);
if (destructuring === 'all') {
// 只有当所有解构变量都不被重新赋值时才报告
const allDestructuringVars = variablesToCheck.filter(item => item.isDestructuring);
if (destructuringItems.length === allDestructuringVars.length) {
context.report({
node,
messageId: 'useConstDestructuring',
fix: getFixer(node)
});
}
} else {
// any 模式:任何不被重新赋值的变量都报告
context.report({
node,
messageId: 'useConstDestructuring',
fix: getFixer(node)
});
}
// 报告非解构变量
for (const { name } of nonDestructuringItems) {
context.report({
node,
messageId: 'useConst',
data: { name },
fix: getFixer(node)
});
}
} else {
// 只有普通变量
for (const { name } of shouldUseConst) {
context.report({
node,
messageId: 'useConst',
data: { name },
fix: getFixer(node)
});
}
}
}
}
return {
VariableDeclaration: checkVariableDeclaration
};
}
};
3. 规则导出
javascript
// lib/rules/index.js
module.exports = {
'no-console-log': require('./no-console-log'),
'prefer-const': require('./prefer-const')
};
4. 配置定义
javascript
// lib/configs/recommended.js
module.exports = {
plugins: ['custom'],
rules: {
'custom/no-console-log': 'warn',
'custom/prefer-const': 'error'
},
env: {
es6: true,
node: true
},
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module'
}
};
javascript
// lib/configs/strict.js
module.exports = {
extends: ['./recommended'],
rules: {
'custom/no-console-log': ['error', {
allowInDevelopment: false,
allowedMethods: []
}],
'custom/prefer-const': ['error', {
destructuring: 'all',
ignoreReadBeforeAssign: false
}]
}
};
javascript
// lib/configs/index.js
module.exports = {
recommended: require('./recommended'),
strict: require('./strict')
};
5. 工具函数
javascript
// lib/utils/ast-utils.js
/**
* AST 工具函数
*/
module.exports = {
/**
* 检查节点是否是函数
*/
isFunction(node) {
return node && (
node.type === 'FunctionDeclaration' ||
node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression'
);
},
/**
* 检查节点是否是方法定义
*/
isMethodDefinition(node) {
return node && node.type === 'MethodDefinition';
},
/**
* 获取函数名称
*/
getFunctionName(node) {
if (node.id && node.id.name) {
return node.id.name;
}
if (node.key && node.key.name) {
return node.key.name;
}
return null;
},
/**
* 检查是否是构造函数
*/
isConstructor(node) {
return this.isMethodDefinition(node) &&
node.key &&
node.key.name === 'constructor';
},
/**
* 获取节点的作用域
*/
getScope(context, node) {
let scope = context.getScope();
while (scope) {
if (scope.block === node) {
return scope;
}
scope = scope.upper;
}
return null;
},
/**
* 检查变量是否在作用域中定义
*/
isVariableDefined(scope, name) {
while (scope) {
if (scope.set.has(name)) {
return true;
}
scope = scope.upper;
}
return false;
},
/**
* 获取字符串字面量的值
*/
getStringValue(node) {
if (node.type === 'Literal' && typeof node.value === 'string') {
return node.value;
}
if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
return node.quasis[0].value.cooked;
}
return null;
},
/**
* 检查节点是否是常量
*/
isConstant(node) {
return node.type === 'Literal' ||
(node.type === 'UnaryExpression' &&
node.operator === '-' &&
node.argument.type === 'Literal');
}
};
javascript
// lib/utils/rule-utils.js
/**
* 规则工具函数
*/
module.exports = {
/**
* 创建规则选项验证器
*/
createOptionsValidator(schema) {
return function validateOptions(options) {
// 简单的选项验证逻辑
if (!options || typeof options !== 'object') {
return false;
}
for (const [key, value] of Object.entries(options)) {
const schemaProperty = schema.properties[key];
if (!schemaProperty) {
return false;
}
if (schemaProperty.type && typeof value !== schemaProperty.type) {
return false;
}
if (schemaProperty.enum && !schemaProperty.enum.includes(value)) {
return false;
}
}
return true;
};
},
/**
* 创建修复函数生成器
*/
createFixerGenerator(sourceCode) {
return {
replaceText(node, text) {
return fixer => fixer.replaceText(node, text);
},
insertTextBefore(node, text) {
return fixer => fixer.insertTextBefore(node, text);
},
insertTextAfter(node, text) {
return fixer => fixer.insertTextAfter(node, text);
},
removeText(node) {
return fixer => fixer.remove(node);
}
};
},
/**
* 创建消息格式化器
*/
createMessageFormatter(messages) {
return function formatMessage(messageId, data = {}) {
let message = messages[messageId];
if (!message) {
return `Unknown message: ${messageId}`;
}
// 替换占位符
for (const [key, value] of Object.entries(data)) {
message = message.replace(new RegExp(`{{${key}}}`, 'g'), value);
}
return message;
};
},
/**
* 创建规则上下文增强器
*/
enhanceContext(context) {
const sourceCode = context.getSourceCode();
return {
...context,
// 增强的报告方法
reportEnhanced(options) {
const { node, messageId, data, fix, suggest } = options;
context.report({
node,
messageId,
data,
fix: fix ? fix(sourceCode) : undefined,
suggest: suggest ? suggest.map(s => ({
...s,
fix: s.fix ? s.fix(sourceCode) : undefined
})) : undefined
});
},
// 获取节点文本
getNodeText(node) {
return sourceCode.getText(node);
},
// 获取节点注释
getNodeComments(node) {
return {
leading: sourceCode.getCommentsBefore(node),
trailing: sourceCode.getCommentsAfter(node)
};
},
// 检查节点是否有注释
hasComments(node) {
const comments = this.getNodeComments(node);
return comments.leading.length > 0 || comments.trailing.length > 0;
}
};
}
};
6. 插件测试
javascript
// tests/lib/rules/no-console-log.test.js
const { RuleTester } = require('eslint');
const rule = require('../../../lib/rules/no-console-log');
const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module'
}
});
ruleTester.run('no-console-log', rule, {
valid: [
// 允许的情况
{
code: 'console.error("error message");',
options: [{ allowedMethods: ['error'] }]
},
{
code: '// @dev\nconsole.log("debug");',
options: [{ allowInDevelopment: true }]
},
{
code: 'const logger = { log: () => {} }; logger.log();'
},
{
code: 'function console() {} console();'
}
],
invalid: [
// 不允许的情况
{
code: 'console.log("hello");',
errors: [{
messageId: 'unexpected',
type: 'CallExpression'
}],
output: '// console.log("hello");'
},
{
code: 'console.warn("warning");',
errors: [{
messageId: 'unexpectedMethod',
data: { method: 'warn' },
type: 'CallExpression'
}],
output: '// console.warn("warning");'
},
{
code: 'const { log } = console; log("test");',
errors: [{
messageId: 'unexpectedMethod',
data: { method: 'log' },
type: 'CallExpression'
}]
},
{
code: 'console.log("not in dev");',
options: [{ allowInDevelopment: true }],
errors: [{
messageId: 'unexpected',
type: 'CallExpression'
}]
}
]
});
javascript
// tests/lib/rules/prefer-const.test.js
const { RuleTester } = require('eslint');
const rule = require('../../../lib/rules/prefer-const');
const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module'
}
});
ruleTester.run('prefer-const', rule, {
valid: [
// 允许的情况
'const x = 1;',
'let x = 1; x = 2;',
'let x; x = 1;',
'for (let i = 0; i < 10; i++) {}',
'let { a, b } = obj; a = 1;',
{
code: 'let { a, b } = obj; a = 1;',
options: [{ destructuring: 'all' }]
}
],
invalid: [
// 不允许的情况
{
code: 'let x = 1;',
errors: [{
messageId: 'useConst',
data: { name: 'x' },
type: 'VariableDeclaration'
}],
output: 'const x = 1;'
},
{
code: 'let { a, b } = obj;',
errors: [{
messageId: 'useConstDestructuring',
type: 'VariableDeclaration'
}],
output: 'const { a, b } = obj;'
},
{
code: 'let [a, b] = arr;',
errors: [{
messageId: 'useConstDestructuring',
type: 'VariableDeclaration'
}],
output: 'const [a, b] = arr;'
},
{
code: 'let { a, b } = obj; b = 1;',
options: [{ destructuring: 'any' }],
errors: [{
messageId: 'useConstDestructuring',
type: 'VariableDeclaration'
}]
}
]
});
7. 插件配置文件
json
// package.json
{
"name": "eslint-plugin-custom",
"version": "1.0.0",
"description": "Custom ESLint rules for team coding standards",
"main": "lib/index.js",
"scripts": {
"test": "mocha tests/**/*.test.js",
"test:watch": "npm test -- --watch",
"lint": "eslint lib tests",
"lint:fix": "npm run lint -- --fix",
"build": "npm run lint && npm test",
"prepublishOnly": "npm run build",
"docs:generate": "node scripts/generate-docs.js",
"docs:validate": "node scripts/validate-docs.js"
},
"keywords": [
"eslint",
"eslintplugin",
"eslint-plugin",
"javascript",
"typescript",
"code-quality"
],
"author": "Your Team <team@company.com>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/your-org/eslint-plugin-custom.git"
},
"bugs": {
"url": "https://github.com/your-org/eslint-plugin-custom/issues"
},
"homepage": "https://github.com/your-org/eslint-plugin-custom#readme",
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"eslint": ">=8.0.0"
},
"devDependencies": {
"eslint": "^8.57.0",
"mocha": "^10.2.0",
"@types/eslint": "^8.56.0"
},
"files": [
"lib",
"docs",
"README.md",
"LICENSE"
]
}
8. 文档生成脚本
javascript
// scripts/generate-docs.js
const fs = require('fs');
const path = require('path');
const plugin = require('../lib/index');
/**
* 生成规则文档
*/
function generateRuleDocs() {
const docsDir = path.join(__dirname, '../docs/rules');
// 确保文档目录存在
if (!fs.existsSync(docsDir)) {
fs.mkdirSync(docsDir, { recursive: true });
}
// 为每个规则生成文档
for (const [ruleId, rule] of Object.entries(plugin.rules)) {
const docPath = path.join(docsDir, `${ruleId}.md`);
const docContent = generateRuleDoc(ruleId, rule);
fs.writeFileSync(docPath, docContent, 'utf8');
console.log(`Generated documentation for rule: ${ruleId}`);
}
}
/**
* 生成单个规则的文档
*/
function generateRuleDoc(ruleId, rule) {
const meta = rule.meta || {};
const docs = meta.docs || {};
let content = `# ${ruleId}\n\n`;
// 描述
if (docs.description) {
content += `> ${docs.description}\n\n`;
}
// 规则类型
if (meta.type) {
const typeMap = {
problem: '🚨 Problem',
suggestion: '💡 Suggestion',
layout: '🎨 Layout'
};
content += `**类型:** ${typeMap[meta.type] || meta.type}\n\n`;
}
// 是否可修复
if (meta.fixable) {
content += `**可自动修复:** ✅\n\n`;
}
// 推荐配置
if (docs.recommended) {
content += `**推荐配置:** ✅\n\n`;
}
// 规则详情
content += `## 规则详情\n\n`;
content += `此规则${docs.description || '执行特定的代码质量检查'}。\n\n`;
// 选项
if (meta.schema && meta.schema.length > 0) {
content += `## 选项\n\n`;
content += `此规则接受以下选项:\n\n`;
const schema = meta.schema[0];
if (schema.properties) {
content += `\`\`\`json\n${JSON.stringify(schema, null, 2)}\n\`\`\`\n\n`;
}
}
// 示例
content += `## 示例\n\n`;
content += `### ❌ 错误的代码示例\n\n`;
content += `\`\`\`javascript\n// TODO: 添加错误示例\n\`\`\`\n\n`;
content += `### ✅ 正确的代码示例\n\n`;
content += `\`\`\`javascript\n// TODO: 添加正确示例\n\`\`\`\n\n`;
// 何时不使用
content += `## 何时不使用此规则\n\n`;
content += `如果你不关心此规则检查的代码质量问题,可以禁用此规则。\n\n`;
return content;
}
/**
* 生成主 README
*/
function generateMainReadme() {
const readmePath = path.join(__dirname, '../README.md');
let content = `# eslint-plugin-custom\n\n`;
content += `Custom ESLint rules for team coding standards.\n\n`;
// 安装
content += `## 安装\n\n`;
content += `\`\`\`bash\nnpm install --save-dev eslint-plugin-custom\n\`\`\`\n\n`;
// 使用
content += `## 使用\n\n`;
content += `在你的 \`.eslintrc.js\` 或 \`eslint.config.js\` 中添加插件:\n\n`;
// ESLint 9+ 配置
content += `### ESLint 9+ (eslint.config.js)\n\n`;
content += `\`\`\`javascript\nimport customPlugin from 'eslint-plugin-custom';\n\nexport default [\n {\n plugins: {\n custom: customPlugin\n },\n rules: {\n 'custom/no-console-log': 'warn',\n 'custom/prefer-const': 'error'\n }\n }\n];\n\`\`\`\n\n`;
// 传统配置
content += `### 传统配置 (.eslintrc.js)\n\n`;
content += `\`\`\`javascript\nmodule.exports = {\n plugins: ['custom'],\n rules: {\n 'custom/no-console-log': 'warn',\n 'custom/prefer-const': 'error'\n }\n};\n\`\`\`\n\n`;
// 预设配置
content += `## 预设配置\n\n`;
content += `插件提供了以下预设配置:\n\n`;
for (const [configName, config] of Object.entries(plugin.configs)) {
content += `### ${configName}\n\n`;
content += `\`\`\`javascript\n// eslint.config.js\nimport customPlugin from 'eslint-plugin-custom';\n\nexport default [\n customPlugin.configs.${configName}\n];\n\`\`\`\n\n`;
}
// 规则列表
content += `## 规则\n\n`;
content += `| 规则 | 描述 | 可修复 | 推荐 |\n`;
content += `|------|------|--------|------|\n`;
for (const [ruleId, rule] of Object.entries(plugin.rules)) {
const meta = rule.meta || {};
const docs = meta.docs || {};
const fixable = meta.fixable ? '✅' : '❌';
const recommended = docs.recommended ? '✅' : '❌';
content += `| [${ruleId}](docs/rules/${ruleId}.md) | ${docs.description || ''} | ${fixable} | ${recommended} |\n`;
}
content += `\n`;
// 贡献
content += `## 贡献\n\n`;
content += `欢迎提交 Issue 和 Pull Request!\n\n`;
// 许可证
content += `## 许可证\n\n`;
content += `MIT\n`;
fs.writeFileSync(readmePath, content, 'utf8');
console.log('Generated main README.md');
}
// 执行生成
generateRuleDocs();
generateMainReadme();
console.log('Documentation generation completed!');
9. 发布和使用
发布到 npm
bash
# 1. 构建和测试
npm run build
# 2. 生成文档
npm run docs:generate
# 3. 版本管理
npm version patch # 或 minor, major
# 4. 发布
npm publish
# 5. 推送到 Git
git push origin main --tags
在项目中使用
bash
# 安装插件
npm install --save-dev eslint-plugin-custom
javascript
// eslint.config.js (ESLint 9+)
import customPlugin from 'eslint-plugin-custom';
export default [
// 使用推荐配置
customPlugin.configs.recommended,
// 或自定义配置
{
plugins: {
custom: customPlugin
},
rules: {
'custom/no-console-log': ['error', {
allowInDevelopment: true,
allowedMethods: ['error', 'warn']
}],
'custom/prefer-const': ['error', {
destructuring: 'all'
}]
}
}
];
总结
通过本文档,我们深入了解了 ESLint 的工程化实践:
核心收获
- ESLint 9+ 升级:掌握了新配置格式和迁移策略
- 前端工程规范:建立了完整的代码规范体系
- 配置管理:学会了
eslint.config.js
的使用和优化 - 团队协作:打造了可共享的 eslint-config 包
- 架构理解:深入了解了 ESLint 的微内核架构
- 插件开发:掌握了完整的插件开发流程
最佳实践
- 渐进式升级:分阶段迁移到 ESLint 9+
- 规范先行:建立团队编码规范和流程
- 工具支撑:使用自动化工具保证规范执行
- 持续改进:根据团队反馈不断优化规则
- 文档完善:维护清晰的配置和使用文档
技术要点
- 配置系统:理解 ESLint 的配置解析和合并机制
- 规则引擎:掌握规则的执行流程和 AST 遍历
- 插件机制:了解插件的加载、注册和管理
- 扩展性:设计可扩展的规则和配置架构
通过这些实践,团队可以建立起完善的前端工程化体系,提升代码质量和开发效率。
javascript
// eslint/lib/rules/rule-manager.js - 规则管理器
class RuleManager {
constructor() {
this.rules = new Map();
this.ruleCategories = new Map();
this.deprecatedRules = new Set();
}
/**
* 注册规则
* @param {string} ruleId - 规则 ID
* @param {Object} rule - 规则对象
*/
registerRule(ruleId, rule) {
// 验证规则格式
this.validateRule(rule);
// 检查是否已存在
if (this.rules.has(ruleId)) {
throw new Error(`规则 ${ruleId} 已存在`);
}
// 注册规则
this.rules.set(ruleId, {
...rule,
id: ruleId,
registeredAt: Date.now()
});
// 分类管理
const category = rule.meta?.category || 'uncategorized';
if (!this.ruleCategories.has(category)) {
this.ruleCategories.set(category, new Set());
}
this.ruleCategories.get(category).add(ruleId);
}
validateRule(rule) {
if (typeof rule !== 'object' || rule === null) {
throw new Error('规则必须是对象');
}
if (typeof rule.create !== 'function') {
throw new Error('规则必须有 create 方法');
}
if (rule.meta) {
this.validateRuleMeta(rule.meta);
}
}
validateRuleMeta(meta) {
const requiredFields = ['type', 'docs'];
for (const field of requiredFields) {
if (!(field in meta)) {
throw new Error(`规则元数据缺少必需字段: ${field}`);
}
}
const validTypes = ['problem', 'suggestion', 'layout'];
if (!validTypes.includes(meta.type)) {
throw new Error(`无效的规则类型: ${meta.type}`);
}
}
/**
* 获取规则
* @param {string} ruleId - 规则 ID
* @returns {Object|null} 规则对象
*/
getRule(ruleId) {
return this.rules.get(ruleId) || null;
}
/**
* 获取所有规则
* @returns {Map} 规则映射
*/
getAllRules() {
return new Map(this.rules);
}
/**
* 按类别获取规则
* @param {string} category - 类别名称
* @returns {Array} 规则列表
*/
getRulesByCategory(category) {
const ruleIds = this.ruleCategories.get(category) || new Set();
return Array.from(ruleIds).map(id => this.rules.get(id));
}
/**
* 标记规则为已弃用
* @param {string} ruleId - 规则 ID
* @param {string} replacement - 替代规则
*/
deprecateRule(ruleId, replacement) {
this.deprecatedRules.add(ruleId);
const rule = this.rules.get(ruleId);
if (rule) {
rule.meta = {
...rule.meta,
deprecated: true,
replacedBy: replacement
};
}
}
/**
* 检查规则是否已弃用
* @param {string} ruleId - 规则 ID
* @returns {boolean} 是否已弃用
*/
isDeprecated(ruleId) {
return this.deprecatedRules.has(ruleId);
}
}
3. 插件系统架构
javascript
// eslint/lib/plugins/plugin-loader.js - 插件加载器
class PluginLoader {
constructor() {
this.plugins = new Map();
this.loadedPlugins = new Set();
this.pluginDependencies = new Map();
}
/**
* 加载插件
* @param {string} pluginName - 插件名称
* @param {string} pluginPath - 插件路径
* @returns {Object} 插件对象
*/
loadPlugin(pluginName, pluginPath) {
// 检查是否已加载
if (this.loadedPlugins.has(pluginName)) {
return this.plugins.get(pluginName);
}
try {
// 动态导入插件
const plugin = require(pluginPath);
// 验证插件格式
this.validatePlugin(plugin, pluginName);
// 处理插件依赖
this.resolveDependencies(plugin, pluginName);
// 注册插件
this.registerPlugin(pluginName, plugin);
return plugin;
} catch (error) {
throw new Error(`加载插件 ${pluginName} 失败: ${error.message}`);
}
}
validatePlugin(plugin, pluginName) {
if (typeof plugin !== 'object' || plugin === null) {
throw new Error(`插件 ${pluginName} 必须导出对象`);
}
// 验证规则
if (plugin.rules) {
if (typeof plugin.rules !== 'object') {
throw new Error(`插件 ${pluginName} 的 rules 必须是对象`);
}
for (const [ruleId, rule] of Object.entries(plugin.rules)) {
this.validatePluginRule(rule, `${pluginName}/${ruleId}`);
}
}
// 验证配置
if (plugin.configs) {
if (typeof plugin.configs !== 'object') {
throw new Error(`插件 ${pluginName} 的 configs 必须是对象`);
}
}
// 验证处理器
if (plugin.processors) {
if (typeof plugin.processors !== 'object') {
throw new Error(`插件 ${pluginName} 的 processors 必须是对象`);
}
}
}
validatePluginRule(rule, ruleId) {
if (typeof rule !== 'object' || rule === null) {
throw new Error(`规则 ${ruleId} 必须是对象`);
}
if (typeof rule.create !== 'function') {
throw new Error(`规则 ${ruleId} 必须有 create 方法`);
}
}
resolveDependencies(plugin, pluginName) {
if (plugin.dependencies) {
const dependencies = Array.isArray(plugin.dependencies)
? plugin.dependencies
: [plugin.dependencies];
this.pluginDependencies.set(pluginName, dependencies);
// 递归加载依赖
for (const dep of dependencies) {
if (!this.loadedPlugins.has(dep)) {
this.loadPlugin(dep, this.resolvePluginPath(dep));
}
}
}
}
registerPlugin(pluginName, plugin) {
this.plugins.set(pluginName, plugin);
this.loadedPlugins.add(pluginName);
// 注册规则到全局规则管理器
if (plugin.rules) {
for (const [ruleId, rule] of Object.entries(plugin.rules)) {
const fullRuleId = `${pluginName}/${ruleId}`;
global.ruleManager.registerRule(fullRuleId, rule);
}
}
// 注册处理器
if (plugin.processors) {
for (const [processorId, processor] of Object.entries(plugin.processors)) {
const fullProcessorId = `${pluginName}/${processorId}`;
global.processorManager.registerProcessor(fullProcessorId, processor);
}
}
// 注册环境
if (plugin.environments) {
for (const [envId, env] of Object.entries(plugin.environments)) {
const fullEnvId = `${pluginName}/${envId}`;
global.environmentManager.registerEnvironment(fullEnvId, env);
}
}
}
resolvePluginPath(pluginName) {
// 解析插件路径的逻辑
const possiblePaths = [
`eslint-plugin-${pluginName}`,
`@eslint/eslint-plugin-${pluginName}`,
pluginName
];
for (const path of possiblePaths) {
try {
require.resolve(path);
return path;
} catch {
// 继续尝试下一个路径
}
}
throw new Error(`找不到插件: ${pluginName}`);
}
/**
* 获取插件
* @param {string} pluginName - 插件名称
* @returns {Object|null} 插件对象
*/
getPlugin(pluginName) {
return this.plugins.get(pluginName) || null;
}
/**
* 卸载插件
* @param {string} pluginName - 插件名称
*/
unloadPlugin(pluginName) {
const plugin = this.plugins.get(pluginName);
if (!plugin) {
return;
}
// 卸载规则
if (plugin.rules) {
for (const ruleId of Object.keys(plugin.rules)) {
const fullRuleId = `${pluginName}/${ruleId}`;
global.ruleManager.unregisterRule(fullRuleId);
}
}
// 清理插件
this.plugins.delete(pluginName);
this.loadedPlugins.delete(pluginName);
this.pluginDependencies.delete(pluginName);
}
}