Pnpm 构建 Monorepo

Pnpm 构建 Monorepo
Photo by Volodymyr Hryshchenko / Unsplash

最近开始搭建 monorepo 的项目,了解到以下方案:

  • lerna 有强大完善的依赖管理和构建、打包发布流程,而且 v5 版本将 nx 与 lerna 整合了,提升了性能和速度
  • 业界大多使用 lerna + yarn workspace 的方式来管理,yarn 用来管理依赖,lerna 用于管理发布

之前简单使用过 lerna ,不过再加上 yarn workspace , 学习容多、配置多又得折腾,太费劲🤪,暂时搁置。

最终尝试使用单一的 pnpm workspace + changesets 的方式来构建 monorepo,本身我就使用 pnpm 作为包管理工具,目前我的场景简单,pnpm 就已足够。

本文记录自己使用 pnpm workspace 搭建 monorepo 来管理多个脚手架的项目实践,👉 项目属于 nodejs 项目,非前端 UI 项目,开发环境如下:

  • pnpm workspace 管理 monorepo 的所有依赖
  • 提供 typescript 的开发环境
  • 使用 es module 开发

初始化

新建工程目录 pnpm-monorepo,并将项目初始化 pnpm 管理,git 初始化 repo

pnpm init && git init

新建 pnpm-workspace.yaml 配置如下,通过 glob 语法将 packages 下的文件夹当做子 package 管理

packages:
 - 'packages/**'

在 packages 文件夹中新建几个子 package 如下

├── package.json
├── packages
│   ├── app
│   ├── lib
│   └── log
└── pnpm-workspace.yaml

分别在 app、lib、log 这些子 package 目录执行 pnpm init ,👉 并且将子 package 中的 name 字段改为以后要发布到 npm 上的包名,示例如下:

{
  "name": "@hey-cli/app",
  "version": "0.0.1",
  "description": "@hey-cli脚手架入口文件",
  "main": "build/index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "CaptainJack",
  "license": "ISC"
}

这种重复新建子 package,需要新建文件夹,重复 pnpm init 并且修改 name 的操作,挺繁琐的,如何优化?

常用命令

--filter 指令,功能很强。--recursive 是个递归执行的执行

pnpm add 安装依赖

pnpm add: 通过 pnpm -h 命令可以查看 add 命令可搭配的详细参数

workspace 顶层安装依赖

pnpm add typescript -w

在指定的子 package 中安装依赖,使用强大的 --filter 命令,其中 @hey-cli/app 为子package 的 name 字段

pnpm add commander --filter @hey-cli/app -D

pnpm remove 移除依赖

pnpm remove commander -D

如果需要安装线上 npm 的依赖包,直接 pnpm add 即可,如果是本地子 package 相互依赖引用,需要使用 pnpm link 来建立依赖。

在子 package @hey-cli/app 中安装本地依赖 @hey-cli/log,workspace 的 monorepo 模式下,pnpm 会优先链接到本地 package ,比如安装 @hey-cli/log 时,pnpm 先查找到 pnpm-workspace.yaml 文件中 配置的 packages 中就有 @hey-cli/log 这个包,就会直接使用这个本地依赖

pnpm add @hey-cli/log --filter @hey-cli/app

注意此处的 workspace 标识,代表是从子 package 中链接的本地包

"dependencies": {
  "@hey-cli/log": "workspace:^0.0.1"
}

当执行 pnpm publish 时,上述 workspace:^0.0.1 依赖会自动变成 ^0.0.1 ,即线上依赖的形式,在发布环节会具体介绍

执行子 package script 脚本

⚠️注意,不可以在子 package 路径下,直接运行命令,这样会无法使用到顶层的公共项目依赖。

pnpm 也不需要携带 run 来执行命令,直接执行命令本身就行,如 pnpm start

在子 package.json 文件中新增一个 start 命令

// package.json
"scripts": {
  "start": "echo \"这是子 package @hey-cli/app 的 script 脚本\""
}

