- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- **这是源码共读的第 35 期,链接:juejin.cn/post/711356… **
前言
为了提高代码质量,大家可能会在项目中接入 eslint
、prettier
等工具,并且制定一些属于自己团队的代码规范,在新增项目时,会从旧项目中去拷贝相关的配置的文件,同时去安装对应插件库(当然,在大一点或规范一点的团队会去维护自己的脚手架),除了脚手架的形式,是否还有更加高效的方式去给新项目添加 eslint
和 prettier
吗?答案是肯定的,vite-pretty-lint 就实现了为 vite
项目一键添加 eslint
和 prettier
,下面让我们一起来学习一下它是如何实现的。
源码
- 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
)
- 执行结果
二、源码分析
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
中查找需要插入的位置,并插入相关内容 - 再通过
babel
把AST
转成普通JS
代码
export function viteEslint(code) {
// NOTE: 将传入的代码转换为 AST
const ast = babel.parseSync(code, {
// 指示代码应该被解析的模式,可以是 'script'、'module' 或 'unambiguous'。
sourceType: 'module',
// 是否在生成的 AST 中输出注释
comments: false,
});
// 取出主题程序部分的 AST
const { program } = ast;
// 取出引入依赖(import)的 AST
const importList = program.body
.filter((body) => {
return body.type === 'ImportDeclaration';
})
.map((body) => {
// 删除注释的 AST
delete body.trailingComments;
return body;
});
// NOTE: 查询是否引入了 vite-plugin-eslint,若已经引入了,就直接返回传人的代码 code
if (importList.find((body) => body.source.value === 'vite-plugin-eslint')) {
return code;
}
// NOTE: 取出非 import 部分的代码的 AST
const nonImportList = program.body.filter((body) => {
return body.type !== 'ImportDeclaration';
});
// NOTE: 取出 「export default」声明的代码 AST
const 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 输出
2.4 加载进度提示
- nanospinner,最简单和最小的终端旋转器
三、扩展
3.1 如何查看代码的完整 AST
- 通过 astexplorer.net ,我们可以查看代码的完整
AST
AST
的这些节点类型代表啥意思,看不懂怎么半? 可以在这里查找相关的 AST 节点- 外语不太好的同学想要更加深入的了解
babel
以及AST
,推荐大家看一看神光的Babel 插件通关秘籍
3.2 定制属于自己团队的 lint-pretty 插件
通过上面的源码分析我们可以知道,实现原理就是通过提前定义好不同项目类型的模版,模版中包含对应的配置信息以及所需的依赖包名,当用户输入对应的项目类型和包管理工具时,选择对应的模版,安装相关的依赖,把相应的配置写入对应文件中,即实现了自动为项目添加 eslint
和 pretty
的功能。因此,定制我们自己的插件只需按照以下步骤修改即可
- Fock vite-pretty-lint 项目
- 修改或替换
./lib/templates
下的模版文件 - 因为
vite-pretty-lint
是基于vite
项目的,如果是非vite
项目,就需要调整./lib/main.js
文件中对vite.config.ts
文件进行修改的部分代码。
四、总结
通过对 vite-pretty-lint
的源码学习,我们了解给项目添加 eslint
、pretty
的新思路,后续也可以制定属于自己团队的快捷插件。我们还了解通过 babel
操作 AST
的方式修改源代码,相较于通过正则匹配的形式,通过 babel
修改源代码自由度会更高些。
另外我们还了解一些能让我们的控制台交互更加优雅、文案输出更加漂亮的库
- chalk,给你的终端输出内容加上样式
- gradient,在终端输出漂亮的颜色渐变
- nanospinner,最简单和最小的终端旋转器