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

1,333 阅读6分钟

前言

为了提高代码质量,大家可能会在项目中接入 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

4mk2j-smk7c.gif

  • 执行结果

iShot_2022-08-17_18.05.50.png

二、源码分析

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: 将传入的代码转换为 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 输出

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

2.4 加载进度提示

三、扩展

3.1 如何查看代码的完整 AST

3011660818392_.pic.jpg

  • 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 修改源代码自由度会更高些。

另外我们还了解一些能让我们的控制台交互更加优雅、文案输出更加漂亮的库

  • chalk,给你的终端输出内容加上样式
  • gradient,在终端输出漂亮的颜色渐变
  • nanospinner,最简单和最小的终端旋转器