给Vite项目自动添加Eslint和Prettier, 学习create-vite-pretty-lint实现

129 阅读5分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,    点击了解详情一起参与。

这是源码共读的第35期 | 为 vite 项目自动添加 eslint 和 prettier

前言

平时的开发项目中总是会用到 Eslint 或者 Prettier去帮助我们做代码的校验和格式化, 这次学习和使用create-vite-pretty-lint 来解放双手, 一键配置Vite项目中的Eslint和Prettier

vite-pretty-lint

使用

先创建一个Vite项目

npm init vite

然后一键部署下

npm init vite-pretty-lint

关于 npm init 前面的文章也介绍过 就不费口舌了

流程

入口

先看README.md 了解项目的调试 安装 和一些基本信息,接着我们看package.json 命令的执行入口

image.png

npm run test

然后在想要测试的地方打下断点,接着进行调试!

这里对Lib 目录下 的文件做个简要分类:

  1. templates 不同框架及其TS版本用到的包以及对应的配置
  2. ast.js 解析vite配置文件时通过AST语法树转换用到的配置
  3. utils.js 得到用户选择对话函数执行完后的配置
  4. shared.js 默认的包信息以及配置 和处理vite配置文件的转换函数

主要文件(main.js)

主要是run函数部分的执行,先看顶部,得到命令执行目录 以及我们要写入的文件目录变量

//取出选项式对话函数  基于enquirer,之前有遇到类似的工具prompts
import { askForProjectType } from './utils.js';

const projectDirectory = process.cwd();
// 定义eslint配置文件
const eslintFile = path.join(projectDirectory, '.eslintrc.json');
// 定义prettierrc文件
const prettierFile = path.join(projectDirectory, '.prettierrc.json');
// 定义忽略文件
const eslintIgnoreFile = path.join(projectDirectory, '.eslintignore');

然后先看run函数第一部分,取出用户的选项

let projectType, packageManager;

try {
const answers = await askForProjectType();
//得到选项结果  配置的是Vue 还是React  以及使用的包管理器
//这里我觉得包管理器可以用之前install pkg的思路 去找锁文件 来确定这个包管理器
projectType = answers.projectType;
packageManager = answers.packageManager;
} catch (error) {
console.log(chalk.blue('\n👋 Goodbye!'));
return;
}

askForProjectType函数

//读取templates目录下所有的文件, 通过path.parse 取出文件名返回成一个数组作为选项
export function getOptions() {
  const OPTIONS = [];
  fs.readdirSync(path.join(__dirname, 'templates')).forEach((template) => {
    const { name } = path.parse(path.join(__dirname, 'templates', template));

    OPTIONS.push(name);
  });
  return OPTIONS;
}

export function askForProjectType() {
  return enquirer.prompt([
    {
      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'],
    },
  ]);
}

如果有看过之前create-vite 以及create-vue的文章,这一步很好理解,getOptions函数遍历 templates目录下所有的文件,并返回文件名组成的数组作为选项使用

//根据选择的框架 得到对应的 要安装的包信息以及  eslint配置
  const { packages, eslintOverrides } = await import(
    `./templates/${projectType}.js`
  );

  //将公共包 和 框架对应的包 以及选项 拼接  以此类推
  const packageList = [...commonPackages, ...packages];
  const eslintConfigOverrides = [...eslintConfig.overrides, ...eslintOverrides];
  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 viteConfigFiles = ['vite.config.js', 'vite.config.ts'];
  //通过遍历项目目录 将文件和目录拼接起来 然后 去检查 实际存在的 Vite配置文件 并返回这个文件对应路径文本
  const [viteFile] = viteConfigFiles
    .map((file) => path.join(projectDirectory, file))
    .filter((file) => fs.existsSync(file));

这一部分则是通过用户选择的框架以及包管理器, 得到要安装的包名和配置。 然后通过拼接得到所有的包名,和对应的选项以及Vite配置文件的目录

const viteConfig = viteEslint(fs.readFileSync(viteFile, 'utf8'));

//shared.js
export function viteEslint(code) {
    //解析传入的vite 配置文件代码 code
  const ast = babel.parseSync(code, {
    sourceType: 'module',
    comments: false,
  });
  const { program } = ast;
    //取出所有的import 语句 并删除尾部注释
  const importList = program.body
    .filter((body) => {
      return body.type === 'ImportDeclaration';
    })
    .map((body) => {
      delete body.trailingComments;
      return body;
    });
    //如果 import语法中 有包含vite-plugin-eslint 这个值 则直接返回 code 代表已经安装了eslint插件
  if (importList.find((body) => body.source.value === 'vite-plugin-eslint')) {
    return code;
  }
    //所有非import语句
  const nonImportList = program.body.filter((body) => {
    return body.type !== 'ImportDeclaration';
  });
  //所有 export default 默认导出语句
  const exportStatement = program.body.find(
    (body) => body.type === 'ExportDefaultDeclaration'
  );
    //这里应该是找export default 默认导出语句中的 调用表达式 参数类型为 object的
    //找到object中的 plugins 这个key, 并给它的值push eslintPluginCall这个对象
  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);
      }
    }
  }
   //之前取出的 所有import语句后面 增加eslint插件的import
  importList.push(eslintImport);
  //增加一个空行
  importList.push(blankLine);
  //赋值
  program.body = importList.concat(nonImportList);

  ast.program = program;

  return babel.transformFromAstSync(ast, code, { sourceType: 'module' }).code;
}

微信截图_20221209162837.png 对Babel和AST转换比较陌生,只能模棱两可的看个七七八八,注释写了个大概。 主要理解它干了什么然后接着往下看。

//取出要安装的包命令
  const installCommand = commandMap[packageManager];
  //特效
  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;
    }
    //命令执行成功后 写入eslint prettier eslintignore viteconfig 文件
    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!')
    );
  });

最后就是执行安装包命令,然后写入对应的文件,然后就全部执行完成了!

小结

对Babel、AST还比较陌生, 后面会去刷 Babel插件通关秘籍 补补课。 另外附上AST语法树转换的在线网址:astexplorer帮助理解

整体下来主要核心就是先收集用户的框架以及包管理器,然后通过模板 取出默认定义好的包名以及配置选项,然后对Vite配置文件做AST语法转换插入 Eslint的Import声明语句和Plugins声明,最后 通过 node的线程API去执行包安装命令,写入文件就完事了。

一些以后开发说不定用到的库:

  1. gradient-string 控制台输出彩色文字
  2. nanospinner 控制台输出转圈圈的特效
  3. chalk 类似gradient-string 增强文字的显示
import gradient from 'gradient-string';
import { createSpinner } from 'nanospinner';
import chalk from 'chalk';

console.log(
    gradient.summer('\n🚀 Welcome to Eslint & Prettier Setup for Vite!\n')
);

const spinner = createSpinner('Installing packages...').start();

setTimeout(() => {
    spinner.success({ text: chalk.bold.green('All done! 🎉'), mark: '✔' });
}, 2000)