单独运行子 package 的脚本

使用 --filter 指令来指定子 package 包 @hey-cli/app 来运行脚本

🚜  CaptainJack # ~/Temp/pnpm-monorepo 🌵 Git > master 🤖 [12:18:58]
👉 pnpm --filter @hey-cli/app start
​
> @hey-cli/[email protected] start /Users/pearl/Temp/pnpm-monorepo/packages/app
> echo "这是子 package @hey-cli/app 的 script 脚本"

--recursive 来运行所有的子 package 的命令

--recursive 可以简写为 -r ,支持递归执行子 package 的命令

🚜  CaptainJack # ~/Temp/pnpm-monorepo 🌵 Git > master 🤖 [12:19:06]
👉 pnpm -r start
  Scope: 3 of 4 workspace projects
  packages/lib start$ echo "这是子 package @hey-cli/lib 的 script 脚本"
  │ 这是子 package @hey-cli/lib 的 script 脚本
  └─ Done in 12ms
  packages/app start$ echo "这是子 package @hey-cli/app 的 script 脚本"
  │ 这是子 package @hey-cli/app 的 script 脚本
  └─ Done in 7ms

提供 typescript + es module 支持

安装配置 typescript

安装到顶层 workspace 的 devDependencies 中,顶层 workspace 中的依赖,子 package 中能直接使用

pnpm add typescript @types/node ts-node -D -w

新建一个通用的 tsconfig.json,其他具体 packages 子包可以基于这个通用 tsconfig.json 来做扩展,一定要开启 esModuleInterop 选项, typescript 中支持通过 esModuleInterop 配置 es module 导入 CJS 这种情况时引入额外的辅助函数,进一步对兼容性进行支持。

{
  "compilerOptions": {
    "module": "ESNext",
    "target": "ES2020",
    "esModuleInterop": true,
    "moduleResolution": "node",
    "declaration": true,
    "noImplicitAny": true,
    "removeComments": true,
    "noLib": false,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "sourceMap": true,
    "lib": ["es6"]
  },
  "exclude": ["node_modules", "**/*.spec.ts"]
}

配置 es module

在子 package 的 package.json 中,设置 "type": "module" ,代表其是 es module 项目

{
  "type": "module",
  "scripts": {
    "start": "ts-node-esm src/index.ts"
  },
}

此处必须使用 ts-node-esm 来提供对 es module 的支持,不能用 ts-node 命令。

构建

对于大型项目,有复杂构建的场景的,好像需要考虑云构建、缓存、并发等要素,估计我也没有到这种公司和项目中来。而且 pnpm 也可以集成 nx 来做构建,需要的话再来做笔记

changeset 版本管理

pnpm 的 publish 命令只能发布单个 npm 包,并不像 lerna 自带交互集成式的 monorepo 的 publish 的功能,所以需要补全一部分发布的工作流:bump version + changelog。

pnpm 官方推荐了 changesets 和 微软的 rush 两种工具用来管理版本,在 npm trends 上,rush 比 changesets 使用的更多,但是 rush 比较复杂,我的个人项目,选择使用 changesets 即可,虽然在工作流上比 lerna 多了一步添加 changeset 的过程,但是可以更加灵活的定制发布的工作流倒也不错

🎉最终步骤(基于pnpm 7.18.2 版本): 参考 Using Changesets with pnpm

  1. pnpm run build🛠本地开发了一个功能,执行自定义的 build 脚本,或者后续选择对接 github actions 之类的云端构建,那么这一步本地构建就可以省略
  2. pnpm changeset 记录变更集
  3. pnpm changeset version 提升版本,生成 changelog
  4. pnpm install 会更新 lockfile 以及 rebuild packages
  5. git commit xxx
  6. pnpm publish -r 发布 workspace 中所有出现版本变更的包

初始化

安装依赖

pnpm add @changesets/cli -w -D

changeset 初始化配置

pnpm changeset init

会在根目录新建 .changeset 目录用来管理,其中的 config.json 是 changeset 的配置文件

