- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第35期,链接:为 vite 项目自动添加 eslint 和 prettier。
说明
本质上针对create-vite-pretty-lint插件的使用,后面我们就该插件如何实现为 vite 项目自动添加 eslint 和 prettier,进行源码分析。
准备
我们先看下该插件的使用时机。
- 使用
npm init vite创建vite项目(本质是使用create-vite脚手架创建项目); - 终端执行
npm init vite-pretty-lint(本质是使用create-vite-pretty-lint插件); - 根据终端提示,选择当前框架类型(react/vue/react-ts/vue-ts);
- 根据终端提示,选择当前包管理工具(npm/yarn/pnpm);
- 选择完成,自动根据框架和包管理工具类型,自动安装对应的eslint和prettier插件,并写入对应的配置文件;
目标
- 了解
create-vite-pretty-lint的实现过程; - 学会几款给终端交互、加特效插件的使用;
- 对path模块几个方法的重温;
- 其他一些收获;
源码分析
源码克隆:
git clone https://github.com/tzsk/vite-pretty-lint.git
关于源码克隆等一些过程,此处不再阐述。
一、入口文件main.js
从package.json中可以知道入口文件是bin目录下的main.js文件。
从main.js可以看到,整体就是执行了run函数。
二、run函数
核心的逻辑都在这个run函数中,等下我们就针对函数中主要的逻辑进行分析,由于逻辑中引用了一些公共方法,因此等下分析的过程会穿插那些方法。
1、一些特效插件
这边简单介绍下此处使用到的几个插件。
- chalk:文字显示时指定的颜色(终端可console.log输出);
- gradient-string:文字渐变色(终端可console.log输出)
- enquirer:终端交互的插件(如输入、选择...)
- nanospinner:给终端加执行状态动画(如开始、成功、失败、警告...)
以上四款插件,都是用于终端交互、加文字特效动画的,读者可自行上npm/github中深入如何使用。 其实就run函数中对于他们的使用还是比较简单的,因此不再过多阐述。
PS:大家可以自行建文件夹,去安装这几个插件,并分别使用来测试各插件的效果。
2、终端交互
enquirer插件进行终端交互的方法封装在askForProjectType中,下面简单描述下过程。
const answers = await askForProjectType();
过程:
- 第一个交互:选择框架类型;
- 选择的选项来自getOptions方法返回值;
- 使用fs读取template目录下的框架模板文件路径,并使用parse解析文件路径对象,拿到文件名(不包含后缀),如react/vue/react-ts/vue-ts,然后选择;
- 包管理工具直接就是['npm', 'yarn', 'pnpm']选择;
这个过程你应该掌握以下知识点:
- path的join:拼接路径,注意resolve是获取绝对路径;
- path的parse:对文件地址解析,转成对象,包含文件名、后缀、目录地址等;
- import.meta.url:获取当前文件的file协议的地址;
- url的fileURLToPath:可以把file协议地址转成磁盘路径;
- fs的readFileSync:同步读取文件;
- fs的readdirSync:同步读取目录;
- JSON.stringify:第三个参数如果是字符串,则作为缩进对应的字符串,如果是数字则缩进对应数量空格;
3、汇总配置信息
let projectType, packageManager;
try {
const answers = await askForProjectType();
projectType = answers.projectType;
packageManager = answers.packageManager;
} catch (error) {
console.log(chalk.blue('\n👋 Goodbye!'));
return;
}
const { packages, eslintOverrides } = await import(
`./templates/${projectType}.js`
);
const packageList = [...commonPackages, ...packages];
const eslintConfigOverrides = [...eslintConfig.overrides, ...eslintOverrides];
const eslint = { ...eslintConfig, overrides: eslintConfigOverrides };
根据交互选择的信息,去模板中取对应模板预设的需要安装的插件信息、eslint的不同文件的处理配置的信息,并和公共的配置信息合并,得到所有需要安装的插件列表packageList和eslint配置信息eslint。
4、babel处理vite配置文件
const viteConfigFiles = ['vite.config.js', 'vite.config.ts'];
const [viteFile] = viteConfigFiles
.map((file) => path.join(projectDirectory, file))
.filter((file) => fs.existsSync(file));
if (!viteFile) {
console.log(
chalk.red(
'\n🚨 No vite config file found. Please run this command in a Vite project.\n'
)
);
return;
}
const viteConfig = viteEslint(fs.readFileSync(viteFile, 'utf8'));
export function viteEslint(code) {
const ast = babel.parseSync(code, {
sourceType: 'module',
comments: false,
});
const { program } = ast;
const importList = program.body
.filter((body) => {
return body.type === 'ImportDeclaration';
})
.map((body) => {
delete body.trailingComments;
return body;
});
if (importList.find((body) => body.source.value === 'vite-plugin-eslint')) {
return code;
}
const nonImportList = program.body.filter((body) => {
return body.type !== 'ImportDeclaration';
});
const exportStatement = program.body.find(
(body) => body.type === 'ExportDefaultDeclaration'
);
if (exportStatement.declaration.type === 'CallExpression') {
const [argument] = exportStatement.declaration.arguments;
if (argument.type === 'ObjectExpression') {
const plugin = argument.properties.find(
({ key }) => key.name === 'plugins'
);
if (plugin) {
plugin.value.elements.push(eslintPluginCall);
}
}
}
importList.push(eslintImport);
importList.push(blankLine);
program.body = importList.concat(nonImportList);
ast.program = program;
return babel.transformFromAstSync(ast, code, { sourceType: 'module' }).code;
}
获取原项目中的vite配置文件,并用babel插件进行转换处理,主要是插入vite-plugin-eslint插件的import导入代码(该过程具体我也不是很懂,总之对配置文件转成ast树,判断import中是否有导入该插件,有就直接返回原配置文件内容,否则处理,插入导入vite-plugin-eslint的代码,并换行,ast编译回配置文件,并返回)。
5、终端执行子进程
const commandMap = {
npm: `npm install --save-dev ${packageList.join(' ')}`,
yarn: `yarn add --dev ${packageList.join(' ')}`,
pnpm: `pnpm install --save-dev ${packageList.join(' ')}`,
};
const installCommand = commandMap[packageManager];
if (!installCommand) {
console.log(chalk.red('\n✖ Sorry, we only support npm、yarn and pnpm!'));
return;
}
const spinner = createSpinner('Installing packages...').start();
exec(`${commandMap[packageManager]}`, { cwd: projectDirectory }, (error) => {
if (error) {
spinner.error({
text: chalk.bold.red('Failed to install packages!'),
mark: '✖',
});
console.error(error);
return;
}
fs.writeFileSync(eslintFile, JSON.stringify(eslint, null, 2));
fs.writeFileSync(prettierFile, JSON.stringify(prettierConfig, null, 2));
fs.writeFileSync(eslintIgnoreFile, eslintIgnore.join('\n'));
fs.writeFileSync(viteFile, viteConfig);
spinner.success({ text: chalk.bold.green('All done! 🎉'), mark: '✔' });
console.log(
chalk.bold.cyan('\n🔥 Reload your editor to activate the settings!')
);
});
使用exec创建子进程,使用包管理工具执行安装插件命令,并使用fs写入eslint、prettire、vite、eslintignore等配置文件。 以上就是create-vite-pretty-lint实现自动添加eslint和prettier的代码逻辑。
总结和收获
其实除了babel处理的那部分逻辑比较复杂,其余基本上还是比较简单的。
整体看下来,或多或少都要去查各种资料,比如那几个插件是如何使用的?path模块一些方法的具体结果等等,虽然今天了解了用法,明天就可能忘了,但是有印象,记录下来,下次遇到就会很快明白;其次,有些东西学到了,也可以在自己的代码中去运用,熟能生巧,自然就会了。