分析Anthony Fu的eslint monorepo实践

4,352 阅读5分钟

⚠更新:

目前antfu佬的项目已经不再使用monorepo进行eslint的分包了,但是local-pkg仍然是有用到的。

本文目标:

  • 了解如何Anthony Fu大佬是如何用Monorepo的方式撰写eslint配置的
  • 接触local-pkg,了解如何一套eslint适配不同项目(react/vue/vanillaJS/ts)

前言

我是一个普通的 antfu(Anthony Fu) 的迷弟。某一天,在研究他的 Vitesse 模板的时候,我看到了他的 eslint 配置竟然如此简单

{
  "extends": [
    "@antfu",
    "@unocss"
  ]
}

我们都知道 extends 配置是继承已经写好了的eslint的配置,但是不同于我经常在网上看到的一大堆extends、rules还有parser之类的配置,antfu的 extends 只有这一点,并且没有任何其它的配置,这就让我不禁想要了解一下他的具体实现

目录结构

让我们排除掉一些无关的文件和目录,专注于我们需要的部分

eslint-config
├─ packages # 多个eslint配置包
│  ├─ eslint-config # 项目的入口
│  │  ├─ index.js # extends: "@antfu"时找到的文件
│  │  └─ package.json 
│  ├─ eslint-config-basic # 基础、通用的eslint配置
│  │  ├─ index.js
│  │  ├─ package.json
│  │  └─ standard.js
│  ├─ eslint-config-ts # ts项目专用的eslint配置
│  │  ├─ index.js
│  │  └─ package.json
│  ├─ eslint-config-vue # Vue专用的eslint配置
│  │  ├─ index.js
│  │  └─ package.json
│  ├─ eslint-config-react # React专用的eslint配置
│  │  ├─ index.js
│  │  └─ package.json
│  └─ eslint-plugin-antfu # antfu自己写的eslint插件,文章分享中将不涉及
├─ .eslintrc.json # eslint配置
├─ package.json # 项目整体的package.json
├─ pnpm-workspace.yaml # pnpm
├─ README.md
└─ tsconfig.json # typescipt配置

我们点进 packages/eslint-config/index.js进行查看,发现里面的代码无它,只有一个短短的 extends

module.exports = {
  extends: [
    '@antfu/eslint-config-vue',
  ],
}

那么这个 @antfu/eslint-config-vue 是从哪儿来的呢,我们进入到 package.json就能看到这其实就是一个包

  "dependencies": {
    "@antfu/eslint-config-vue": "workspace:*",
    ...
  },

不了解 workspace 的朋友也不用担心,我们之后会讲的。接下来我们直接进入 packages/eslint-config-vue 开始分析吧!

适配js/ts

Anthony Fu的eslint是会自动判断项目是基于js还是ts,然后引入对应的配置的,那么具体是怎么实现的呢?

区分项目类型 - local-pkg 的妙用

进入到 package.json,我们会看到该包入口即为index.js

  "main": "index.js", // https://docs.npmjs.com/cli/v9/configuring-npm/package-json#files
  "files": [ // https://docs.npmjs.com/cli/v9/configuring-npm/package-json#files
    "index.js"
  ],

然后我们来看下 index.js的代码,大概内容如下

const { isPackageExists } = require('local-pkg') // 引入local-pkg

const TS = isPackageExists('typescript') // 判断

module.exports = {
  overrides: [
    {
      files: ['*.vue'], // 覆盖 .vue 文件的eslint配置,比如parser, rules
      ...
    },
  ],
  extends: [
    'plugin:vue/vue3-recommended',
    TS
      ? '@antfu/eslint-config-ts' // 如果是ts项目,extends ts配置
      : '@antfu/eslint-config-basic', // 如果不是ts项目, extends 基础配置
  ],
  rules: { ... } // vue项目的规则
 }

在这里我们发现他首先从 local-pkg 中引入了 isPackageExists,然后用 isPackageExists 去判断项目中是否使用了ts。

如果使用了ts,就引入 @antfu/eslint-config-ts 配置,没有的话,就引入 @antfu/eslint-config-basic 配置。

antfu佬是只做了ts/js的适配,但是我们知道了原理就可以自己去写一套eslint适配各个技术栈的项目。

简化 extends 写法

那从这里我们也大概知道了只要合理利用 locak-pkg,我们就能够适配多个项目了。那么为什么可以直接继承 @antfu 而不是 @antfu/eslint-config呢,这个其实是归功于eslint自身的extends的设定,比如 extends: 'eslint-config-test' 可以被简化为 extends: 'test',那么简化的规则有哪些呢

  • "extends": "eslint-config-test" => "extends": "test"
  • "extends": "@scoped/eslint-config" => "extends": "@scoped"
  • "extends": "@scoped/eslint-config-test" => "extends": "@scoped/test"

(详情可以参考 eslint官方文档的说明

项目中的monorepo是如何实践的

package.json中的 workspaces (工作区)

如果设置了 workspaces (也就是工作区),那么你在项目根目录进行 npm install的时候,npm会自动找到 workspaces 中的目录下的package.json,将该package.json下的依赖安装到项目根目录,然后将该目录符号链接到node_modules下。

比如说我现在有一个项目目录结构如下

test-workspaces
├─ packages 
│  ├─ child
│  |  | index.js
│  └─ └─ package.json 
├─ index.js
└─ package.json

image.png

image.png 执行npm install之后安装对应依赖(express),然后进行符号链接

image.png

注意!pnpm不适用package.json下的 workspaces

pnpm-workspace 有什么作用

如果你只在package.json中设置了 workspaces,会发现执行 pnpm install 的时候有一行提示

image.png

pnpm天生支持monorepo,你可以在项目根目录下创建 pnpm-workspace.yaml 去指定工作区

packages:
 - 'packages/*'

版本号是 workspace: * 意味着什么?

我们在 packages/eslint-config-vue/package.json 中可以看到,两个eslint配置包并没有指定版本,而是 workspace: *

image.png

这样是为了告诉npm,使用工作区中的这个包作为依赖项。

换句话来说,在 @antfu/eslint-config-vue 这个项目中,引入 @antfu/eslint-config-basic 其实是从工作区引入的,* 代表了使用该包最新的本地版本

如何发布到线上

pnpm publish? pnpm -r publish!

适配原理讲完了,工作区的概念也有了,那么接下来就剩下发布了。

我们不能只发布 eslint-config,其它的依赖比如 eslint-config-basiceslint-config-tseslint-config-...这些都需要进行发布,那难道我们需要直接进入到packges里面一个个发布吗?

其实我们只需要一行代码,pnpm就能自动帮我们递归地进行发布

pnpm -r publish

-r 代表 recursive,也就是递归。

image.png

手动更改版本号做版本控制? bumpp

我们不希望每次都手动去修改版本号,antfu佬必然也不希望,那么他是怎么做的呢?项目根目录下的package.json有这样一行script

  "scripts": {
    "release": "bumpp -r && pnpm -r publish"
  },

可以看到在进行publish之前会先执行执行 bumpp -r。其实 bumpp -r就是递归地将一个Monorepo中的项目都进行版本控制。执行 pnpm add -D bumpp 之后再在package.json中添加一句scripts

  "scripts": {
    "bump": "bumpp -r",
  },

执行pnpm bump就可以看到下面的画面(确保你的项目是个git仓库并且已经设置了远程origin)

image.png

选择要更新的版本,然后它就会自动进行版本更新、commit、tag、push到远程仓库,遵循 conventional commits 的规范。

如果这篇文章对你有帮助的话,还请动动小手点个免费的赞,这会让我创作更有动力,谢谢你🥳

如果这篇文章哪里有问题的话,也欢迎你指出,我们可以友好讨论🤓

参考资料:

  1. docs.npmjs.com/cli/v9/conf…
  2. pnpm.io/workspaces
  3. pnpm.io/pnpm-worksp…

如果你正好在写简历,毛遂自荐一下自己的简历生成器: