更加高效的为新项目添加 eslint 和 prettier

前言

为了提高代码质量,大家可能会在项目中接入 eslintprettier 等工具,并且制定一些属于自己团队的代码规范,在新增项目时,会从旧项目中去拷贝相关的配置的文件,同时去安装对应插件库(当然,在大一点或规范一点的团队会去维护自己的脚手架),除了脚手架的形式,是否还有更加高效的方式去给新项目添加 eslintprettier 吗?答案是肯定的,vite-pretty-lint 就实现了为 vite 项目一键添加 eslintprettier,下面让我们一起来学习一下它是如何实现的。

源码

  • vite-pretty-lint
  • 项目文件结构(以下结构仅包含了主要的实现代码 lib 目录)
.
├── ast.js
├── main.js
├── shared.js
├── templates
│   ├── react-ts.js
│   ├── react.js
│   ├── vue-ts.js
│   └── vue.js
└── utils.js 

一、使用

1.1 如何使用

  • 由于这个库限定了使用环境(vite 项目),所以我们通过以下命令创建一个验证项目
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
更加高效的为新项目添加 eslint 和 prettier_第1张图片
  • 执行结果
更加高效的为新项目添加 eslint 和 prettier_第2张图片

二、源码分析

2.1 整体流程

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!'));});
} 

2.2 shell 交互问答–askForProjectType 实现

代码在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'],},]);
} 

2.3 viteEslint,通过babel修改原有的 vite 配置文件

  • 读取 vite.config.ts 文件的代码
  • 通过 babel 转化成抽象语法书
  • AST 中查找需要插入的位置,并插入相关内容
  • 再通过 babelAST 转成普通 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;
} 

2.3 彩色渐变的 log 输出

  • chalk,给你的终端输出内容加上样式
  • gradient,在终端输出漂亮的颜色渐变

2.4 加载进度提示

  • nanospinner,最简单和最小的终端旋转器

三、扩展

3.1 如何查看代码的完整 AST

  • 通过 astexplorer.net ,我们可以查看代码的完整 AST
更加高效的为新项目添加 eslint 和 prettier_第3张图片
  • AST 的这些节点类型代表啥意思,看不懂怎么半? 可以在这里查找相关的 AST 节点
  • 外语不太好的同学想要更加深入的了解 babel 以及 AST,推荐大家看一看神光的Babel 插件通关秘籍

3.2 定制属于自己团队的 lint-pretty 插件

通过上面的源码分析我们可以知道,实现原理就是通过提前定义好不同项目类型的模版,模版中包含对应的配置信息以及所需的依赖包名,当用户输入对应的项目类型和包管理工具时,选择对应的模版,安装相关的依赖,把相应的配置写入对应文件中,即实现了自动为项目添加 eslintpretty 的功能。因此,定制我们自己的插件只需按照以下步骤修改即可

  • Fock vite-pretty-lint 项目
  • 修改或替换 ./lib/templates 下的模版文件
  • 因为 vite-pretty-lint 是基于 vite 项目的,如果是非 vite 项目,就需要调整 ./lib/main.js 文件中对 vite.config.ts 文件进行修改的部分代码。

四、总结

通过对 vite-pretty-lint 的源码学习,我们了解给项目添加 eslintpretty 的新思路,后续也可以制定属于自己团队的快捷插件。我们还了解通过 babel 操作 AST 的方式修改源代码,相较于通过正则匹配的形式,通过 babel 修改源代码自由度会更高些。

你可能感兴趣的:(javascript,前端,vue.js)