cal.diy 平台库开发指南:@calcom/platform-libraries 的本地构建、Dev 调试与 NPM 发布工作流
本篇技术指南围绕 cal.diy 仓库中 packages/platform/libraries/README.md 所定义的开发工作流展开,讲解 @calcom/platform-libraries 这个 NPM 包的版本管理机制、本地构建(yarn local / yarn build:dev)、一键发布(yarn publish-npm)以及合并到主分支前的手动发布流程。读完本文,你将掌握如何在开发 API v2 时让平台库指向本地源码、如何把改动同步到 NPM 包、以及什么情况下必须触发一次新版本发布。
一、@calcom/platform-libraries 是什么
@calcom/platform-libraries 是 cal.diy 仓库中的一个独立 NPM 包,位于 packages/platform/libraries,它是平台 API(apps/api/v2)与 cal.diy 核心业务逻辑(packages/features、packages/lib、packages/trpc 等)之间的桥梁:API v2 并不直接 import 仓库内部模块,而是通过这个库获得经过打包的、可独立版本化的业务能力。
从 index.ts 的导出内容可以清晰看到它的职责范围:
- 预订(Bookings):
getBookingForReschedule、getAllUserBookings、getBookingInfo、handleCancelBooking、confirmBookingHandler等; - 日历与忙碌时间:
getBusyCalendarTimes、updateEvent、getConnectedDestinationCalendarsAndEnsureDefaultsInDb、cityTimezonesHandler; - 事件类型(Event Types):
dynamicEvent、getUsernameList、validateCustomEventName、parseBookingLimit、parseRecurringEvent; - 认证与安全:
symmetricEncrypt/symmetricDecrypt、verifyCodeUnAuthenticated、sendEmailVerificationByCode、checkAdminOrOwner; - OAuth 与仓储层:
OAuthService、CredentialRepository、ProfileRepository、SelectedCalendarRepository、BookingReferenceRepository; - 平台常量与枚举:
ENABLE_ASYNC_TASKER、MINUTES_TO_BOOK、SchedulingType、MembershipRole、WebhookTriggerEvents等。
需要特别注意的是,当前仓库为社区版(community edition),index.ts 中为部分已被移除的 EE 功能保留了 stub 导出(如 roundRobinManualReassignment、createApiKeyHandler、verifyPhoneNumber),它们要么是空操作,要么直接抛出 "not available in community edition" 错误,目的仅在于维持 API v2 的既有 import 不中断。这属于从源码结构可以推断的实现细节,社区版开发者不应依赖这些函数返回有效结果。
二、包结构与构建配置
2.1 package.json 的关键设计
packages/platform/libraries/package.json 中有几个值得注意的设计点:
- 版本号保持
0.0.0:本地开发时包的版本始终为占位符0.0.0,真实版本号只在发布流程中临时写入(见下文脚本分析); - 多入口 exports:除了默认入口
.(指向dist/index.js/dist/index.cjs/dist/index.d.ts),还按业务域暴露了 13 个子路径,如./schedules、./bookings、./event-types、./app-store、./slots、./emails、./conferencing、./repositories、./organizations、./private-links、./errors、./calendars、./tasker,每个子路径都同时提供import(ESM)、require(CJS)和types(.d.ts)三种产物; - peerDependencies:
react ^18 || ^19、react-dom、stripe、zod作为宿主依赖外置,避免打包时重复捆绑; - workspace 依赖:
@calcom/features、@calcom/i18n、@calcom/lib均为workspace:*,构建时会被打包进产物。
2.2 Vite 多入口构建
构建由 vite.config.js 驱动,采用 Vite Library Mode + SSR 构建(build.ssr: true、platform: "node"、target: "node18"),入口即上文提到的 14 个 ts 文件(index.ts 及各子域模块)。该配置的核心策略是"尽可能把业务代码打进产物,同时把运行时依赖外置":
rollupOptions.external列出了 100+ 个外部依赖,包括react、zod、dayjs、stripe、@prisma/client、next-i18next、@sentry/nextjs、各种日历/视频/CRM SDK 等,由宿主环境提供;commonjsOptions通过dynamicRequireRoot指向apps/web,用于处理如next-i18next.config.js这类运行时动态 require;resolve.alias将@calcom/lib、@calcom/trpc、@calcom/prisma/client等 workspace 路径映射到仓库内部真实位置,其中tslog被桥接到 apps/api/v2/src/lib/logger.bridge.ts,保证日志实现与 API v2 一致;vite-plugin-dts负责生成类型声明文件,@vitejs/plugin-react处理 JSX。
构建产物输出到 dist/,每个入口同时产出 .js(ESM)与 .cjs(CJS),这就是 package.json 中 exports 能同时服务 import 与 require 的原因。
三、本地开发工作流:让 API v2 指向本地库
README 定义了一套"第一次构建 / 后续重构建"的两步工作流,对应 package.json 中的三个脚本。
3.1 第一次改动:yarn local
在 packages/platform/libraries 目录下执行:
yarn local
该命令展开后等价于(见 package.json 中的 local 脚本):
node scripts/local.js && npx rimraf dist && yarn build:dev && cd ../../.. && yarn
它依次完成四件事:
- 运行 scripts/local.js:把
packages/platform/libraries/package.json的版本临时写为9.9.9,同时把 apps/api/v2/package.json 中@calcom/platform-libraries的依赖改写为npm:@calcom/platform-libraries@9.9.9,用npm:协议强制从 npm registry 安装(当前仓库中 API v2 的默认依赖是workspace:*,见 apps/api/v2/package.json,二者互斥切换); - 清空旧的
dist/产物(rimraf dist); - 执行
yarn build:dev重新打包(见下节); - 回到仓库根目录执行
yarn重新解析 workspace 链接,使 API v2 实际引用本地构建产物。
3.2 后续改动:yarn build:dev
如果你已经执行过一次 yarn local,那么之后每次修改平台库源码只需重新构建:
yarn build:dev
该命令等价于:
yarn vite build && sed -i'' -e 's/const CACHE = new lruCache\.LRUCache({ max: 1e3 });/const CACHE = new lruCache({ max: 1e3 });/g' ./dist/index.cjs
注意其中的 lru-cache 修复:Vite 打包会把 lru-cache 的调用编译成 new lruCache.LRUCache(...),而实际运行时 lru-cache 的导出形态不同,因此构建后用 sed 将 lruCache.LRUCache 替换回 lruCache,避免运行时 TypeError。这也是 package.json 中另有一个 watch-lru-fix 脚本存在的原因——在 watch 模式下无法自动做这一步,需要手动轮询修补。若在 watch 开发中遇到 lruCache 相关报错,可参考该脚本。
四、一键发布:yarn publish-npm
当本地开发完成、功能验证通过后,README 推荐使用一条命令完成全部发布动作:
yarn publish-npm
该命令等价于(见 package.json 中 publish-npm 脚本):
yarn && node scripts/prepublish.js && npx rimraf dist && yarn build && npm publish --access public && node scripts/postpublish.js
4.1 prepublish.js:自动递增版本
scripts/prepublish.js 的逻辑如下:
- 通过
https.get("https://registry.npmjs.org/@calcom/platform-libraries")读取 npm registry 的dist-tags.latest,拿到当前线上最新版本; - 调用
incrementPatchVersion对 patch 位 +1(a.b.c -> a.b.c+1); - 把新版本号写入
packages/platform/libraries/package.json。
也就是说,每次 publish-npm 都只会递增 patch 版本,不涉及 minor / major。如果你需要 minor 或 major 跳版,需要走下一节描述的手动发布流程。
4.2 构建与发布
版本号写入后,脚本清空 dist/,执行 yarn build(即 yarn vite build,注意这里不会执行 lru-cache 的 sed 修补),然后 npm publish --access public 以公开包形式发布到 npm。
4.3 postpublish.js:收尾与依赖回写
scripts/postpublish.js 负责发布后的收尾工作:
- 读取刚写入
package.json的版本号作为publishedVersion; - 调用
waitForNewestNpmRelease轮询 npm registry(每 5 秒一次,最多 12 次),确认线上已能查到刚发布的版本; - 将
packages/platform/libraries/package.json的版本重置回0.0.0(与 README 中"reset 回 0.0.0"的说明一致); - 把 apps/api/v2/package.json 中
@calcom/platform-libraries依赖改写为npm:@calcom/platform-libraries@<publishedVersion>,即从"本地构建"切换回"线上 npm 包"; - 在仓库根目录执行
yarn install重新安装依赖,使 API v2 使用刚发布的 npm 版本。
至此,一次完整的"本地开发 → 构建 → 发布 → API v2 回切 npm 依赖"闭环完成。
五、合并到 main 之前:手动发布流程
README 明确要求:在把改动合并到 main 之前,必须先发布你的平台库版本。这是因为 CI / 其他协作者拉取 main 后,API v2 依赖的是 npm 上的真实包版本,而不是你本地未发布的 0.0.0 构建。
手动发布需要满足三个前提,缺一不可:
- 具备发布权限:你必须是
@calcom/platform-libraries这个 npm 包的贡献者(contributor); - CLI 认证:先在本地通过
npm auth完成 npm CLI 登录认证; - 合理递增版本号:按照语义化版本规范手动设置新版本(例如需要 minor / major 改动时,不能只依赖
publish-npm的 patch 自动递增)。
手动发布的步骤为:
# 1. 在 packages/platform/libraries 下按需修改版本号
# 例如将 version 从 0.0.0 改为 0.1.0
# 2. 发布到 npm
npm publish
# 3. 发布成功后,把 packages/platform/libraries/package.json 的 version 改回 0.0.0
# 4. 在仓库根目录重新安装依赖
yarn
# 5. 此时 API v2 应使用 npm 包而非本地构建版本
最后一步"运行 yarn"很关键:README 提醒执行完后"你应该正在使用 npm 包而不是本地构建的版本"——这与 postpublish.js 中"更新 API v2 依赖为 npm:@calcom/platform-libraries@<version> 并执行 yarn install"的行为一致,只是手动流程需要你自己完成依赖回写与重装。
版本判断的实操提示
在执行手动发布前,建议先确认 npm 上的 latest 版本,再决定递增策略:
npm view @calcom/platform-libraries version
如果你的改动只是新增导出或修 bug,递增 patch 即可;如果存在破坏性变更(函数签名变化、删除导出、依赖行为变化),应遵循 semver 规则考虑 minor / major。
六、什么时候必须发布新版本
README 定义了三条触发发布新版本的标准,它们对应三类"API 表面变化":
index.js(即 index.ts)新增导出:任何新函数、新类型通过平台库暴露给 API v2 时,必须发版,否则 API v2 无法引用到新能力;- 已导出函数发生代码变更:修改了某个已导出函数的实现逻辑(例如修复 bug、调整行为),必须发版才能让线上 API v2 获得修复;
- Prisma schema 变更破坏了当前已发布版本中函数的实现:例如 packages/prisma/schema.prisma 中模型字段调整,导致旧版本库中依赖旧数据形状的函数(如
getAllUserBookings、getBookingForReschedule、各 Repository 的查询)在新 schema 下编译或运行失败时,必须同步发布兼容的新版本。
从 CHANGELOG.md 的历史记录也能印证这一规律:例如 0.0.51 是为支持"预订时由参会者指定地点"(attendee specified location)发布的;0.0.41 是为"handleCancelBooking 向 webhook 传递 oauth client id"发布的;0.0.38 则为 API 新增了 bookerLayouts、color、confirmationPolicy、seats 等事件类型属性的内部/API 双向转换器(translator)。可以看到,几乎所有版本发布都对应一次"API v2 需要新导出或行为修复"的变更。
七、实践建议与常见问题
7.1 开发循环速查
| 场景 | 命令 | 说明 |
|---|---|---|
| 首次改动平台库源码 | yarn local |
构建 + 让 API v2 指向本地库 |
| 后续改动重新构建 | yarn build:dev |
重新打包并执行 lru-cache 修补 |
| 一键发布新版本 | yarn publish-npm |
自动递增 patch、发布、回写依赖 |
| 手动发布(需要 minor/major 或非 contributor 自动流程) | npm publish 等 |
见第五节 |
| watch 模式下的 lru-cache 修补 | yarn watch-lru-fix |
轮询修补 dist 产物 |
7.2 常见问题
- 改了平台库源码但 API v2 表现没变:先确认是否执行过
yarn build:dev;再检查 apps/api/v2/package.json 中@calcom/platform-libraries的依赖是workspace:*(本地模式)还是npm:@calcom/platform-libraries@x.y.z(npm 模式); - 发布后 API v2 仍引用旧版本:检查
postpublish.js是否成功执行(它会更新依赖并运行yarn install),或手动执行yarn重新安装; - 需要 patch 之外的版本跳跃:
publish-npm固定只递增 patch,请改用第五节的手动流程; - watch 模式遇到
lruCache.LRUCache is not a constructor之类的报错:运行yarn watch-lru-fix修补产物,或改用yarn build:dev的完整构建流程。
八、小结
@calcom/platform-libraries 的版本管理策略可以概括为"本地永远 0.0.0,发布时临时写真实版本,发布后回写 npm 依赖"。这套工作流通过 scripts/local.js、scripts/prepublish.js、scripts/postpublish.js 三个脚本将"本地开发 → dev 构建 → npm 发布 → API v2 回切线上依赖"完全自动化,开发者只需牢记三条规则:
- 首次改动用
yarn local,后续改动用yarn build:dev; - 合并 main 之前必须先把你的版本发布到 npm,并把本地版本号重置回
0.0.0、重新yarn; - 新增导出、修改已导出函数、Prisma schema 破坏性变更,三者任一发生都要发布新版本。
理解这套机制后,你在 cal.diy 中为 API v2 贡献平台层能力时,就能始终保证"本地调试不污染线上、线上依赖永远指向已发布的稳定版本"。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00