给vite项目自动添加eslint和prettier

203 阅读4分钟

说明

本质上针对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

image.png 从package.json中可以知道入口文件是bin目录下的main.js文件。

image.png

从main.js可以看到,整体就是执行了run函数。

二、run函数

核心的逻辑都在这个run函数中,等下我们就针对函数中主要的逻辑进行分析,由于逻辑中引用了一些公共方法,因此等下分析的过程会穿插那些方法。

1、一些特效插件

这边简单介绍下此处使用到的几个插件。

  • chalk:文字显示时指定的颜色(终端可console.log输出);
  • gradient-string:文字渐变色(终端可console.log输出)
  • enquirer:终端交互的插件(如输入、选择...)
  • nanospinner:给终端加执行状态动画(如开始、成功、失败、警告...)

以上四款插件,都是用于终端交互、加文字特效动画的,读者可自行上npm/github中深入如何使用。 其实就run函数中对于他们的使用还是比较简单的,因此不再过多阐述。

image.png PS:大家可以自行建文件夹,去安装这几个插件,并分别使用来测试各插件的效果。

2、终端交互

enquirer插件进行终端交互的方法封装在askForProjectType中,下面简单描述下过程。

const answers = await askForProjectType();

image.png 过程:

  • 第一个交互:选择框架类型;
  • 选择的选项来自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模块一些方法的具体结果等等,虽然今天了解了用法,明天就可能忘了,但是有印象,记录下来,下次遇到就会很快明白;其次,有些东西学到了,也可以在自己的代码中去运用,熟能生巧,自然就会了。