├── .changeset
│   ├── README.md
│   └── config.json

changeset config.json 配置说明

  • changelog: changelog 生成方式
  • commit: publish 时 changeset 是否先执行 git add 操作
  • linked: 配置需要共享版本的包
  • access: 内网使用 restricted ,开源使用 public
  • baseBranch: 项目主分支
  • updateInternalDependencies: 确保某包依赖的包发生 upgrade,该包也要发生 version upgrade 的衡量单位(量级)
    ignore: 不需要变动 version 的包
  • ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: 在每次 version 变动时一定无理由 patch 抬升依赖他的那些包的版本,防止陷入 major 优先的未更新问题

执行 pnpm changeset

执行 pnpm changeset 会将 packages 的包全部列出来,选择需要发布的包即可,选择时使用空格键选择。这一步是收集版本发布升级的信息的,并不是真正的版本升级。

需要发布的包版本类型可以使用 semver 规范,有 patch, minor, major ,这一步并不会修改到子 package 的 version ,而是把版本提升的信息记录到 .changeset 文件夹中的 md 文件中。ps: changset 同样提供了预发布 prerelease 的模式,常见的如 alpha/beta/rc 等,具体使用查看文档

每次 pnpm changeset,都会在 .changeset 文件夹生成一个随机名称的 .md 文件用以描述版本和提交信息,这些 md 文件,都会在 pnpm changeset version 作为素材被添加到 changelog.md 文件中然后被 changeset 移除🤩

执行 pnpm changeset version

这一步是升级版本,会使用 .changeset 目录中刚才生成的 .md 文件内容,修改到子 package 的 package.json 的 version 字段,即对版本进行了提升,并且在发布的包的目录生成一个 changelog.md 文件,然后将上述 .changeset 文件夹中的这些记录版本信息的 md 文件都删除掉

执行 pnpm changeset publish

将上一步的版本变更发布到 npm 上

最终 package.json 配置实例

顶层 package.json 配置

{
  "name": "hey-cli",
  "version": "0.0.1",
  "description": "CaptainJack 的专用脚手架",
  "scripts": {
    "start": "pnpm -r --parallel --filter='./packages/*' run start",
    "build": "pnpm -r --filter='./packages/*' run build",
    "release": "pnpm run build && pnpm publish -r"
  },
  "author": "CaptainJack",
  "license": "ISC",
  "devDependencies": {
    "@changesets/cli": "^2.25.2",
    "@types/node": "^18.11.15",
    "ts-node": "^10.9.1",
    "typescript": "^4.9.4"
  }
}

子 package.json 配置

{
  "name": "@hey-cli/app",
  "version": "1.0.2",
  "type": "module",
  "description": "hey-cli入口包",
  "main": "build/index.js",
  "module": "build/index.js",
  "scripts": {
    "start": "ts-node-esm src/index.ts",
    "tsc": "tsc"
  },
  "publishConfig": {
    "access": "public", // 当使用npm 组织形式发布时,默认会是 private ,此处需改成 public ,否则发布失败
  },
  "author": "CaptainJack",
  "license": "ISC",
  "dependencies": {
    "@hey-cli/log": "workspace:^0.0.1"
  }
}

参考文章

本文是 22 年学习 monorepo 的时候,沉淀的文章,当时使用 monorepo 开发了自己的一套命令行

Read more

AI 时代的一次远程面试经历

AI 时代的一次远程面试经历

今天聊了一个远程面试,记录一下自己的感受: 负责人也是技术出身,刚好负责两个团队,一个在美国做 AI Startup,还有一个在香港做软件开发,所以我们直接约了个群聊,相互介绍了团队和自己在当前节点的状态,以及对这个职位的预期 我对这两种业务模式都有兴趣,相当于是一次性参加了两个面试了: AI Startup 这种模式,我刚好在 23 年的时候,跟微软和字节的朋友一起做 AI 电商创业,当时从立项,研发逐步推进,MVP 构建,VC 融资,市场营销方面跟了下来,学习到了非常多的东西 而外包业务,我在很多年前就开始做一些副业,与甲方沟通需求,自己找 UI ,测试的成员组队,可能与多数人不同的是,我对外包项目的接受度很高(这里说的不是传统的如中软,东软那样的外包团队),而是创业型的外包公司,这种环境下,是真的可以在技术、业务、商务、业务视角学到很多。 远程面试一般比较直接,大家都直奔主题,能够迅速感知到候选人和团队的契合程度,在

