原文地址: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功能,比如测试当前文件,告别命令行:
成品
最后有新项目建立只要往packages下加,然后执行pnpm sync就行啦,再也不用一个个配置eslint和jest环境了。
工作中的大型项目可能不适用这种模式,但是自己的玩具项目/练手项目的eslint/jest配置可以通过这种形式进行统一注入,还是比较省事的。
参考资料:
- Quick Start:用 pnpm 管理 Monorepo 项目:zhuanlan.zhihu.com/p/422740629
- 一个很好的monorepo最小库,可以用来作为研究monorepo的起点:github.com/mmazzarolo/…
- jestjs.io/docs/config…