mcpField
T
ts-sdk-author

ts-sdk-author

github.com/mindfold-ai开发15.0k87 次安装4 周前更新
代码仓库

关于 ts-sdk-author

ts-sdk-author 是面向 TypeScript 开发者的工作流技能,用于在 pnpm monorepo 中构建独立 npm SDK。内容涵盖包设计、构建、验证和发布,包括预发布渠道。

主要功能

  1. 01规划 pnpm 工作区与包布局
  2. 02设计公共 API 和 TypeScript 类型
  3. 03配置构建与包导出
  4. 04设置 Turborepo 流水线
  5. 05发布前验证软件包
  6. 06管理预发布与稳定版本

适用场景

  1. 01从 CLI 或应用中提取 SDK
  2. 02设计供外部使用者调用的库 API
  3. 03配置 ESM 与 CJS 双格式分发
  4. 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.md
package-json-exports.md
tsdown-bundling.md
turborepo-for-sdk.md
type-design-for-public-api.md
verification-and-publishing.md
workspace-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 包。

核心步骤:

  1. 在工作区根目录采用 apps/ + packages/ + 可选的 tools/ 结构
  2. 将 SDK 放在 packages/<sdk-name>/(或 packages/core/)中
  3. 为其指定带作用域的名称(@<org>/<sdk-name>)
  4. 使用 workspace:* 协议连接工作区内部依赖
  5. 在开始其他工作之前就作出决定:此包最终会发布,因此应以此为前提设计边界和命名
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 — §2 src/ 边界、§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 错误都出在这里。五项不变规则:

  1. types 必须在每个 import / require 分支中排在最前面
  2. default 必须排在最后
  3. 同时支持 ESM 和 CJS 时,需要分别提供 .d.mts 和 .d.cts(TS 5.0+)
  4. 始终包含 "./package.json": "./package.json"(方便 publint/attw 进行内省)
  5. 如果同时使用 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.jsonname、version、type、exports、files、sideEffects、bin、scripts.prepublishOnly运行 publint --strict
软件包 tsconfig.json编辑器 + tsc --noEmitstrict: 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.tsTS 会按错误的模块模式解析 .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 原文示例。

评论

支持 Markdown