pnpm技术体系之:打造企业级 pnpm 开源组件

lxf2023-03-20 20:42:01

开场

pnpm 是 performant npm(高性能的 npm),它是一款快速的,节省磁盘空间的包管理工具,同时,它也较好地支持了 workspace 和 monorepos,简化开发者在多包组件开发下的复杂度和开发流程。

在上一篇《pnpm技术体系之:高性能包管理工具》讲到pnpm的优势,在本章节,我们开始着手搭建一个完整流程的开源组件。

pnpm monorepo搭建

本篇章的全部代码已上传到 github,有需要自取。

1. 初始化项目

1.1. 安装pnpm

npm install pnpm -g

1.2. 初始化package.json

pnpm init

1.3. 配置 .npmrc

此外,我们要额外创建pnpm的配置文件.npmrc,配置如下:

shamefully-hoist=false
detect_chromedriver_version=true
strict-peer-dependencies=false

一般教程都是这样配置的:shamefully-hoist=true,但本人不推荐。这样做会把里面的依赖提升到全局node_module里面,有可能出现幽灵依赖的风险。

1.4. 创建工作空间

pnpm 内置了对单一存储库(也称为多包存储库、多项目存储库或单体存储库)的支持, 你可以创建一个 workspace 以将多个项目合并到一个仓库中,这样的作用是能在我们开发调试多包时,彼此间的依赖引用更加简单。

创建工作空间也非常简单,假设我们的项目中有3个包:

.
└── packages
    ├── playground
    ├── small-color-ui
    └── utils

这时候我们在根目录创建一个 pnpm-workspace.yaml文件,里面添加如下配置,这样在packages范围下的包都能共享工作空间了。

packages:
  - 'packages/*'

完事后,假如我们想在small-color-ui包里面使用utils,那直接在small-color-ui终端执行安装命令(安装包名为utilspackage.json文件name字段):

$ cd packages/small-color-ui
$ pnpm i -D @small-color-ui/utils

接下来会看到packages/small-color-ui/package.json中已经包含utils包的依赖了。

pnpm技术体系之:打造企业级 pnpm 开源组件

至于utils的版本workspace:*,是因为pnpm是由workspace管理的,所以有一个前缀workspace可以指向utils下的工作空间从而方便本地调试各个包直接的关联引用,但这种引用会在publish时自动被pnpm纠正为正常版本。你可以在 官网 找到workspace version更多信息。

2. 组件的package.json配置

基础框架搭建好后,我们再看下如何配置组件包的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"
  }
}

解析一下关键字段:

  • name:组件名,也是我们要发布到npm上面的名称。假如有子依赖包(如上面的utils包),请注册到同一个组织下面。这时候utils的包名就可以为:@small-color-ui/utils,代表隶属@small-color-ui组织。
  • private:布尔类型,true代表私有包,publish时不会执行发布操作。
  • version:发布版本。
  • type:文件引入规范,module | commonjs,分别代表采用ESModule或commonjs规范来引入文件。
  • mainmodule:定义入口文件,项目在具备ESM 规范情况下,module具备更高的识别优先级。
  • publishConfig:在publish时,里面对应的入口会替换掉外层,一般本地开发时指向src目录,发布后指向dist目录。
  • typings:组件的typescript类型描述,缺失会导致组件被引用时失去类型提示。
  • files:组件作为依赖项时会安装的目录/文件,支持正则匹配,默认会带上4项:package.jsonREADMELICENSE / LICENCE 和 主入口文件。
  • dependencies:打包带上的子依赖。
  • devDependencies:开发环境的子依赖。

3. 关于依赖安装

一般来讲,pnpm对于工作空间的依赖安装分2种,一种是普通安装,另一种是使用-w(--workspace-root)参数,它代表把依赖安装到工作空间中。关于-w的作用,举个例子

假如你使用以下命令,那么在整个工作空间内的所有组件都能直接使用react

pnpm i -Sw react

但如果你在某个包使用以下命令,那么react只能在这个包内被引用,其他组件不会识别到react依赖。

pnpm i -S react

这里的建议是,假如多包共享的依赖,可以直接安装到工作空间里,特性包则避免使用这参数。

关于-w的更多用法,你可以参考官网说明。

4. 生产.d.ts类型描述文件

一般优秀的开源组件,都会在发布时顺便发布一份类型描述文件,这样的作用:一是能友好给使用者方法引入以及参数类型提示;二是能保证组件参数传递规范。

我们要生成对应的类型文件,只需要在tsconfig.json加上以下配置:

"compilerOptions": {
  "declaration": true,
  "emitDeclarationOnly": true,
}

为了能达到更好的项目配置分离,我们可以把生成类型的配置单独抽离出来,配合extends把通用的tsconfig.json融合进来即可,如下图:

pnpm技术体系之:打造企业级 pnpm 开源组件

最后,在package.json增加以下命令,在构建类型文件时指定tsconfig

"scripts": {
  "build:types": "tsc --p tsconfig.types.json"
},

5. 打包配置

由于本项目用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[],
    },
  },
});

6. 发布组件

6.1. npm创建账号与组织

要发布自己的软件包到npm,先要注册一个个人或企业账号,注册入口。

另外,假如你包里有子依赖,并隶属一个组织下,还要再添加个组织,一般组织名和你主包名一致。组织创建入口

