monorepo在jest、eslint配置共享中的应用

2,658 阅读6分钟

原文地址:www.yuque.com/docs/share/… 《monorepo在jest、eslint配置共享中的应用》

高质量的代码开发离不开eslint语法检查工具和jest单元测试保障,当需要维护多个项目库,而不得不新建多份jest/eslint配置时,事情就会变得很麻烦:初始化的时候需要各种复制粘贴,各种零散的依赖安装也觉得头疼,不同依赖间的版本冲突、依赖更新时不得不更新另一个依赖,项目一多维护起来就加倍麻烦;不知不觉主体项目的devDependencies列表变得奇长无比;想自己建一个玩具项目也要各种配置eslint和jest,结果半天下来项目一个字没写,配置反而弄了半天。

怎么办?

monorepo可以解决上述的困境。

思路是将eslint和jest分别抽离开来变成独立的package,然后将自己的项目们作为其他的packages一起放在monorepo中,将通用的规则进行共享,并提供可以自定义的入口进行区分。

monorepo建立

这里我已经搭建了一个template:github.com/sdhr27/shar…

3步尝鲜:

(1)将你的项目复制粘贴到pacakges中(已有配置可能冲突,也可以用内置例子exercise)

(2)运行pnpm install && pnpm sync

(3)重启编辑器查看eslint、编写jest单元测试

使用pnpm作为monorepo的包管理工具会简单很多,在.npmrc中进行如下配置可以加快包安装的速度、简化根目录包安装繁琐度(不用每次都加-w)

sass_binary_site=https://registry.npmmirror.com/-/binary/node-sass/
registry=https://registry.npmmirror.com
ignore-workspace-root-check=true

配置pnpm-workspace.yaml

packages:
  - 'packages/**'

建立packages文件夹,并在其中导入项目即可。

eslint-config

目录结构

eslint共享相对简单,需要分离的内容为:eslint规则、eslint相关各种依赖:

其中index.js输出通用eslint规则,注意这里要用overrides定义,因为并不是直接的eslint配置文件:

package.json收集相关依赖并定义包名:

配置引用

完成2份核心文件后,在需要eslint配置的文件中进行引用和eslint配置文件书写即可:

比如有一个package/exercise项目需要获取eslint配置,只需要在其devDependecies中声明对应依赖来自workspace:

并在其目录下新建.eslintrc.js文件引用@easymn/eslint-config即可:

如果需要进行进一步的eslint自定义,直接在该文件下进行配置添加即可。

jest-config

jest配置相对繁琐,需要设置各种环境,转换css、less、多媒体格式文件、各种格式文件、各种es语法支持(babel配置)、canvas mock、ts规则设置、dom环境、浏览器环境模拟等等,每次单独配置都觉得很头大。

目录结构

jest统一配置目录如下:

(1)scripts中配置各种jest环境支持脚本,都是在进行单元测试场景的单测环境报错问题解决方案,可以在官方文档和社区中找到这些解决方案,这里主要有3个脚本:

  • jest-file-mock.js用于处理特殊文件的导出
  • jest-setup-mock用于模拟localstorage等浏览器环境
  • jest-setup用于引入@testing-library/jest-dom。

(2)babel.config.js用于配置测试中的各种高级es语法兼容:

const BabelPresetEnv = require('@babel/preset-env');
const BabelPresetTypescript = require('@babel/preset-typescript');
const BabelPresetReact = require('@babel/preset-react');
const BabelPluginSyntaxDynamicImport = require('@babel/plugin-syntax-dynamic-import');
const BabelPluginProposalClassProperties = require('@babel/plugin-proposal-class-properties');
const BabelPluginProposalObjectResetSpread = require('@babel/plugin-proposal-object-rest-spread');
const BabelPluginProposalOptionalChaining = require('@babel/plugin-proposal-optional-chaining');

module.exports = {
  env: {
    test: {
      presets: [
        [BabelPresetEnv, { targets: { node: 'current' } }],
        [BabelPresetTypescript],
        [BabelPresetReact, { runtime: 'automatic' }],
      ],
      plugins: [
        BabelPluginSyntaxDynamicImport,
        BabelPluginProposalClassProperties,
        BabelPluginProposalObjectResetSpread,
        BabelPluginProposalOptionalChaining,
      ],
    },
  },
};

(3)jest.config.js为jest主要配置文件:

注意这里的路径大多添加了pacakges/jest-config,这是因为单测环境包被独立到了packages目录下。

