用pnpm搭建menorepo工程
初始化项目
本项目采用monorepo
,所以优先安装pnpm
npm install -g pnpm
创建一个空项目,执行 pnpm init
生成package.json
文件
pnpm init
在根目录新建pnpm-workspace.yaml
,内容如下:
packages:
- 'packages/*'
我们所有的 packages 都放在 packages 目录下。在根目录创建 packages
文件夹。
在packages下分别创建两个文件夹 core
和 vue
且分别在 core
和 vue
文件夹下执行命令 npm init
。name
分别为 @mysentry/core
和 @mysentry/vue
。
常用的monorepo pnpm命令
-w, --workspace-root
在根目录执行命令,比如在根目录安装依赖,那么这个依赖可以在所有的packages中使用
// 例如根目录安装rollup打包
pnpm add rollup -w -D
-F<package_name>, --filter <package_name>
在过滤的指定包运行命令,我们可以通过下面的命令在指定的package安装依赖,这个依赖只可以在该package中使用
pnpm -F @mysentry/core add lodash
给@mysentry/vue添加@mysentry/core依赖
pnpm install @mysentry/core -r --filter @mysentry/vue
可以看到vue
文件夹下的package.json
文件中的dependencies
中就存在了@mysentry/core
。
集成TypeScript
安装依赖
// 根目录执行
pnpm install typescript@4.9.4 rollup-plugin-typescript2 tslib -D -w
初始化
// 根目录执行
pnpm tsc --init
初始化后会在根目录生成.tsconfig.json
文件,并修改为如下内容:
{
"compilerOptions": {
"outDir": "dist", // 输出的目录
"sourceMap": true, // 开启 sourcemap
"target": "es2016", // 转译的目标语法
"module": "esnext", // 模块格式
"moduleResolution": "node", // 模块解析方式
"strict": false, // 关闭严格模式,就能使用 any 了
"resolveJsonModule": true, // 解析 json 模块
"esModuleInterop": true, // 允许通过 es6 语法引入 commonjs 模块
"jsx": "preserve", // jsx 不转义
"lib": ["esnext", "dom"], // 支持的类库 esnext 及 dom
"baseUrl": ".", // 当前目录,即项目根目录作为基础目录
}
}
集成Eslint + prettier
安装Eslint
根目录下执行
pnpm install eslint@8.29.0 -D -w
安装typescript解释器及ts规则
pnpm install @typescript-eslint/parser@5.46.1 @typescript-eslint/eslint-plugin@5.46.1
创建.eslintrc文件
此文件为eslint配置文件
{
// 配置root后就不会向上寻找配置
"root": true,
// ts 解释器
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
// eslint官方扩展,无需再安装npm包就可使用
"eslint:recommended",
// 通过使用这个扩展,可以继续利用 ESLint 的核心推荐规则集,同时避免与 TypeScript 类型系统的检查产生不必要的冲突
"plugin:@typescript-eslint/eslint-recommended",
// 为 TypeScript 项目提供的一个推荐规则集
"plugin:@typescript-eslint/recommended",
],
"rules": {
"no-useless-escape": "off",
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off"
}
}
创建.eslintignore文件
此文件是为了告诉eslint
忽略哪些文件不做检查
dist
errorJson.js
browser.js
node_modules
test
.gitignore
.prettierignore
scripts/*
rollup.config.mjs
添加Lint命令
在package.json
中配置lint命令
"scripts": {
// 省略...
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix"
}
安装Prettier
pnpm install prettier -D -w
创建.prettierrc.json
{
// 单行100字符
"printWidth": 100,
// 缩进
"tabWidth": 2,
// 空格缩进
"useTabs": false,
// 分号
"semi": true,
// 单引号
"singleQuote": true,
// 箭头函数参数 单个参数不需要括号
"arrowParens": "avoid",
// 不检测CRLF or LF
"endOfLine": "auto"
}
将代码格式化交给Prettier
我们引入prettier
会和eslint
存在格式化代码冲突,我们将代码格式化交给prettier
,eslint
负责ts
检查。
pnpm install --save-dev eslint-plugin-prettier eslint-config-prettier -D -w
修改.eslintrc
{
"root": true,
// ts 解释器
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
+ "prettier"
],
"extends": [
// eslint官方扩展,无需再安装npm包就可使用
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
+ "prettier/prettier": "error",
"no-useless-escape": "off",
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off"
}
}
集成husky + lint-staged
下载husky
pnpm install husky -D -w // 9.0.6
初始化husky
// 执行此命令初始化 v9版本
npx husky init
ps:注意此命令是V9版本husky配置,V8版本看此处husky版本文档
修改pre-commit文件
执行命令后会生成.husky
文件夹,修改.husky
文件夹下pre-commit
文件
- npm test
+ npm run lint
测试husky
修改任意文件,使得不符合eslint
规范,然后执行git add .
,再执行git commit -m '测试husky'
,会提交失败并且将不符合eslint
规范得地方抛出为成功。
下载lint-staged
lint-staged
是配置只检查暂存区的代码进行检验
pnpm install lint-staged -w -D // 15.2.0
配置lint-staged
在package.json
文件中添加
"lint-staged": {
"**/*.{ts,tsx,json}": [
"prettier --write",
"eslint --fix"
]
},
在pre-commit
文件修改如下:
- npm run lint
+ npx lint-staged
测试lint-staged
我在gitee
上修改core/src/index.ts
文件,将不符合规范得代码保存,本地拉取后,本地修改core/src/index.ts
文件,只检查了core/src/index.ts
文件,视为正确。
集成commitizen + cz-customizable
git提交约定,我们将采用Angular
提交规范
全局安装commitizen
npm install -g commitizen
安装cz-customizable
此包为自定义提交规则,我们暂时不用,可以直接看下一步
pnpm install cz-customizable -D -w
Commitizen适配器
这里使用得得提交规则为Angular
pnpm install cz-conventional-changelog -D -w
- 在项目中安装cz-conventional-changelog 适配器依赖
- 将适配器依赖保存到
package.json
的devDependencies
字段信息 - 在
package.json
中手动新增config.commitizen
字段信息,主要用于配置cz工具的适配器路径:
"devDependencies": {
"cz-conventional-changelog": "^3.3.0"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
接下来可以使用cz的命令**git cz
代替git commit
进行提交说明**
然后我们发现用git commit -m '非法提交'
任然生效,后面我们将解决这个问题
commitlint
校验提交说明是否符合规范,安装校验工具
npm install --save-dev @commitlint/cli
@commitlint/config-conventional
安装符合Angular
风格的校验规则
npm install --save-dev @commitlint/config-conventional
配置文件 commitlint.config.js
在根目录下创建commitlint.config.js
文件
module.exports = {
extends: ['@commitlint/config-conventional'],
};
使用husky中commit-msg钩子验证
在.husky
文件夹下创建commit-msg
文件,内容如下:
npx --no-install commitlint --edit $1
验证
使用git commit -m '不符合angular规范'
提交代码,会失败即为成功。
创建Packages
创建子包
在packages
文件夹下创建core
文件夹。在core
文件夹下,执行npm init
。修改package.json
如下:
{
"name": "@mysentry/core",
// 打包配置
"buildOptions": {
"name": "MySentryCore",
"formats": [
"esm",
"cjs"
]
},
// 版本号
"version": "1.0.2",
"description": "",
// 包入口
"main": "dist/core.cjs.js",
"module": "dist/core.esm.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"lodash": "^4.17.21"
}
}
在packages/core
下创建src
目录,在src
目录下创建core
目录和index.t
index.ts
入口文件,编写如下代码:
import { SDK_NAME, add } from './core/index'
function init ():void {
console.log('init')
let sum = add(1, 3)
}
export default {
SDK_NAME,
init
}
在packages/core/src/core
目录下,分别创建index.ts
,add.ts
,config.ts
index.ts
export * from './add'
export * from './config'
add.ts
export const add = (a: number, b:number): number => {
return a + b
}
config.ts
export const SDK_NAME = 'Sentry';
接下来,在packages
目录下创建vue
文件夹,在vue
目录下执行npm init
,修改package.json
如下:
{
"name": "@mysentry/vue",
"buildOptions": {
"name": "MySentryVue",
"formats": [
"esm",
"cjs",
"umd"
]
},
"version": "2.0.1",
"description": "",
"main": "dist/vue.cjs.js",
"module": "dist/vue.esm.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
// 此依赖是执行上面提到的给@mysentry/vue添加@mysentry/core依赖命令 pnpm install @mysentry/core -r --filter @mysentry/vue
"@mysentry/core": "workspace:^"
}
}
创建src
文件夹,创建index.ts
文件并编写如下代码:
import { reactive } from "./vue/index";
export default {
reactive
}
在src
文件夹再创建vue
文件夹并创建index.ts
,编写如下代码:
export * from './reactive'
在vue
目录下创建reactive.ts
import core from '@mysentry/core';
export const reactive = () => {
console.log(core.SDK_NAME)
}
Rollup打包
我们将基于已经大家安好的Monorepo
项目进行rollup
打包
依赖安装
因为vue
采用的是rollup
进行打包, 我们也采用rollup
。
# 安装rollup
pnpm install rollup -D -w
# 安装rollup插件
pnpm install @rollup/plugin-json @rollup/plugin-node-resolve -D -w
# 安装execa
pnpm install execa -D -w
rollup
是一个类似于webpack
的打包工具,rollup文档@rollup/plugin-json
是一个能让我们从json
文件中导入数据的插件。@rollup/plugin-node-resolve
是一个能让我们从node_modules
中引入第三方模块的插件。execa
是一个能让我们手动执行脚本命令的一个工具。
依赖全部安装完毕后,根目录packages.json
文件的devDependencies
信息如下:
"devDependencies": {
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"execa": "^8.0.1",
"rollup": "^4.13.0"
}
脚本命令
在实现之前,我们先在根目录下创建scripts
目录,并新建一个build.js
文件,其内容如下:
console.log('build.js')
然后,在根目录package.json
文件中添加打包命令,如下:
"scripts": {
"build": "node scripts/build.js"
}
最后,在控制台中执行build
命令,可以在终端成功看到打印内容:
# 执行命令
pnpm run build
# 输出内容
node scripts/build.js
build.js
打包
现在,我们思考一个问题:因为我们packages
目录下可能会存在很多个子包,所以我们需要为每个子包都执行一次打包命令,并输出dist
到对应的目录下。
Nodejs在14版本以上,可以通过在
package.json
中设置type为module,来使项目可以使用import xx from 'xx'
来引入依赖。
基于以上问题,我们将可能面临的问题进行拆分
-
如何准确识别出所有的子包?
可以采用
node
中的fs
模块去读packages
目录下的所有子文件夹/文件,然后保留所有文件夹就是我们的所有子包,实现代码如下:const fs = require("fs"); const pkgs = fs.readdirSync('packages').filter(p => { // statSync() 方法返回文件信息状态 isDirectory()如果是文件目录则返回true return fs.statSync(`packages/${p}`).isDirectory() }) console.log(pkgs) // ['core', 'vue']
代码介绍:
readdirSync()
返回指定目录下所有文件名称组成的数组,statSync()
和isDirectory()
返回指定文件的详细信息对象,isDirectory()
方法返回当前文件是否为文件夹。在撰写完以上代码后,我们再次执行打包命令,可以看到如下打印信息。
pnpm run build # 输出信息 node scripts/build.js ['core', 'vue']
-
如何使用execa进行一次打包命令
假设现在要给
packages/core
打包,可以先这样做:const fs = require("fs"); + const execa = require("execa"); const build = async (pkg) => { await execa('rollup', ['-c', '--environment', `TARGET:${pkg}`], { stdio: 'inherit' }) } build('core')
以上
execa
执行的命令相当于rollup -c --environment TARGET:core
命令解读:
-c
代表制定rollup
配置文件,如果其后没有跟文件名,则默认取根目录下的rollup.config.js
文件。--environment
表示注入一个环境变量,在我们的打包命令中注入了一个TARGET
,可以使用process.env.TARGET
取出来,其值为core
。现在,我们在根目录下新建
rollup.config.js
文件,并编写如下代码:const pkg = process.env.TARGET console.log(pkg)
然后,再次运行打包命令:
TIP
因为
rollup.config.js
还没有配置,所以运行报错是正常的 -
如何批量执行打包命令
有了
core
的打包经验,我们就可以实现给所有子包都打包,其实现代码如下:const runParallel = (targets, buildFn) => { const res = [] for(const target of targets) { res.push(buildFn(target)) } return Promise.all(res) } const build = async (pkg) => { await execa('rollup', ['-c', '--environment', `TARGET:${pkg}`], { stdio: 'inherit' }) } runParallel(pkgs, build)
再次执行打包命令:输出结果如下:
TIP
因为
rollup.config.js
还没有配置,所以运行报错是正常的# 打包命令 pnpm run build
-
如何配置rollup?
在根路径下创建
rollup.config.mjs
文件,根据rollup
的基础知识,我们知道需要提供input
、output
以及plugin
等配置,可以撰写如下代码:import json from '@rollup/plugin-json' import nodeResolve from '@rollup/plugin-node-resolve' // 根据执行build.js中代码分别设置 packages 不同的值,例如打包core文件夹时 pkg就为core,同理打包vue时,pkg则为vue const pkg = process.env.TARGET const createConfig = (name) => { return { input: 'src/index.js', output: { name, file: `dist/${name}.esm.js`, format: 'esm' }, plugins: [ json(), nodeResolve() ] } } module.exports = createConfig(pkg)
先别着急运行打包命令,因为以上代码还存在一些问题:
a.
input
和file
的路径不正确,需要定义一个resolve
函数来表示当前package
包的路径b.
output.name
的值,我们希望是MySentryXxx
而不是xxx
c.
format
方式希望能够支持ESM
、Commonjs
和UMD
这三种规范先来解决第一个问题,我们第一个的
resolve
函数如下:// rollup.config.js 新增代码 import path from 'path' import { fileURLToPath } from 'url' const pkg = process.env.TARGET // 获取绝对路径并且实现 获取到打包时当前package文件夹,例如正在打包core时,获取的就是packages/core下的p文件 const __dirname = fileURLToPath(new URL('.', import.meta.url)) const resolve = (p) => { return path.resolve(`${__dirname}/packages/${pkg}`, p); }; console.log(resolve('src/index.js'))
既然
resovle
函数已经定义好了,那么修改rollup.config.cjs
文件后完整代码如下:import path from 'path' import { fileURLToPath } from 'url' import json from '@rollup/plugin-json' import nodeResolve from '@rollup/plugin-node-resolve' const pkg = process.env.TARGET const resolve = (p) => { return path.resolve(`${__dirname}/packages/${pkg}`, p) } const createConfig = (name) => { return { input: resolve('src/index.js'), output: { name, file: resolve(`dist/${name}.esm.js`), format: 'esm' }, plugins: [ json(), nodeResolve() ] } } module.exports = createConfig(pkg)
接下来解决第二个问题,既然要自定义名字,那么可以选择在当前包的
package.json
文件中去定义,我们以packages/core
为例:修改
packages/core/package.json
文件{ "name": "@mysentry/core", "buildOptions": { "name": "MySentryCore" }, ...省略其他 }
其他packages
下的子项目同理修改,都修改好后我们需要去读取这个配置,代码如下:
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const { buildOptions } = require(resolve('package.json'))
接着,在createConfig
方法中去修改output.name
的值:
const createConfig = (name) => {
return {
output: {
name: buildOptions.name,
...省略其他
},
...省略其他
}
}
最后解决第三个问题,根据前面配置name
的思路,可以在buildOptions
中去定义另外一个属性formats
,同样以core
为例:
// packages/core目录下的packages.json
{
"name": "@mysentry/core",
"buildOptions": {
"name": "MySentryCore",
"formats": [
"esm",
"cjs"
]
},
...省略其他
}
为了更好的进行区分打包的产物,我们在core
配置["esm", "cjs"]
,而对于vue
配置["esm", "cjs", "umd"]
。
在所有formats
配置完毕后,我们再次修改rollup.config.cjs
,完整代码如下:
import path from 'path'
import { createRequire } from 'module'
import { fileURLToPath } from 'url'
import json from '@rollup/plugin-json'
import nodeResolve from '@rollup/plugin-node-resolve'
import typescript from 'rollup-plugin-typescript2';
import commonjs from '@rollup/plugin-commonjs';
const pkg = process.env.TARGET;
const require = createRequire(import.meta.url)
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const resolve = (p) => {
return path.resolve(`${__dirname}/packages/${pkg}`, p);
};
const { buildOptions } = require(resolve('package.json'));
const formatMap = {
esm: {
file: resolve(`dist/${pkg}.esm.js`),
format: 'esm'
},
cjs: {
file: resolve(`dist/${pkg}.cjs.js`),
format: 'cjs'
},
umd: {
file: resolve(`dist/${pkg}.js`),
format: 'umd'
}
}
const createConfig = (output) => {
output.name = buildOptions.name
return {
input: resolve("src/index.ts"),
output,
plugins: [
typescript({
tsconfigOverride: {
compilerOptions: {
module: 'ESNext',
},
},
useTsconfigDeclarationDir: true,
}),
json(),
commonjs(),
nodeResolve()],
};
};
const configs = buildOptions.formats.map(format => createConfig(formatMap[format]))
export default configs
接着,我们来运行打包命令:
# 运行打包命令
pnpm run build
# 输出信息
created packages/core/dist/core.cjs.js in 12ms
created packages/core/dist/core.esm.js in 37ms
created packages/vue/dist/vue.esm.js in 58ms
created packages/vue/dist/vue.cjs.js in 30ms
created packages/vue/dist/vue.js in 22ms
可以看到 packages/core/dist 和 packages/vue/dist。
发布到npm
如果要想发布package
,还需要有一个默认的入口文件,我们以packages/core
为例,在其目录下新建index.js
,并撰写如下代码:
需要在core
根目录下创建index.js
module.exports = require('./dist/core.cjs.js')
你可能会很疑惑,为什么只导出CommomJs
规范的文件,ESM
和UMD
规范的文件又该如何导出呢?
其实,这两个规范可以在其目录下的package.json
文件中去配置导出,以core
为例:
{
"name": "@mysentry/core",
"main": "dist/core.cjs.js",
"module": "dist/core.esm.js", // ESM规范导出
"unpkg": "dist/core.js" // UMD规范配合CDN
}
安装和初始化changesets
在项目根目录下执行 pnpm install @changesets/cli -D -w
pnpm install @changesets/cli -D -w
执行pnpm changeset init
初始化 changeset
, 在根目录下会生成.changeset
文件夹,其中config.json
文件是 changeset
`的配置文件。
.changeset
文件夹不能被 git 忽略,需要一起提交到git上!
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
// changelog 生成方式
"changelog": "@changesets/cli/changelog",
// 不要让 changeset 在 publish 的时候帮我们做 git add
"commit": false,
"fixed": [],
// linked: 配置哪些包要共享版本
"linked": [],
// access: 公私有安全设定,内网建议 restricted ,开源使用 public
"access": "public",
// baseBranch: 项目主分支
"baseBranch": "master",
// updateInternalDependencies: 确保某包依赖的包发生 upgrade,该包也要发生 version upgrade 的衡量单位(量级)
"updateInternalDependencies": "patch",
// ignore: 不需要变动 version 的包
"ignore": []
}
发布第一个版本
优先将整个项目推入到gitee/github
上,这步我们就不赘述了。
然后我们需要在npm官网上创建一个Organization
, 名称为mysentry
,其实就是取得@mysentry/xxx
中得,创建好后还需要执行pnpm adduser
登录自己得npm账号密码还有邮箱。
然后执行 pnpm changeset publish
就发布到npm上了。
更新版本
修改功能代码,我们还是用core
来举例子:
// 新增代码
export const test = () => {
console.log('test')
}
执行pnpm run build
执行打包
然后执行 pnpm changeset add
,会生成对话,选择子项目等等。
然后执行pnpm changeset version
更新版本号
最后执行pnpm changeset publish
推送到npm官网
下载包及使用
创建一个vue项目,然后执行pnpm install @mysentry/core
下载包,然后在main.js
中使用
import { test } from '@mysentry/core'
test()