对于免费开源包,一般选下面Unlimited public packages即可。

pnpm技术体系之:打造企业级 pnpm 开源组件

6.2. 发布命令

万事俱备,我们回到项目控制台里面,在发包前先登录npm账号:

# 建议指定registry,避免登录到公司内部的开源库中去
pnpm login --registry https://registry.npmjs.org/

按部就班输入以下4项,便能登录成功。

pnpm技术体系之:打造企业级 pnpm 开源组件

6.3. 组件打包

众所周知,我们发布到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:corebuild:utils 就是分别执行2个包的构建,再把2条命令整合到 build 中,完成发包前的组件构建流程。

6.4. 自动化发布流和生成发布记录

这里要借用到某个插件——changesets

它是一款切合pnpm体系下的一款管理版本控制和变更日志的工具,专注于多包存储库。虽然pnpm下暂时没有像lerna完善的发布流程工具,但changesets也算的上是官方推荐的一款,将就用吧。

changesets的执行流程大概可以理解为:生成临时的changelog → 消耗changelog生成组件的更新记录,并更新组件version → 发布组件

6.4.1. 安装changeset

pnpm install @changesets/cli

6.4.2. 初始化changeset配置

根目录运行changeset init,会生成一个 .changeset 目录,里面会生成一个 changesetconfig 文件(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
  }
}

6.4.3. 配置changeset发布流命令

然后在根目录的package.json添加以下命令:

"changeset": "changeset",
"update:version": "changeset version",
"release": "changeset publish",

其中:

  • changeset:生成临时的changelog
  • update:version:消耗changelog生成组件的更新记录,并更新组件version
  • release:发布组件

6.4.4. 生成changeset临时日志

执行命令:pnpm changeset,按提示输出,最后生成临时日志。

pnpm技术体系之:打造企业级 pnpm 开源组件

pnpm技术体系之:打造企业级 pnpm 开源组件

日志里面包含发版的组件包,版本更新类型(major | minor | patch),最下面带有更新内容。

6.4.5. 消耗日志

执行命令:pnpm update:version,临时日志被消耗,会在组件包生成CHANGELOG.md,另外,package.json的版本号也同步修改。

pnpm技术体系之:打造企业级 pnpm 开源组件

6.4.6. 发版

执行命令:pnpm release,更新组件会发布到npm。

pnpm技术体系之:打造企业级 pnpm 开源组件

7. eslint与prettier

到上面为止,我们已经完成在pnpm monorepo的完整开发到发布流程,但对于企业开发者来讲,代码仓库的质量也是追求的重要指标之一,我们现在把eslintprettier引入到项目中。

7.1. 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"
},

7.2. 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',
};

8. git规范

8.1. git hooks

众所周知 Git 有很多的钩子函数,让我们在不同的阶段对代码进行不同的操作。我们可以在项目的.git/hooks目录中,找到所有的hooks的例子:

pnpm技术体系之:打造企业级 pnpm 开源组件

8.2. 配置代码提交规范

8.2.1. 工具

  • huskygit 钩子捕获
  • lint-staged:暂存区代码检查工具

8.2.2. 安装

pnpm i -Dw husky lint-staged

8.2.3. 初始化husky

添加husky安装命令,执行完后会自动在package.json添加一条script:

npm pkg set scripts.prepare="husky install"

接下来执行prepare命令,完成husky初始化,最终会在项目根路径生成.husky目录。

pnpm prepare"

8.2.4. husky关联lint-staged

上面讲了,lint-staged会检查缓存区代码,但假如需要git hooks触发时执行检查操作,那么就要把lint-staged关联到husky中去了。

关联pre-commit hook

pnpx husky add .husky/pre-commit "pnpx lint-staged"

完成后.husky目录如下:

pnpm技术体系之:打造企业级 pnpm 开源组件

8.2.5. 添加lint-staged检查逻辑

在package.json文件下添加如下代码:

"lint-staged": {
  "*.{js,jsx,ts,tsx}": [
    "prettier --write",
    "eslint --fix"
  ]
},

这里在触发代码检查会做两件事:1. 修复缓存区代码风格;2. 修复缓存区代码格式错误;

测试一下,OJBK了。

pnpm技术体系之:打造企业级 pnpm 开源组件

8.3. 配置提交message规范

对于提交信息的规范,当然是大名鼎鼎的Google AnguarJS 规范。 格式如下:

<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>

要完成上面的规范化提交格式,我们需要借用2个工具。

8.3.1. 工具

  • commitlint:commit 信息校验工具
  • commitizen:命令行交互插件

8.3.2. 安装

pnpm i -Dw commitizen cz-conventional-changelog @commitlint/config-conventional @commitlint/cli

8.3.3. 配置commitlint

在根目录创建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拦截了。

pnpm技术体系之:打造企业级 pnpm 开源组件

8.3.4. 配置commitizen

假如是我们纯粹输入commit message的话,要完全符合规范实属鸡肋,接下来,我们要使用命令交互式流程嵌入到commitlint中。

我们再增加一条script

npm pkg set scripts.commit="cz"

然后运行pnpm commit命令,控制台交互如下:

pnpm技术体系之:打造企业级 pnpm 开源组件

10. 单元测试

对于规范的组件开源法则来讲,单测也是重要一环,它能保证组件的稳定性。由于单测是持续性建设的工作,这块日后有空再补齐。