module.exports = {
  clearMocks: true,
  testEnvironment: 'jsdom',
  coveragePathIgnorePatterns: ['/node_modules/'],
  setupFiles: [
    '<rootDir>/packages/jest-config/scripts/jest-setup-mock.js',
    '<rootDir>/packages/jest-config/node_modules/jest-canvas-mock',
  ],
  setupFilesAfterEnv: ['<rootDir>/packages/jest-config/scripts/jest-setup.ts'],
  moduleNameMapper: {
    '\.(css|less)':
      '<rootDir>/packages/jest-config/node_modules/identity-obj-proxy/src/index.js',
  },
  transform: {
    '\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|css)$':
      '<rootDir>/packages/jest-config/scripts/jest-file-mock.js',
    '^.+\.(ts|tsx)$':
      '<rootDir>/packages/jest-config/node_modules/ts-jest/preprocessor.js',
    '^.+\.(js|jsx|mjs)$':
      '<rootDir>/packages/jest-config/node_modules/babel-jest/build/index.js',
  },
  globals: {
    'ts-jest': {
      tsconfig: '<rootDir>/tsconfig.json',
      diagnostics: false,
      babelConfig: '<rootDir>/babel.config.js',
      useESM: true,
    },
  },
};

(4)最后用索引文件进行配置项导出:

module.exports.babel = require('./babel.config');
module.exports.jest = require('./jest.config');

jest配置引用

jest配置引用需执行以下步骤:

  • 在根目录下新建一份jest.config.js、babel.config.js、tsconfig.json
  • 在根目录下安装eslint、jest、typescript、@types/jest、@types/react(react项目)、@easymn/jest-config(内置jest-config包)(特别注意这些包可能存在版本冲突,高低版本要协调好)

jest.config.js的配置最为重要,需要根据不同的projects进行区分测试环境配置。@easymn/jest-config内置包只提供最通用的测试环境和配置,可以额外添加更细化的配置,比如路径alias:

module.exports = {
  // 配置收集代码覆盖率
  collectCoverage: true,
  // 配置monorepo projects
  projects: [
    {
      ...require('@easymn/jest-config').jest,
      // 一个项目一份配置,可写上displayName进行区分
      displayName: 'exercise',
      // 进一步缩减项目测试范围
      roots: ['<rootDir>/packages/exercise'],
    },
  ],
};

babel配置很通用,基本没有自定义需求,如果有,像平常一样写即可:

module.exports = require('@easymn/jest-config').babel;

当然内置包的引用肯定要有:

{
  ...,
  "devDependencies": {
    "@easymn/eslint-config": "workspace:^0.0.0",
    "@easymn/jest-config": "workspace:^0.0.0"
  }
  ...
}

如此看来其实jest-config装在根目录也可以,因为单个项目的配置定义是通过projects字段完成的而不像eslint是在更深层级中声明.eslintrc文件进行覆盖。

但如果希望项目的依赖更整洁(依赖整理强迫症),逻辑更清晰,分个包其实也是不错的选择,毕竟jest的环境依赖实在太多了。

sync-config同步配置脚本

jest配置初始化一次即可,可以通过templete实现,后续自定义可以再往相同文件上进行添加。

内置依赖项初始化、eslint配置文件每次添加新的package都需要执行,因此可以编写一个自动化脚本来执行这些重复工作。

同步脚本执行2个任务:

  • 注入@easymn/jest-config依赖和@easymn/eslint-config依赖到package.json中
  • 写入/改写.eslintrc.js文件,将@easymn/eslint-config加入到配置文件的extends字段中。

由于文件改写相关内容涉及ast语法解析,需要安装babel相关支持库@babel/generator、@babel/traverse、@babel/types、@babel/parser,且其功能相对独立,也可以作为独立的package进行抽离,其package.json结构如下:

在npm scripts调用脚本实现上述任务。

最后在根目录的npm scripts中调用packages/sync-config中的对应脚本:

{
	"scripts": {
    "sync": "pnpm -C ./packages/sync-config sync && pnpm lint",
    "lint": "prettier -c --write **/jest.config.js **/.eslintrc.js **/package.json",
  },
}

写文件会有格式问题,可以额外写一个prettier格式化脚本进行格式矫正。

辅助配置

可以额外配置一些prettier自动格式化功能、vscode debug功能来优化我们的开发环境

有了launch.json后可以自定义各种vscode debug功能,比如测试当前文件,告别命令行:

成品

github.com/sdhr27/shar…

最后有新项目建立只要往packages下加,然后执行pnpm sync就行啦,再也不用一个个配置eslint和jest环境了。

工作中的大型项目可能不适用这种模式,但是自己的玩具项目/练手项目的eslint/jest配置可以通过这种形式进行统一注入,还是比较省事的。

参考资料: