为 vite 项目自动添加 eslint 和 prettier

2,488 阅读5分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,  点击了解详情一起参与。 这是源码共读的第35期,链接:juejin.cn/post/711356…
我们知道eslint可以帮助我们检查代码是否符合规范,而prettier则可以对不符合规范的代码进行自动修复。所以我们的项目在工程搭建的时候都会添加eslint和prettier的相关文件并进行后续的配置。今天要学习的vite-pretty-lint可以帮助我们自动添加配置文件并且生成初始配置,真的会提高效率,我们一起学习吧!

1. 准备工作

1.1 源码准备

git  clone git@github.com:tzsk/vite-pretty-lint.git

1.2 项目准备

E:\StudySpace> npm init vite
 Project name: ... vite-test-pretty-lint
 Select a framework: » vue
 Select a variant: » vue

Scaffolding project in E:\StudySpace\vite-test-pretty-lint...

Done. Now run:

  cd vite-test-pretty-lint
  npm install
  npm run dev

可以观察新建的vite-test-pretty-lint项目下的目录结构。然后继续下面的流程

E:\StudySpace\vite-test-pretty-lint>npm init vite-pretty-lint

🚀 Welcome to Eslint & Prettier Setup for Vite!

√ What type of project do you have? · vue
√ What package manager do you use? · npm
✔ All done! 🎉

🔥 Reload your editor to activate the settings!

此时我们再观察项目的目录,和之前对比:

观察整体的目录结构结构发现多了几个文件:

观察package.json发现多了开发依赖:

观察vite.config.js文件,发现多了eslintPlugin插件:

看一下新增的文件的具体内内容,首先.gitignore文件:

.eslintrc.json文件:

.prettierrc.json文件

2. vite-pretty-lint源码分析

2.1 源码分析入口

package.json中,有这样一段代码:

{
  "bin": {
    "create-vite-pretty-lint": "lib/main.js"
  },
}

可以看到其入口代码文件为lib文件夹下的main.js文件。如下图所示:

2.2 整体流程

分析main.js文件的执行过程,将其主要流程总结为下图所示的流程图:

下面详细分析这些流程所涉及的代码。

2.3 详细分析

2.3.1 询问项目类型和包管理器

let projectType, packageManager;

try {
  const answers = await askForProjectType();
  projectType = answers.projectType;
  packageManager = answers.packageManager;
} catch (error) {
  console.log(chalk.blue('\n👋 Goodbye!'));
  return;
}

调用askForProjectType方法询问开发者项目类型和包管理器,askForProjectType代码如下:

import enquirer from 'enquirer';

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'],
    },
  ]);
}

使用enquirer的prompt方法询问项目类型和包管理器。包管理器的选项是npm,yarn以及pnpm。而项目类型的选项是getOptions函数的执行结果,getOptions代码如下:

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));

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;
}

上述代码读取templates目录下的文件,获取文件名放入OPTIONS中,最后的OPTIONS就包含了所有的项目类型。包括react-ts, react, vue-ts和vue, 如下图所示:

2.3.2 根据项目类型确定模板

const { packages, eslintOverrides } = await import(
  `./templates/${projectType}.js`
);

以projectType是vue为例说明模板文件的内容:

// lib/templates/vue.js
export const packages = ['vue-eslint-parser', 'eslint-plugin-vue'];

export const eslintOverrides = [
  {
    files: ['*.js'],
    extends: ['eslint:recommended', 'plugin:prettier/recommended'],
  },
  {
    files: ['*.vue'],
    parser: 'vue-eslint-parser',
    parserOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module',
    },
    extends: [
      'eslint:recommended',
      'plugin:vue/vue3-recommended',
      'plugin:prettier/recommended',
    ],
    rules: {
      'vue/multi-word-component-names': 'off',
    },
  },
];

packages定义vite项目中要使用eslint需要安装的依赖,这里有vue-eslint-parser和eslint-plugin-vue。其中vue-eslint-parser是eslint处理.vue文件的自定义解析器,eslint-plugin-vue是Vue.js官方ESLint 插件。

2.3.3 确定npm包和eslint配置文件内容

const packageList = [...commonPackages, ...packages];
const eslintConfigOverrides = [...eslintConfig.overrides, ...eslintOverrides];
const eslint = { ...eslintConfig, overrides: eslintConfigOverrides };

packageList的构成分成两部分一部分是公共的npm包,由commonPackages提供;一部分是和特定项目类型相关的npm包,由模板文件定义和导出packages提供。看一下commonPackages的内容:

export const commonPackages = [
  'eslint',
  'prettier',
  'eslint-plugin-prettier',
  'eslint-config-prettier',
  'vite-plugin-eslint',
];

