关于 ts-sdk-author
ts-sdk-author 是面向 TypeScript 开发者的工作流技能,用于在 pnpm monorepo 中构建独立 npm SDK。内容涵盖包设计、构建、验证和发布,包括预发布渠道。
主要功能
- 01规划 pnpm 工作区与包布局
- 02设计公共 API 和 TypeScript 类型
- 03配置构建与包导出
- 04设置 Turborepo 流水线
- 05发布前验证软件包
- 06管理预发布与稳定版本
适用场景
- 01从 CLI 或应用中提取 SDK
- 02设计供外部使用者调用的库 API
- 03配置 ESM 与 CJS 双格式分发
- 04准备 beta、RC 和稳定版本发布
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mindfold-ai/ trellis --skill ts-sdk-author安装后在 Claude Code 中输入/ts-sdk-author确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/module-boundaries-and-plugins.mdpackage-json-exports.mdtsdown-bundling.mdturborepo-for-sdk.mdtype-design-for-public-api.mdverification-and-publishing.mdworkspace-and-layout.md详细介绍
TypeScript SDK 编写者
在 pnpm monorepo 中,从内部将 TypeScript SDK 作为独立 npm 包发布的端到端工作流:工作区布局、公共 API 设计、构建配置、分发结构、monorepo 流水线、验证,以及完整的发布生命周期,包括 beta / rc / canary 通道。
七篇参考文档涵盖了深入内容。本文是统一工作流,也是日常常用模式的速查表。
何时使用此技能
- 从现有 CLI 或应用中提取核心库(例如
packages/core、packages/sdk),并将其放入 pnpm 工作区 - 为 TypeScript 库设计公共 API,让陌生用户也能使用——品牌类型、泛型客户端、插件扩展点
- 选择构建工具——tsdown、tsup、仅使用 tsc 或 unbuild
- 编写
package.json的exports字段,支持 ESM+CJS 双模块格式、同构运行时条件和子路径插件入口 - 配置 Turborepo,使 SDK 仅在输入变化时重新构建,并让下游应用使用 SDK 的构建输出(或通过自定义条件直接使用原始
src) - 将
publint --strict和attw --pack接入prepublishOnly或 CI - 管理预发布通道——每次提交发布
canary,即将推出的主版本使用next,稳定阶段使用beta/rc,稳定版使用latest——以及它们之间的转换(beta.N→rc.0→1.0.0→1.1.0-beta.0) - 使用 GitHub Actions
changesets/action@v1和 npm provenance 配置 changesets
执行工作流
单个 TS SDK 的构建流程分为以下七个阶段。跳过任何阶段,都可能导致后续出现问题——各阶段之间的依赖关系确实存在。
阶段 1 — 工作区与包骨架
搭建 monorepo,并创建空的 SDK 包。
核心步骤:
- 在工作区根目录采用
apps/+packages/+ 可选的tools/结构 - 将 SDK 放在
packages/<sdk-name>/(或packages/core/)中 - 为其指定带作用域的名称(
@<org>/<sdk-name>) - 使用
workspace:*协议连接工作区内部依赖 - 在开始其他工作之前就作出决定:此包最终会发布,因此应以此为前提设计边界和命名
my-repo/
├── pnpm-workspace.yaml
├── package.json # root: only devDeps + workspace scripts
├── apps/
│ └── example-app/ # consumer of the SDK
└── packages/
├── sdk/ # ← the SDK
└── shared-tsconfig/ # internal-only, never published
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
接下来阅读: references/workspace-and-layout.md — §2 布局、§3 SDK 命名模式、§4 创建内部包、§5 package.json 骨架、§7 从多仓库迁移到 monorepo。
阶段 2 — 公共 API
编写任何代码之前,先决定 SDK 的公共接口是什么样的。
两个并行考虑事项:
A. 模块边界。 src/ 目录树应清晰地划分为 api/(重新导出并属于契约的内容)和 internal/(禁止从包外导入)。package.json 的 exports 字段是成本最低的约束机制——未在其中列出的内容,消费者无法导入。
packages/sdk/src/
├── index.ts # barrel — re-exports from api/
├── api/
│ ├── client.ts
│ └── types.ts
└── internal/
├── transport.ts # NOT exported
└── state.ts # NOT exported
B. 类型设计。 SDK 类型面向陌生用户,不能泄漏内部实现,并且必须便于演进。请使用:
- 品牌类型表示不透明 ID:
type UserId = Brand<string, "UserId"> - 泛型客户端并提供合理的默认值,以便日后添加类型参数时不造成破坏性变更:
createClient<Schema = DefaultSchema>(...) - 判别联合表示结果类型:
Result<T, E>,其结构为{ ok: true; value: T } | { ok: false; error: E } - 当选项组合很重要时,使用构建器模式实现类型安全的配置
- 如果用户可能需要通过声明合并来扩展类型,请使用接口(而非类型别名)
接下来阅读:
references/module-boundaries-and-plugins.md— §2src/边界、§3 运行时分层、§4 提供方/适配器、§5 插件扩展、§6 边界约束、§7 模式与反模式references/type-design-for-public-api.md— §1 品牌类型、§2 泛型接口、§3 条件类型/映射类型、§4 类型守卫、§5 构建器、§6 工具类型的发布与内部使用、§7 库的 tsconfig、§8 API 演进
阶段 3 — 构建配置
你需要:(a)一个针对库输出进行调优的 tsconfig.json,以及(b)一个能够生成实际 dist/ 的打包工具。
库的 tsconfig — 关键标志:
// tsconfig.build.json — library build config
{
"compilerOptions": {
"target": "es2022",
"module": "nodenext",
"moduleResolution": "nodenext",
"strict": true,
"declaration": true, // emit .d.ts
"declarationMap": true, // sourcemap from .d.ts → .ts
"sourceMap": true,
"verbatimModuleSyntax": true, // TS 5.0+ — strict import elision
"isolatedDeclarations": true, // TS 5.5+ — explicit return types on public API
"composite": true, // enable project references
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
2026 年的打包工具选择:tsdown。tRPC 和 Inngest 已从 tsup 迁移到它;tsup 自己的 README 现在也写道:"This project is not actively maintained anymore. Please consider using tsdown instead."
最低可用的 tsdown.config.ts:
import { defineConfig } from "tsdown";
export default defineConfig({
entry: ["src/index.ts", "src/plugin/index.ts", "src/testing/index.ts"],
format: ["esm", "cjs"],
dts: true,
sourcemap: true,
treeshake: true,
clean: true,
outExtensions: ({ format }) => ({
js: format === "esm" ? ".mjs" : ".cjs",
dts: format === "esm" ? ".d.mts" : ".d.cts",
}),
});
替代方案:
tsc-only/zshy— 适用于没有运行时依赖的小型 SDK,可忠实于源代码发布unbuild— 仅适用于已经处于 UnJS 生态中的项目tsup— 社区熟悉度高,但关注度正在下降;惯性使用时仍可行
接下来阅读:
references/tsdown-bundling.md— §3 结论、§4 可用配置、§6–§8 替代方案、§11 选择决策树references/type-design-for-public-api.md§7 — 完整的库 tsconfig 讲解
阶段 4 — 分发结构(package.json exports)
大多数 TS SDK 错误都出在这里。五项不变规则:
types必须在每个import/require分支中排在最前面default必须排在最后- 同时支持 ESM 和 CJS 时,需要分别提供
.d.mts和.d.cts(TS 5.0+) - 始终包含
"./package.json": "./package.json"(方便 publint/attw 进行内省) - 如果同时使用
module和require,则module必须在require之前
规范的双格式结构(原样摘自 @trpc/server):
{
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
}
}
为插件入口添加子路径,以便它们可以独立于根导出进行版本管理:
{
"exports": {
".": { "import": { ... }, "require": { ... } },
"./plugin": { "import": { ... }, "require": { ... } },
"./testing": { "import": { ... }, "require": { ... } }
}
}
对于同构 SDK(浏览器 / workers / RN / edge),运行时条件应排在 import / require 之前:
{
".": {
"browser": { "import": "./dist/browser.mjs" },
"workerd": { "import": "./dist/workerd.mjs" },
"react-native": { "import": "./dist/rn.mjs" },
"deno": { "import": "./dist/deno.mjs" },
"import": { "types": "./dist/index.d.mts", "default": "./dist/index.mjs" },
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
}
}
接下来阅读:references/package-json-exports.md — §3 五项规则、§4 附注说明的 tRPC 双格式模式、§5 仅 ESM 模式、§6 子路径插件、§7 同构条件(Sanity 客户端模式)、§9 常见错误:错误 → 修正 → 原因。
阶段 5 — Monorepo 流水线(Turborepo)
SDK 可以独立构建后,将它接入工作区,从而实现:
- 仅当 SDK 输出变化时,应用才会重新构建(缓存)
- 本地开发时,SDK 在监视模式下重新构建,同时应用重新加载
- CI 在 PR 上只构建受影响的包
最低可用的 turbo.json:
{
"$schema": "https://turborepo.com/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig*.json", "tsdown.config.ts", "package.json"],
"outputs": ["dist/**"]
},
"test": { "dependsOn": ["^build"], "inputs": ["src/**", "test/**"] },
"lint": { "inputs": ["src/**"] },
"typecheck": { "dependsOn": ["^build"], "inputs": ["src/**", "tsconfig*.json"] },
"dev": { "persistent": true, "cache": false }
}
}
日常使用的 --filter 模式:
pnpm turbo run build --filter=@acme/sdk # SDK alone
pnpm turbo run dev --filter=@acme/sdk... --filter=@acme/example-app
pnpm turbo run test --filter=...@acme/sdk # affected-by-SDK
pnpm turbo run lint --filter=[HEAD^1] # affected since last commit
关键规则:将脚本放在每个包自己的 package.json 中,而不是放在根目录。根目录只负责委托执行 turbo run X。
接下来阅读:references/turborepo-for-sdk.md — §2 最低可用的 turbo.json、§3 按包配置与根目录配置、§4 dependsOn、§5 缓存输入/输出、§6 --filter 模式、§7 boundaries 字段、§8 CI 模式、§9 使用 watch 的开发模式。
阶段 6 — 验证
发布前,两个静态检查和一个运行时检查缺一不可:
# After pnpm build:
pnpm exec publint --strict # static lint of package.json
pnpm exec attw --pack . # simulate Node/Bun/Deno/bundler resolution
# Then pack + install in a sandbox dir
pnpm pack
cd /tmp/sandbox && npm init -y && npm install /path/to/your-pkg-1.0.0.tgz
node -e "console.log(require('@acme/sdk'))" # CJS reaches
node --input-type=module -e "import('@acme/sdk').then(console.log)" # ESM reaches
将这三项全部接入 prepublishOnly:
{
"scripts": {
"prepublishOnly": "pnpm build && pnpm exec publint --strict && pnpm exec attw --pack ."
}
}
**为什么 publint 和 attw 都要用?**publint 会静态检查 package.json 结构;attw 则会实际模拟每种使用方运行时如何解析你的 tarball。attw 最常见的失败是 伪装成 ESM — 一个包含 ESM 但通过 require 暴露的 .js 文件 — publint 无法发现这个问题。
接下来阅读:references/verification-and-publishing.md — §2 publint 规则与 3 个常见错误、§3 attw 解析模式表与 7 种失败模式、§4 冒烟测试(tarball → 全新目录)。
阶段 7 — 发布生命周期
大多数 SDK 项目都会在这一阶段积累技术债务。从第 1 天起就要做好。
Semver + 预发布标识符:
0.x.y # pre-1.0 — breaking changes allowed in minors
1.0.0-alpha.0 # internal feature spike
1.0.0-beta.0 # feature-complete, API may still shift
1.0.0-rc.0 # frozen, blocker-only fixes
1.0.0 # stable
1.0.1 # patch on stable
1.1.0-beta.0 # next minor's beta cycle while 1.0.x ships patches
npm dist-tag — 绝不要将预发布版本发布到 latest:
# Publish a beta under the `beta` tag (NOT `latest`)
npm publish --tag beta
# Recover from a mistaken latest:
npm dist-tag add @acme/[email protected] latest # repoint latest to stable
npm dist-tag rm @acme/sdk beta # if no longer needed
约定标签:latest(稳定版)、next(即将推出的主版本预发布版)、
beta、rc、canary(每次提交)、alpha、experimental、nightly。
changesets 预发布模式 — 标准转换流程:
# Cut beta line
pnpm changeset pre enter beta
pnpm changeset # write a changeset
pnpm changeset version # bumps to 1.0.0-beta.0
pnpm changeset publish
# Feature-complete; move beta → rc
pnpm changeset pre exit
pnpm changeset pre enter rc
pnpm changeset version # bumps to 1.0.0-rc.0
pnpm changeset publish
# RC stable; ship 1.0.0
pnpm changeset pre exit
pnpm changeset version # bumps to 1.0.0
pnpm changeset publish
# Open next minor's beta line
pnpm changeset pre enter beta
pnpm changeset version # bumps to 1.1.0-beta.0
npm provenance — 启用它:
{
"publishConfig": {
"access": "public",
"provenance": true
}
}
在 GitHub Actions 发布作业中配合使用 id-token: write 权限;
npm 会在软件包页面上显示已验证的证明。
接下来阅读: references/verification-and-publishing.md — §5 semver
复习、§6 dist-tag 规则、§7 完整生命周期状态图、§8 案例研究(Next.js / vercel-ai / tRPC / Storybook / Stripe 的实际版本序列)、§9 changesets 预发布流程、§10 GitHub Actions 发布
工作流、§11 provenance、§12 撤回与弃用、§13 策略决策树。
快速参考
文件 / 字段速查表
| 文件 | 负责内容 | 快速检查 |
|---|---|---|
pnpm-workspace.yaml | 哪些目录是软件包 | apps/* + packages/* |
根目录 package.json | 工作区 devDeps + turbo run 委托 | 根目录中没有软件包级构建脚本 |
软件包 package.json | name、version、type、exports、files、sideEffects、bin、scripts.prepublishOnly | 运行 publint --strict |
软件包 tsconfig.json | 编辑器 + tsc --noEmit | strict: true + declaration: true |
tsconfig.build.json | 库构建配置 | 如果需要快速生成 .d.ts,则设置 isolatedDeclarations: true |
tsdown.config.ts | 打包 | format: ['esm', 'cjs'] + 双格式 outExtensions |
turbo.json | 任务流水线 | 使用 dependsOn: ['^build'] 确保编译顺序 |
.changeset/config.json | 发布策略 | commit: false、access: public |
打包工具选择速览
| 情况 | 选择 |
|---|---|
| 现代 TS SDK、双格式 ESM+CJS、插件子路径 | tsdown |
| 零运行时依赖,希望发布与原始源码一致的内容 | 仅用 tsc / zshy |
| 正在使用且运行良好的现有 tsup 项目 | 继续使用 tsup;规划迁移到 tsdown |
| UnJS / Nuxt 生态 | unbuild |
| 需要包拆分 + 高级 rollup 配置 | 直接使用 rolldown |
模块格式决策
| 情况 | 建议 |
|---|---|
| 2026 年新建 SDK 的默认选择 | 双格式 ESM + CJS |
| 库拥有稳定的使用者群体,且要求 Node 22 或更高版本 | 仅 ESM 是合理选择 |
| 库仅在 Node 应用内部使用 | 仅 ESM |
| 库由 Jest、较旧版 Next.js、Lambda CJS 使用 | 必须使用双格式 |
发布标签速览
| 标签 | 含义 | npm install pkg@? 的解析结果 |
|---|---|---|
latest | 当前稳定版 | npm install pkg |
next | 即将推出的主版本预发布版 | npm install pkg@next |
beta | 功能完整的稳定化版本 | npm install pkg@beta |
rc | 已冻结,仅修复阻塞问题 | npm install pkg@rc |
canary | 每次提交 / 每个 PR 的快照 | npm install pkg@canary |
experimental | 不稳定的试验版本 | npm install pkg@experimental |
常用单行代码片段
# Add the SDK as a workspace-internal dep
pnpm add @acme/sdk@workspace:* --filter @acme/example-app
# Build SDK + everything that depends on it
pnpm turbo run build --filter=...@acme/sdk
# Pre-publish gate
pnpm build && pnpm exec publint --strict && pnpm exec attw --pack .
# Cut a snapshot release for a PR (vercel/ai pattern)
pnpm changeset version --snapshot pr-123
pnpm publish --tag pr-123 --no-git-checks
发布前检查清单
每次发布前都要逐项检查。跳过任何一项都可能导致有问题的 SDK 被发布。
构建产物
-
pnpm build会在dist/中生成.mjs和.cjs(双格式时),或仅生成.mjs(仅 ESM 时) - 双格式时存在
.d.mts和.d.cts,或者仅 ESM 时仅有.d.ts - 已生成源映射(
.mjs.map、.d.mts.map) -
dist/大小合理(du -sh dist/— 检查是否正常,确认没有意外膨胀)
package.json
-
name使用作用域名称(@org/name)——如果以后可能设为私有包,则为必需 -
version与即将发布的版本一致 -
type与默认格式一致(默认使用 ESM 时设为"module",默认使用 CJS 时省略) -
exports包含"./package.json": "./package.json" - 每个
exports分支都将types放在首位,将default放在末尾 -
files列出dist(如果要随包发布源码以支持 IDE 跳转到定义,也需列出src) -
sideEffects: false(除非确实存在顶层副作用) - 首次发布作用域包时设置
publishConfig.access: "public" -
publishConfig.provenance: true
验证
-
publint --strict检查通过 -
attw --pack .检查通过(或仅有预期的node10警告) - 冒烟测试:打包 + 在
/tmp中安装 + CJS + ESM + TS 消费者均能正常解析
发布
- 选择正确的 dist-tag(仅稳定版本使用
latest) - 如果是预发布版本:在运行
version之前已运行pnpm changeset pre enter <tag> - 如果是稳定版本:此前处于预发布模式时,已运行
pnpm changeset pre exit - CHANGELOG.md 已反映此次变更
- Git 标签与版本一致(例如
v1.0.0-beta.3) - npm 包页面上可查看来源证明
常见错误
| 错误 | 后果 | 修复方法 |
|---|---|---|
exports 分支中的 types 未放在首位 | TS 将 .js 识别为类型来源 → 消费者端出现一连串错误 | 将 types 移到每个 import / require 分支的顶部(publint 会发出警告) |
ESM+CJS 共用单个 .d.ts | TS 会按错误的模块模式解析 .d.ts | 生成 .d.mts + .d.cts(TS 5.0+);tsdown 会自动处理 |
exports 中缺少 "./package.json": "./package.json" | publint/attw 无法检查你的包 | 始终将其包含在内 |
将预发布版本发布到 latest | 每个运行 npm install pkg 的用户都会安装你的测试版 | 使用 npm publish --tag beta;通过 npm dist-tag add pkg@stable latest 恢复 |
稳定版发布前忘记运行 pnpm changeset pre exit | 稳定版本会变成 1.0.0-beta.N,而不是 1.0.0 | 最终发布前始终运行 pre exit |
根目录 package.json 包含实际构建脚本 | 无法发挥 Turborepo 的并行执行和缓存优势 | 在各个包中定义脚本;根目录仅通过 turbo run 委派 |
消费者从 dist/internal/... 深层导入 | 消费者与内部实现耦合;重构会导致其代码失效 | 不要在 exports 中列出内部模块;使用 ESLint no-restricted-imports |
| 将内部类型泄漏到公共 API 接口 | 用户会看到不应依赖的类型 | 仅从 src/api/*.ts 重新导出;不要对内部模块使用 export * |
无副作用时仍缺少 sideEffects: false | 打包工具无法对你的库进行摇树优化 | 添加 "sideEffects": false,或列出实际产生副作用的文件 |
忘记为来源证明设置 id-token: write 权限 | 来源证明在 CI 中静默失败 | 在发布作业中添加 permissions: { id-token: write, contents: read } |
| 在同一个 Turbo 任务中混用 watch 和 build | 缓存不断失效;watch 无法稳定运行 | 将 build(可缓存)与 dev(persistent: true, cache: false)分开 |
exports 同时包含 module 和顺序错误的无关运行时条件 | Edge 运行时会选错文件 | 运行时条件(browser、workerd)→ module → import → require → default |
在公共 API 类型中使用 enum | 强制消费者使用仅支持 TS 的语法,并破坏可擦除语法 | 使用字符串字面量联合类型或 as const 对象 |
从 SDK 根入口使用 default 导出 | 破坏摇树优化和互操作性 | 始终使用命名导出 |
参考文件
| 文件 | 适用场景 |
|---|---|
references/workspace-and-layout.md | 设置 apps/ + packages/ + tools/;为 SDK 包命名;创建内部包;选择依赖字段(dependencies / peerDependencies / devDependencies);从多仓库迁移 |
references/module-boundaries-and-plugins.md | 将 src/ 拆分为 api/ 与 internal/;设计编排层、适配器与工具;构建带有生命周期钩子的插件扩展模型;通过 eslint-plugin-boundaries / dependency-cruiser / Turbo boundaries 强制执行边界 |
references/type-design-for-public-api.md | 品牌类型;泛型客户端;条件类型与映射类型;类型安全构建器;决定哪些实用类型应发布、哪些应保留为内部类型;库的 tsconfig 标志(verbatimModuleSyntax、isolatedDeclarations、composite);API 演进模式 |
references/package-json-exports.md | 编写 exports 字段;使用单独的 .d.mts/.d.cts 实现双 ESM+CJS;子路径插件入口;同构运行时条件(browser、workerd、react-native、deno、edge-light);修复常见的 exports 问题 |
references/tsdown-bundling.md | 选择 tsdown、tsup、仅使用 tsc 或 unbuild;最小可用的 tsdown.config.ts;子路径输出映射;副作用与 tree-shaking;监视与开发模式;打包器选择决策树 |
references/turborepo-for-sdk.md | 为 SDK 单仓库编写 turbo.json;dependsOn: ['^build'];缓存 inputs/outputs;SDK 开发的 --filter 模式;boundaries 字段;使用远程缓存和仅构建受影响项目的 CI;使用 persistent: true 的开发模式 |
references/verification-and-publishing.md | 配置 publint + attw;tarball 冒烟测试;semver 与预发布标识符;npm dist-tags;完整的 beta → rc → stable → next-cycle 状态机;changesets 预发布模式;标准的 beta → rc 转换命令序列;包含快照 PR 的 GitHub Actions 发布工作流;npm 来源证明;撤回与弃用;发布策略决策树 |
源技能
此技能由 oh-my-openclaw 中的四个源技能组合而成:
agent-cli-architecture(architect-claw)— 工作区结构、模块边界、运行时分层、插件扩展模式。将「Agent CLI」的表述泛化为通用的「SDK + 配套 CLI」。typescript-pro(frontend-claw)— 品牌类型、泛型、条件类型、类型守卫、实用类型、tsconfig 深入解析。调整为面向库与 SDK 作者的关注点。turborepo(frontend-claw)— 任务流水线、缓存、--filter、boundaries。大幅精简,仅保留与 SDK 单仓库相关的部分。monorepo-navigator(architect-claw)— pnpm 工作区、changesets、发布、迁移。
此外,还包括关于 package.json exports、2026 年双 ESM/CJS、tsdown 生态、publint + attw,以及 alpha→beta→rc→stable 生命周期的原创研究,并引用了 tRPC、vercel/ai、Inngest、Sanity client、Hono、Zustand 和 TanStack query-core 的 GitHub package.json 原文示例。