eslint-config-alloy
Version:
AlloyTeam ESLint 规则
286 lines (263 loc) • 10.3 kB
text/typescript
/* eslint-disable @typescript-eslint/no-require-imports */
import fs from 'fs';
import path from 'path';
import doctrine from 'doctrine';
import prettier from 'prettier';
import type { Linter } from 'eslint';
import { ESLint } from 'eslint';
const eslintInstance = new ESLint({});
import insertTag from 'insert-tag';
import xmlEscape from 'xml-escape';
import type { Namespace, Rule } from '../config';
import { NAMESPACE_CONFIG, NAMESPACES, buildEslintrcMeta, locale } from '../config';
import '../site/public/vendors/prism';
declare const Prism: any;
// const DEBUT_WHITELIST = ['jsx-curly-brace-presence'];
class Builder {
private namespace: Namespace = NAMESPACES[0];
/** 插件的 meta 信息 */
private ruleMetaMap: {
[key: string]: {
fixable: boolean;
extendsBaseRule: string;
requiresTypeChecking: boolean;
};
} = {};
/** 当前 namespace 的规则列表 */
private ruleList: Rule[] = [];
/** 当前 namespace 的所有规则合并后的文本,包含注释 */
private rulesContent = '';
/** 插件初始配置的内容,如 test/react/.eslintrc.js */
private initialEslintrcContent = '';
/** build base 时,暂存当前 ruleConfig,供后续继承用 */
private baseRuleConfig: {
[key: string]: Rule;
} = {};
public async build(namespace: Namespace) {
this.namespace = namespace;
this.ruleMetaMap = this.getRuleMetaMap();
this.ruleList = await this.getRuleList();
this.rulesContent = this.getRulesContent();
this.initialEslintrcContent = this.getInitialEslintrc();
this.buildRulesJson();
this.buildLocaleJson();
this.buildEslintrc();
}
/** 获取插件的 meta 信息 */
private getRuleMetaMap() {
const { rulePrefix, pluginName } = NAMESPACE_CONFIG[this.namespace];
const ruleEntries = pluginName
? Object.entries<any>(require(pluginName).rules)
: Array.from<any>(require('eslint/use-at-your-own-risk').builtinRules.entries());
return ruleEntries.reduce((prev, [ruleName, ruleValue]) => {
const fullRuleName = rulePrefix + ruleName;
const meta = ruleValue.meta;
prev[fullRuleName] = {
fixable: meta.fixable === 'code',
extendsBaseRule:
// meta.docs.extendsBaseRule 若为 string,则表示继承的规则,若为 true,则提取继承的规则的名称
meta.docs.extensionRule === true || meta.docs.extendsBaseRule === true
? ruleName.replace(NAMESPACE_CONFIG[this.namespace].rulePrefix, '')
: meta.docs.extendsBaseRule ?? '',
requiresTypeChecking: meta.docs.requiresTypeChecking ?? false,
};
return prev;
}, {});
}
/** 获取规则列表,根据字母排序 */
private async getRuleList() {
const ruleList = await Promise.all(
fs
.readdirSync(path.resolve(__dirname, '../test', this.namespace))
.filter((ruleName) => fs.lstatSync(path.resolve(__dirname, '../test', this.namespace, ruleName)).isDirectory())
// .filter((ruleName) => DEBUT_WHITELIST.includes(ruleName))
.map((ruleName) => this.getRule(ruleName)),
);
return ruleList;
}
/** 解析单条规则为一个规则对象 */
private async getRule(ruleName: string) {
const filePath = path.resolve(__dirname, '../test', this.namespace, ruleName, '.eslintrc.js');
const fileModule = require(filePath);
const fileContent = fs.readFileSync(filePath, 'utf-8');
const fullRuleName = NAMESPACE_CONFIG[this.namespace].rulePrefix + ruleName;
const comments = /\/\*\*.*\*\//gms.exec(fileContent);
const rule: Rule = {
name: fullRuleName,
value: fileModule.rules[fullRuleName],
description: '',
reason: '',
badExample: '',
goodExample: '',
...this.ruleMetaMap[fullRuleName],
};
if (comments !== null) {
// 通过 doctrine 解析注释
const commentsAST = doctrine.parse(comments[0], { unwrap: true });
// 将注释体解析为 description
rule.description = commentsAST.description;
// 解析其他的注释内容,如 @reason
rule.reason = commentsAST.tags.find(({ title }) => title === 'reason')?.description ?? '';
}
// 若没有描述,并且有继承的规则,则使用继承的规则的描述
if (!rule.description && rule.extendsBaseRule) {
rule.description = this.baseRuleConfig[rule.extendsBaseRule].description;
}
// 若没有原因,并且有继承的规则,并且本规则的配置项与继承的规则的配置项一致,则使用继承的规则的原因
try {
if (
!rule.reason &&
rule.extendsBaseRule &&
JSON.stringify(rule.value) === JSON.stringify(this.baseRuleConfig[rule.extendsBaseRule].value)
) {
rule.reason = this.baseRuleConfig[rule.extendsBaseRule].reason;
}
} catch (e) {
console.log(e);
console.log(rule.extendsBaseRule);
}
const badFilePath = path.resolve(
path.dirname(filePath),
`bad.${NAMESPACE_CONFIG[this.namespace].exampleExtension}`,
);
const goodFilePath = path.resolve(
path.dirname(filePath),
`good.${NAMESPACE_CONFIG[this.namespace].exampleExtension}`,
);
if (fs.existsSync(badFilePath)) {
const results = await eslintInstance.lintFiles([badFilePath]);
// 通过 Prism 和 insertMark 生成 html 格式的代码
rule.badExample = this.insertMark(
Prism.highlight(
fs.readFileSync(badFilePath, 'utf-8'),
Prism.languages[NAMESPACE_CONFIG[this.namespace].prismLanguage],
NAMESPACE_CONFIG[this.namespace].prismLanguage,
),
results[0].messages,
).trim();
}
if (fs.existsSync(goodFilePath)) {
rule.goodExample = Prism.highlight(
fs.readFileSync(goodFilePath, 'utf-8'),
Prism.languages[NAMESPACE_CONFIG[this.namespace].prismLanguage],
NAMESPACE_CONFIG[this.namespace].prismLanguage,
).trim();
}
return rule;
}
/** 获取插件初始配置的内容 */
private getInitialEslintrc() {
const initialEslintrcPath = path.resolve(__dirname, `../test/${this.namespace}/.eslintrc.js`);
if (!fs.existsSync(initialEslintrcPath)) {
return '';
}
return fs.readFileSync(initialEslintrcPath, 'utf-8');
}
/** 获取当前 namespace 的所有规则合并后的文本,包含注释 */
private getRulesContent() {
return this.ruleList
.map((rule) => {
let content = ['\n/**', ...rule.description.split('\n').map((line) => ` * ${line}`)];
if (rule.reason) {
content = [
...content,
...rule.reason.split('\n').map((line, index) => (index === 0 ? ` * @reason ${line}` : ` * ${line}`)),
];
}
content.push(' */');
// 若继承自基础规则,并且是 ts 规则,则需要先关闭基础规则
const extendsBaseRule = this.ruleMetaMap[rule.name].extendsBaseRule;
if (extendsBaseRule && this.namespace === 'typescript') {
content.push(`'${extendsBaseRule}': 'off',`);
}
content.push(`'${rule.name}': ${JSON.stringify(rule.value, null, 4)},`);
return content.join('\n ');
})
.join('');
}
/** 写入 config/rules/***.json */
private buildRulesJson() {
const ruleConfig = this.ruleList.reduce<{
[key: string]: Rule;
}>((prev, rule) => {
prev[rule.name] = rule;
return prev;
}, {});
/** build base 时,暂存当前 ruleConfig,供后续继承用 */
if (this.namespace === 'base') {
this.baseRuleConfig = ruleConfig;
}
this.writeWithPrettier(
path.resolve(__dirname, `../config/rules/${this.namespace}.json`),
JSON.stringify(ruleConfig),
'json',
);
}
/** 写入 config/locale/*.json */
private buildLocaleJson() {
const current: any = locale['en-US'];
Object.values(this.ruleList).forEach((rule) => {
if (!current[rule.description]) {
current[rule.description] = rule.description;
}
if (rule.reason && !current[rule.reason]) {
current[rule.reason] = rule.reason;
}
});
this.writeWithPrettier(path.resolve(__dirname, '../config/locale/en-US.json'), JSON.stringify(current), 'json');
}
/** 写入各插件的 eslintrc 文件 */
private buildEslintrc() {
const eslintrcContent =
buildEslintrcMeta() +
this.initialEslintrcContent
// 去掉 extends
.replace(/extends:.*],/, '')
// 将 rulesContent 写入 rules
.replace(/(,\s*rules: {([\s\S]*?)})?,\s*};/, (_match, _p1, p2) => {
const rules = p2 ? `${p2}${this.rulesContent}` : this.rulesContent;
return `,rules:{${rules}}};`;
});
this.writeWithPrettier(path.resolve(__dirname, `../${this.namespace}.js`), eslintrcContent);
}
/** 经过 Prettier 格式化后写入文件 */
private async writeWithPrettier(filePath: string, content: string, parser = 'babel') {
fs.writeFileSync(
filePath,
// 使用 prettier 格式化文件内容
await prettier.format(content, {
...require('../.prettierrc'),
parser,
}),
'utf-8',
);
}
/** 依据 ESLint 结果,给 badExample 添加 <mark> 标签 */
private insertMark(badExample: string, eslintMessages: Linter.LintMessage[]) {
let insertedBadExample = badExample;
eslintMessages.forEach(({ ruleId, message, line, column, endLine, endColumn }) => {
const insertLine = line - 1;
const insertColumn = column - 1;
const insertLineEnd = (endLine || line) - 1;
let insertColumnEnd = (endColumn || column + 1) - 1;
if (insertLineEnd === insertLine && insertColumnEnd === insertColumn) {
insertColumnEnd = insertColumnEnd + 1;
}
insertedBadExample = insertTag(
insertedBadExample,
`<mark class="eslint-error" data-tip="${`${xmlEscape(
xmlEscape(message),
)}<br/><span class='eslint-error-rule-id'>eslint(${ruleId})</span>`}">`,
[insertLine, insertColumn, insertLineEnd, insertColumnEnd],
);
});
return insertedBadExample;
}
}
main();
async function main() {
const builder = new Builder();
for (const namespace of NAMESPACES) {
await builder.build(namespace);
}
}