pnpm
是 performant npm
(高性能的 npm
),它是一款快速的,节省磁盘空间的包管理工具,同时,它也较好地支持了 workspace
和 monorepos
,简化开发者在多包组件开发下的复杂度和开发流程。
在上一篇《pnpm技术体系之:高性能包管理工具》讲到pnpm的优势,在本章节,我们开始着手搭建一个完整流程的开源组件。
本篇章的全部代码已上传到 github,有需要自取。
npm install pnpm -g
pnpm init
此外,我们要额外创建pnpm的配置文件:.npmrc
,配置如下:
shamefully-hoist=false
detect_chromedriver_version=true
strict-peer-dependencies=false
一般教程都是这样配置的:
shamefully-hoist=true
,但本人不推荐。这样做会把里面的依赖提升到全局node_module
里面,有可能出现幽灵依赖的风险。
pnpm 内置了对单一存储库(也称为多包存储库、多项目存储库或单体存储库)的支持, 你可以创建一个 workspace 以将多个项目合并到一个仓库中,这样的作用是能在我们开发调试多包时,彼此间的依赖引用更加简单。
创建工作空间也非常简单,假设我们的项目中有3个包:
.
└── packages
├── playground
├── small-color-ui
└── utils
这时候我们在根目录创建一个 pnpm-workspace.yaml
文件,里面添加如下配置,这样在packages
范围下的包都能共享工作空间了。
packages:
- 'packages/*'
完事后,假如我们想在small-color-ui
包里面使用utils
,那直接在small-color-ui
终端执行安装命令(安装包名为utils
的package.json
文件name
字段):
$ cd packages/small-color-ui
$ pnpm i -D @small-color-ui/utils
接下来会看到packages/small-color-ui/package.json
中已经包含utils包的依赖了。
至于utils的版本为workspace:*
,是因为pnpm是由workspace管理的,所以有一个前缀workspace可以指向utils下的工作空间从而方便本地调试各个包直接的关联引用,但这种引用会在publish时自动被pnpm纠正为正常版本。你可以在 官网 找到workspace version
更多信息。
基础框架搭建好后,我们再看下如何配置组件包的package.json
,让其满足我们的开发&&发布需求。例如,我们的主包:packages/small-color-ui/package.json
,配置如下:
{
"name": "small-color-ui",
"private": false,
"version": "1.0.0",
"type": "module",
"description": "small-color-ui core",
"license": "MIT",
"author": "Johnny",
"contributors": [],
"main": "src/main.tsx",
"module": "src/main.tsx",
"publishConfig": {
"main": "dist/tts-controller.cjs.js",
"module": "dist/tts-controller.es.js",
"typings": "dist/src/main.d.ts"
},
"repository": {
"type": "git",
"url": "git@github.com:JohnnyZhangQiao/pnpm-monorepo-learn.git"
},
"bugs": {
"url": "https://github.com/JohnnyZhangQiao/pnpm-monorepo-learn/issues"
},
"files": [
"dist",
"README.md"
],
"keywords": [
"small-color-ui"
],
"scripts": {
"dev": "vite",
"build": "tsc && vite build && pnpm run build:types",
"preview": "vite preview",
"build:types": "tsc --p tsconfig.types.json"
},
"dependencies": {
"@small-color-ui/utils": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@vitejs/plugin-react-swc": "^3.0.0",
"less": "^4.1.3",
"typescript": "^4.9.3",
"vite": "^4.0.0"
}
}
解析一下关键字段:
utils
包),请注册到同一个组织下面。这时候utils
的包名就可以为:@small-color-ui/utils
,代表隶属@small-color-ui
组织。true
代表私有包,publish
时不会执行发布操作。main
和 module
:定义入口文件,项目在具备ESM 规范情况下,module
具备更高的识别优先级。typescript
类型描述,缺失会导致组件被引用时失去类型提示。package.json
、README
、LICENSE
/ LICENCE
和 主入口文件。一般来讲,pnpm对于工作空间的依赖安装分2种,一种是普通安装,另一种是使用-w
(--workspace-root
)参数,它代表把依赖安装到工作空间中。关于-w
的作用,举个例子:
假如你使用以下命令,那么在整个工作空间内的所有组件都能直接使用react
。
pnpm i -Sw react
但如果你在某个包使用以下命令,那么react
只能在这个包内被引用,其他组件不会识别到react
依赖。
pnpm i -S react
这里的建议是,假如多包共享的依赖,可以直接安装到工作空间里,特性包则避免使用这参数。
关于-w
的更多用法,你可以参考官网说明。
.d.ts
类型描述文件一般优秀的开源组件,都会在发布时顺便发布一份类型描述文件,这样的作用:一是能友好给使用者方法引入以及参数类型提示;二是能保证组件参数传递规范。
我们要生成对应的类型文件,只需要在tsconfig.json
加上以下配置:
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
}
为了能达到更好的项目配置分离,我们可以把生成类型的配置单独抽离出来,配合extends
把通用的tsconfig.json
融合进来即可,如下图:
最后,在package.json
增加以下命令,在构建类型文件时指定tsconfig
:
"scripts": {
"build:types": "tsc --p tsconfig.types.json"
},
由于本项目用vite
来做打包工具,所以主要用到rollup
的打包策略,具体vite.config.ts
配置如下:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import type { OutputOptions } from 'rollup';
export default defineConfig({
plugins: [react()],
build: {
target: 'modules',
//打包文件目录
outDir: 'dist',
//压缩
minify: false,
//css分离
cssCodeSplit: false,
// rbga色值禁止转成十六进制
cssTarget: 'chrome61',
lib: {
entry: './src/App.tsx',
formats: ['es', 'cjs'],
name: 'small-color-ui-core',
},
rollupOptions: {
// 排除打包的库
external: ['react', 'react-dom'],
input: ['./src/App.tsx'],
output: ['esm', 'cjs'].map((format) => ({
name: 'small-color-ui',
format,
dir: 'dist',
entryFileNames: `small-color-ui.[format].js`,
assetFileNames: 'index.css',
preserveModulesRoot: 'src',
})) as OutputOptions[],
},
},
});
要发布自己的软件包到npm,先要注册一个个人或企业账号,注册入口。
另外,假如你包里有子依赖,并隶属一个组织下,还要再添加个组织,一般组织名和你主包名一致。组织创建入口
对于免费开源包,一般选下面Unlimited public packages
即可。
万事俱备,我们回到项目控制台里面,在发包前先登录npm账号:
# 建议指定registry,避免登录到公司内部的开源库中去
pnpm login --registry https://registry.npmjs.org/
按部就班输入以下4项,便能登录成功。
众所周知,我们发布到npm肯定是构建产物,所以在publish
前要对组件执行build
操作,在根目录的package.json
添加以下命令:
"build": "pnpm build:utils && pnpm build:core",
"build:core": "pnpm --filter small-color-ui build",
"build:utils": "pnpm --filter @small-color-ui/utils build",
因为有2个发布包,所以要对它们都要构建,其中pnpm --filter <package_name> <command>
是pnpm的检索属性,它能执行指定的package目录下的某个命令。上面的 build:core
和 build:utils
就是分别执行2个包的构建,再把2条命令整合到 build
中,完成发包前的组件构建流程。
这里要借用到某个插件——changesets
。
它是一款切合pnpm体系下的一款管理版本控制和变更日志的工具,专注于多包存储库。虽然pnpm下暂时没有像lerna
完善的发布流程工具,但changesets
也算的上是官方推荐的一款,将就用吧。
changesets
的执行流程大概可以理解为:生成临时的changelog → 消耗changelog生成组件的更新记录,并更新组件version → 发布组件
pnpm install @changesets/cli
根目录运行changeset init
,会生成一个 .changeset
目录,里面会生成一个 changeset
的 config
文件(linked
字段改成你自己的包名):
{
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [["@small-color-ui/*"]],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
}
}
然后在根目录的package.json
添加以下命令:
"changeset": "changeset",
"update:version": "changeset version",
"release": "changeset publish",
其中:
changeset
:生成临时的changelogupdate:version
:消耗changelog生成组件的更新记录,并更新组件versionrelease
:发布组件执行命令:pnpm changeset
,按提示输出,最后生成临时日志。
日志里面包含发版的组件包,版本更新类型(major | minor | patch
),最下面带有更新内容。
执行命令:pnpm update:version
,临时日志被消耗,会在组件包生成CHANGELOG.md
,另外,package.json
的版本号也同步修改。
执行命令:pnpm release
,更新组件会发布到npm。
到上面为止,我们已经完成在pnpm monorepo
的完整开发到发布流程,但对于企业开发者来讲,代码仓库的质量也是追求的重要指标之一,我们现在把eslint
与prettier
引入到项目中。
eslint
根目录安装:
pnpm i -Dw eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-react
新建.eslintrc
:
{
"env": {
"node": true,
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:prettier/recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["react", "@typescript-eslint", "prettier"],
"rules": {
"react/react-in-jsx-scope": "off",
"react/display-name": 0
}
}
最后,在根目录的package.json
增加以下命令,一键检查代码:
"scripts": {
"lint": "eslint --fix --ext .js,.tsx,ts packages"
},
prettier
根目录安装:
pnpm i -Dw prettier eslint-config-prettier eslint-plugin-prettier
新建.prettierrc.js
:
module.exports = {
// 一行最多 120 字符..
printWidth: 120,
// 使用 2 个空格缩进
tabWidth: 2,
// 不使用缩进符,而使用空格
useTabs: false,
// 行尾需要有分号
semi: true,
// 使用单引号
singleQuote: true,
// 对象的 key 仅在必要时用引号
quoteProps: 'as-needed',
// 末尾需要有逗号
trailingComma: 'all',
// 大括号内的首尾需要空格
bracketSpacing: true,
// jsx 标签的反尖括号需要换行
jsxBracketSameLine: false,
// 箭头函数,只有一个参数的时候,也需要括号
arrowParens: 'always',
// 每个文件格式化的范围是文件的全部内容
rangeStart: 0,
rangeEnd: Infinity,
// 不需要写文件开头的 @prettier
requirePragma: false,
// 不需要自动在文件开头插入 @prettier
insertPragma: false,
// 使用默认的折行标准
proseWrap: 'preserve',
// 根据显示样式决定 html 要不要折行
htmlWhitespaceSensitivity: 'css',
// 换行符使用 lf
endOfLine: 'lf',
};
众所周知 Git
有很多的钩子函数,让我们在不同的阶段对代码进行不同的操作。我们可以在项目的.git/hooks
目录中,找到所有的hooks的例子:
git
钩子捕获pnpm i -Dw husky lint-staged
添加husky安装命令,执行完后会自动在package.json
添加一条script:
npm pkg set scripts.prepare="husky install"
接下来执行prepare
命令,完成husky初始化,最终会在项目根路径生成.husky
目录。
pnpm prepare"
上面讲了,lint-staged
会检查缓存区代码,但假如需要git hooks触发时执行检查操作,那么就要把lint-staged
关联到husky
中去了。
关联pre-commit hook
:
pnpx husky add .husky/pre-commit "pnpx lint-staged"
完成后.husky
目录如下:
在package.json文件下添加如下代码:
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --fix"
]
},
这里在触发代码检查会做两件事:1. 修复缓存区代码风格;2. 修复缓存区代码格式错误;
测试一下,OJBK了。
对于提交信息的规范,当然是大名鼎鼎的Google AnguarJS 规范。 格式如下:
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
要完成上面的规范化提交格式,我们需要借用2个工具。
pnpm i -Dw commitizen cz-conventional-changelog @commitlint/config-conventional @commitlint/cli
在根目录创建commitlint.config.js
,并写入以下配置:
module.exports = { extends: ['@commitlint/config-conventional'] };
接下来,我们要在husky配置commit-msg
钩子,让提交信息与commitlint
关联起来:
pnpx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'
最后,在根目录的package.json
添加配置:
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
},
至此,我们测试下,又OJBK了。。。因为commit
信息不规范,所以被husky
拦截了。
假如是我们纯粹输入commit message的话,要完全符合规范实属鸡肋,接下来,我们要使用命令交互式流程嵌入到commitlint
中。
我们再增加一条script
:
npm pkg set scripts.commit="cz"
然后运行pnpm commit
命令,控制台交互如下:
对于规范的组件开源法则来讲,单测也是重要一环,它能保证组件的稳定性。由于单测是持续性建设的工作,这块日后有空再补齐。👻👻
好了好了,卷到这里又要和朋友们说拜拜,聪明的小伙伴已经跟着搭建起来了,赶紧去试试吧。。。
感谢大家阅览并欢迎纠错,欢迎大家关注本人公众号「似马非马」,一起玩耍起来!🌹🌹
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。