郑州互联网薪资统计及 AI 时代的现状

郑州互联网薪资统计及 AI 时代的现状

22 年郑州前端薪资真实统计(200+份样本) 22 年我在郑州前端大群里面统计了一波郑州互联网薪资,当时以为只是开始,想着后面会有更多的企业来到郑州,情况会越来越好,而现在回头来看,没想到那时才是顶峰。 当时在群里面收集到 200+ 位投票,匿名填写自己的薪资情况,当时跟大家特意强调了要保证数据的真实性,也找群员验证过,基本真实性可信: * 🐣 3-6k 占比不少,主要是郑州的工资水平低,对于刚出来的,在小公司的这个薪资也正常,实习生 or 小作坊 * 🐰 6-12k 的占大多数,涵盖了郑州本土大多数互联网公司的薪资范畴,大多数的常规业务开发 * 🐵 12-20k 主要集中在本土大公司、一线城市在郑州的研发团队、外包公司,核心业务开发人员 🦄 20k+ 高学历 or 有大厂经验,主要为本土大公司或者一线公司在郑州的带队 leader ,能力较强,一般承担一些管理角色。 25年:职场寒冬来袭,状况还在持续恶化 一线公司研发部门的撤离 这也是我最在意的一条: 之前有一些一线城市的研发交付部门会在郑州有成立研发交付中心,从疫情之后,

RAG不是万能的,附常见误解与澄清

RAG不是万能的,附常见误解与澄清

能给人理清目前 AI 在生产落地的问题,是一件难能可贵的事情,AI 在生产落地会有哪些阻碍的讲解,讲干货的真的是很少,至少我自己曾经在这个问题上困惑了很久。 因为之前我在 AI 电商团队,做具体的 AI Sass 落地的时候,团队经常会沟通做到生产级可用的 AI,需要哪些东西,大概的门槛还是了解到一些的,我的能力也顶多在应用层面去做一些工作量的定制与代码衔接,涉及到模型层面,一概歇菜,很多人估计只是想着,写一个 Prompt ,就是真正的拥抱 AI 了。 当我看到自媒体里面铺天盖地的在讲 AI 如何帮助企业提效,如果重塑行业的时候,有种很复杂的感觉,他们真的懂 AI 吗,甚至他们真的懂软件吗? 基本上现在大家接触到的方式就几种: * 割韭菜卖课,讲概念,前景,这些基本上都是拿别人的产品来给自己做嫁衣,自己顶多是个工具的使用者和营销者,比如用 Midjourney,Kimi,豆包,海螺什么的,告诉大家用了就能提效,获得流量,

Shopify 构建商城页面的几种方式

Shopify 构建商城页面的几种方式

当用户需要对 Shopify 商城页面有自定义的需求时,一般会选择: 1、直接使用官方商城的 theme 来构建,可以使用,但是免费模板较少,只有 13 个。 2、使用第三方 theme 来构建,也有很多的模板可以选择,比如 envato 上有数百个 shopify 的模板,这是个非常大的市场。 3、完全自定义开发,自由度最高 当用户对 Shopify 商城页面,有完全自定义的需求,通常是常规主题无法满足需求,推荐使用 Shopify Hydrogen 的官方方案,开发人员可以对商城进行完全的页面级别的自定义。 而对于开发人员来说,官方的 Shopify Liquid 开发起来的效率和体验,是不如 Shopify Hydrogen 来的舒服,而且使用 Shopify Hydrogen ,还可以对性能有提升,