为了提高代码质量,大家可能会在项目中接入 eslint
、prettier
等工具,并且制定一些属于自己团队的代码规范,在新增项目时,会从旧项目中去拷贝相关的配置的文件,同时去安装对应插件库(当然,在大一点或规范一点的团队会去维护自己的脚手架),除了脚手架的形式,是否还有更加高效的方式去给新项目添加 eslint
和 prettier
吗?答案是肯定的,vite-pretty-lint 就实现了为 vite
项目一键添加 eslint
和 prettier
,下面让我们一起来学习一下它是如何实现的。
.
├── ast.js
├── main.js
├── shared.js
├── templates
│ ├── react-ts.js
│ ├── react.js
│ ├── vue-ts.js
│ └── vue.js
└── utils.js
yarn create vite test-vite-app --template react-ts
// NPM
npm init vite-pretty-lint
// YARN
yarn create vite-pretty-lint
// PNPM
pnpm create vite-pretty-lint
react-ts
,包管理工具我选择了 yarn
)lib/main.js
包含了主要的实现代码,下面是主要实现的代码
async function run() {console.log(chalk.bold(gradient.morning('\n🚀 Welcome to Eslint & Prettier Setup for Vite!\n')));let projectType, packageManager;try {/** *NOTE: * 通过命令行交互的形式,获取用户的应用类型 projectType,包管理工具 packageManager * 应用类型主要提供了以下可选值: * react、react-ts、vue、vue-ts * 包管理工具主要提供了以下可选值: * yarn、npm、pnpm */const answers = await askForProjectType();projectType = answers.projectType;packageManager = answers.packageManager;} catch (error) {console.log(chalk.blue('\n👋 Goodbye!'));return;}/** * NOTE: * 通过选择的应用类型 projectType,同步读取对应的配置模版, * 获取需要依赖的包,以及对应的 eslint 配置信息 */const { packages, eslintOverrides } = await import(`./templates/${projectType}.js`);// NOTE: 获取所有依赖的包const packageList = [...commonPackages, ...packages];// NOTE: 将从模版中获取的 eslint 配置信息覆盖到默认配置上const eslintConfigOverrides = [...eslintConfig.overrides, ...eslintOverrides];// NOTE: 整理所需的 eslint 配置信息const eslint = { ...eslintConfig, overrides: eslintConfigOverrides };const commandMap = {npm: `npm install --save-dev ${packageList.join(' ')}`,yarn: `yarn add --dev ${packageList.join(' ')}`,pnpm: `pnpm install --save-dev ${packageList.join(' ')}`,};const viteJs = path.join(projectDirectory, 'vite.config.js');const viteTs = path.join(projectDirectory, 'vite.config.ts');const viteMap = {vue: viteJs,react: viteJs,'vue-ts': viteTs,'react-ts': viteTs,};// NOTE: 获取 vite 配置文件的绝对路径const viteFile = viteMap[projectType];// NOTE: 读取 vite 配置文件,并引入 eslint 配置信息const viteConfig = viteEslint(fs.readFileSync(viteFile, 'utf8'));// NOTE: 根据选择的包管理工具,获取包安装指令const installCommand = commandMap[packageManager];if (!installCommand) {console.log(chalk.red('\n✖ Sorry, we only support npm、yarn and pnpm!'));return;}// NOTE: 创建一个 spinner,用于显示进度const spinner = createSpinner('Installing packages...').start();// NOTE: exec 函数:生成一个 shell,然后在该 shell 中执行“命令”,缓冲任何 生成的输出// 处理传递给 exec 函数的 command 字符串 直接由 shell 和特殊字符(因 shell 而异) 需要相应处理:// 执行安装依赖的 shell 命令exec(`${commandMap[packageManager]}`, { cwd: projectDirectory }, (error) => {if (error) {// NOTE: 如果执行失败,则终止 spinner,并显示错误信息spinner.error({text: chalk.bold.red('Failed to install packages!'),mark: '✖',});console.error(error);return;}// NOTE: 写入 eslint 配置文件fs.writeFileSync(eslintFile, JSON.stringify(eslint, null, 2));// NOTE: 写入 prettier 配置文件fs.writeFileSync(prettierFile, JSON.stringify(prettierConfig, null, 2));// NOTE: 写入 eslint 忽略文件fs.writeFileSync(eslintIgnoreFile, eslintIgnore.join('\n'));// NOTE: 写入 vite 配置文件fs.writeFileSync(viteFile, viteConfig);// NOTE: 执行成功,终止 spinner,并显示成功信息spinner.success({ text: chalk.bold.green('All done! 🎉'), mark: '✔' });console.log(chalk.bold.cyan('\n🔥 Reload your editor to activate the settings!'));});
}
代码在lib/utils.js
中,下面是实现代码,可以看到,是通过 enquirer 库实现
import enquirer from 'enquirer';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
// fileURLToPath:确保正确解码百分比编码的字符 以及确保跨平台有效的绝对路径字符串。
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export function getOptions() {const OPTIONS = [];// NOTE: 读取模版文件夹下的所有模版文件fs.readdirSync(path.join(__dirname, 'templates')).forEach((template) => {// NOTE: 获取模版文件的名称,并去掉后缀const { name } = path.parse(path.join(__dirname, 'templates', template));// NOTE: 将模版文件的名称添加到选项列表中OPTIONS.push(name);});return OPTIONS;
}
export function askForProjectType() {return enquirer.prompt([{// 选择 shell 交互类型 type: 'select',// 获取通过此参数获取对应选择结果name: 'projectType',// 本次操作的标题描述message: 'What type of project do you have?',// 可选择的选项choices: getOptions(),},{type: 'select',name: 'packageManager',message: 'What package manager do you use?',choices: ['npm', 'yarn', 'pnpm'],},]);
}
vite.config.ts
文件的代码babel
转化成抽象语法书AST
中查找需要插入的位置,并插入相关内容babel
把 AST
转成普通 JS
代码export function viteEslint(code) {// NOTE: 将传入的代码转换为 ASTconst ast = babel.parseSync(code, {// 指示代码应该被解析的模式,可以是 'script'、'module' 或 'unambiguous'。sourceType: 'module',// 是否在生成的 AST 中输出注释comments: false,});// 取出主题程序部分的 ASTconst { program } = ast;// 取出引入依赖(import)的 ASTconst importList = program.body.filter((body) => {return body.type === 'ImportDeclaration';}).map((body) => {// 删除注释的 ASTdelete body.trailingComments;return body;});// NOTE: 查询是否引入了 vite-plugin-eslint,若已经引入了,就直接返回传人的代码 codeif (importList.find((body) => body.source.value === 'vite-plugin-eslint')) {return code;}// NOTE: 取出非 import 部分的代码的 ASTconst nonImportList = program.body.filter((body) => {return body.type !== 'ImportDeclaration';});// NOTE: 取出 「export default」声明的代码 ASTconst exportStatement = program.body.find((body) => body.type === 'ExportDefaultDeclaration');// NOTE: 判断当前声明的类型是否为 函数调用表达式if (exportStatement.declaration.type === 'CallExpression') {// NOTE: 取出函数调用表达式的入参const [argument] = exportStatement.declaration.arguments;// NOTE: 判断入参的类型是否为对象表达式if (argument.type === 'ObjectExpression') {// NOTE: 取出对象表达式的 plugins 属性const plugin = argument.properties.find(({ key }) => key.name === 'plugins');if (plugin) {// NOTE: 把 vite-plugin-eslint 插件加入到 plugins 属性中plugin.value.elements.push(eslintPluginCall);}}}importList.push(eslintImport);importList.push(blankLine);program.body = importList.concat(nonImportList);ast.program = program;// NOTE: 将 AST 转换为代码return babel.transformFromAstSync(ast, code, { sourceType: 'module' }).code;
}
AST
AST
的这些节点类型代表啥意思,看不懂怎么半? 可以在这里查找相关的 AST 节点babel
以及 AST
,推荐大家看一看神光的Babel 插件通关秘籍通过上面的源码分析我们可以知道,实现原理就是通过提前定义好不同项目类型的模版,模版中包含对应的配置信息以及所需的依赖包名,当用户输入对应的项目类型和包管理工具时,选择对应的模版,安装相关的依赖,把相应的配置写入对应文件中,即实现了自动为项目添加 eslint
和 pretty
的功能。因此,定制我们自己的插件只需按照以下步骤修改即可
./lib/templates
下的模版文件vite-pretty-lint
是基于 vite
项目的,如果是非 vite
项目,就需要调整 ./lib/main.js
文件中对 vite.config.ts
文件进行修改的部分代码。通过对 vite-pretty-lint
的源码学习,我们了解给项目添加 eslint
、pretty
的新思路,后续也可以制定属于自己团队的快捷插件。我们还了解通过 babel
操作 AST
的方式修改源代码,相较于通过正则匹配的形式,通过 babel
修改源代码自由度会更高些。