eslint-plugin-prettier是将Prettier作为一个Eslint规则运行并将差异报告为单个Eslint问题。eslint-config-prettier用于关闭eslint中不必要的或者可能和prettier冲突的规则,是配合eslint使用的。vite-plugin-eslint是vite的eslint插件。

eslintConfigOverrides定义了eslint overrides规则,由eslintConfig.overrides以及特定项目eslintOverrides组成。eslintConfig.overrides提供一个空数组:

export const eslintConfig = {
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  overrides: [],
};

以vue.js模板为例看一eslintOverrides的内容:

export const eslintOverrides = [
  {
    files: ['*.js'],
    extends: ['eslint:recommended', 'plugin:prettier/recommended'],
  },
  {
    files: ['*.vue'],
    parser: 'vue-eslint-parser',
    parserOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module',
    },
    extends: [
      'eslint:recommended',
      'plugin:vue/vue3-recommended',
      'plugin:prettier/recommended',
    ],
    rules: {
      'vue/multi-word-component-names': 'off',
    },
  },
];

最终eslint配置文件内容由eslintConfig的overrides属性重写后的对象组成。

prettier的配置如下代码所示:

export const prettierConfig = {
  trailingComma: 'es5',
  tabWidth: 2,
  semi: true,
  singleQuote: true,
};

2.3.4 确定安装依赖包的命令

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;
}

commandMap提供了多个包管理器的安装命令,根据packageManager确定使用哪一个包管理器对应的命令,如果没有找到对应包管理器的安装命令则提示错误信息。

2.3.5 确定vite配置文件

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,
};

const viteFile = viteMap[projectType];
const viteConfig = viteEslint(fs.readFileSync(viteFile, 'utf8'));

vite的配置文件要么就是vite.config.js,要么就是vite.config.ts。如果projectType是vue或者是react则使用vite.config.js,否则使用了ts则使用vite.config.ts。viteConfig为读取vite配置文件并经过viteEslint方法重写后的内容。viteEslint方法代码如下:

import * as babel from '@babel/core';
import { blankLine, eslintImport, eslintPluginCall } from './ast.js';

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;
}

下面分析其主要流程:

(1)将代码解析为AST

babel.parseSync 解析代码返回AST , AST即抽象语法树是编译原理中的一个概念。您可以使用astexplorer.net/将代码解析为抽象语法树。如下图所示是笔者将被处理后的vite.config.js解析为抽象语法树。

(2)根据AST判断是否导入了vite-plugin-eslint

viteEslint方法接着取得AST的program属性,也就是代码对应的AST。然后从其body属性中(为一个数组)过滤ImportDeclaration,也就是所有的导入语句。然后判断是否导入了vite-plugin-eslint,如果已经导入了vite-plugin-eslint则函数返回。否则获取没有导入vite-plugin-eslint的导入语句集合并赋值给nonImportList。之后要在nonImportList中加入导入vite-plugin-eslint所代表的AST结构。

(3)处理eslintPlugin的调用

exportStatement是获取导出语句,导出语句导出的是vite的配置,viteEslint是想把eslint插件的调用放入到plugins选项中:

export default defineConfig({
  plugins: [vue(), eslintPlugin()]
});

那么就需要找到plugins选项,下图展示了viteEslint找到plugins选项的过程,您可以结合代码来理解:

找到plugins选项之后要在其value数组中加入对eslintPlugin的调用eslintPlugin(), 对应得AST结构如下:

(4)处理vite-plugin-eslint的导入

还要在importList加入导入vite-plugin-eslint语句对应得AST结构,对应得图示如下:

(5)返回重写后代码

整个AST重写完之后还要返回代码,使用transformFromAstSync来完成此工作。

2.3.6 执行安装npm包命令

exec(`${commandMap[packageManager]}`, { cwd: projectDirectory }, (error) => {})

如果包管理器使用的时npm则commandMap[packageManager]其实就是npm:`npm install --save-dev ${packageList.join(' ')}`也就是安装所有依赖的npm包。

2.3.7 将配置信息写入eslint和prettier以及vite的配置文件

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);

调用fs.writeFileSync将eslint以及prettier还有vite配置文件的内容写入相关文件。

至此,我们分析完了vite-pretty-lint的核心源码,我们总结一下相关的知识。

3.总结

本文结合调试过程详细分析了vite-pretty-lint的作用和其原理。vite-pretty-lint会根据项目类型和包管理器选择不同的配置文件模板为我们生成配置文件和安装相关依赖包。学习vite-pretty-lint在处理vite配置文件过程中我们还接触到了AST相关的概念,让我们对底层的知识也有所接触。另外vite-pretty-lint的实现也让我们看到了node.js相关API的重要性。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