用 Claude Code 创建并发布 npm 包的实战指南
用 Claude Code 创建 npm 包,覆盖 tsup、exports、类型声明、npm pack 验证和 CI 发布流程。
让 Claude Code 生成一个 npm 包并不难,难的是让这个包真正适合发布和维护。初学者最容易漏掉的不是函数实现,而是 package.json 的入口、exports、类型声明、README、npm pack 里的文件、以及发布 CI。只要其中一个环节不清楚,包可能在本地能跑,却在用户项目里无法 import、无法 require,或者把不该发布的文件一起上传到 registry。
本文用一个 TypeScript 字符串工具包作为例子。Claude Code 的角色不是替你盲目发布,而是帮助你生成文件、检查配置、解释风险。npm 规则会更新,所以正式发布前请对照官方资料:npm 的 package.json 文档、scoped public packages、npm pack、trusted publishing 以及 Claude Code 官方说明。
先写清楚包的设计
不要一开始只说“帮我做一个 npm 包”。更好的提示词应该说明包名、使用者、运行环境、模块格式、测试方式和发布策略。type: "module"、main、exports、types 是一组联动配置,后补容易出错。让 Claude Code 先根据下表检查,再开始写文件。
| 项目 | 本文选择 | 需要 Claude Code 检查 |
|---|---|---|
| 包名 | @acme/string-kit | scoped public 包是否需要 --access public |
| 使用者 | Node.js 和 TypeScript 用户 | ESM import 与 CJS require 是否都能工作 |
| 构建 | tsup 输出 ESM、CJS、声明文件 | dist 中是否存在 exports 指向的文件 |
| 发布内容 | 只发布 dist、README.md、LICENSE | npm pack --dry-run 是否有意外文件 |
| 发布方式 | GitHub Actions + npm Trusted Publishing | 是否避免长期 npm token |
流程可以先画出来,避免 Claude Code 只生成代码却忘记打包和发布检查。
flowchart LR
A["设计说明"] --> B["package.json"]
B --> C["src/index.ts"]
C --> D["Vitest"]
D --> E["tsup build"]
E --> F["npm pack dry-run"]
F --> G["CI publish"]
如果你的目标是 CLI 包,可以继续阅读 Claude Code CLI 工具开发。日常提示词和验证习惯可以参考 Claude Code 效率技巧。
创建最小项目
先在干净目录中验证,不要直接塞进复杂 monorepo。小项目能让你更快分辨问题来自包配置、构建工具还是工作区本身。把下面命令作为 Claude Code 的完成条件之一,可以减少“看起来生成了很多文件,但无法运行”的情况。
mkdir string-kit
cd string-kit
npm init -y
npm install -D typescript tsup vitest @types/node
mkdir src scripts
package.json 是公开契约。main 面向 CJS,module 兼容部分旧 bundler,types 给 TypeScript,exports 控制现代入口。files 要收窄,否则测试数据、草稿、source map 或本地配置都可能被打包。
{
"name": "@acme/string-kit",
"version": "0.1.0",
"description": "Small TypeScript string utilities used as an npm package example.",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./package.json": "./package.json"
},
"files": ["dist", "README.md", "LICENSE"],
"sideEffects": false,
"scripts": {
"build": "tsup",
"test": "vitest run",
"docs": "node scripts/write-readme.mjs",
"test:pack": "npm pack --dry-run",
"prepublishOnly": "npm run test && npm run build && npm run test:pack"
},
"keywords": ["string", "typescript", "utilities"],
"license": "MIT",
"devDependencies": {
"@types/node": "^22.15.0",
"tsup": "^8.5.0",
"typescript": "^5.8.0",
"vitest": "^3.2.0"
}
}
TypeScript 只负责类型检查,实际输出交给 tsup。这样构建路径单一,Claude Code 也更容易判断 dist 是否与 exports 对齐。
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"declaration": true,
"declarationMap": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src", "tsup.config.ts"]
}
写可运行的实现和测试
文章里的代码必须能复制运行,而不是伪代码。下面的包包含四个小函数:slugify 生成英数字 slug,truncate 控制展示长度,interpolate 做简单模板替换,byteLength 返回 UTF-8 字节数。功能不大,但足够覆盖 ESM、CJS、类型、测试和打包。
export function slugify(input: string): string {
return input
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.trim()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
export function truncate(input: string, maxLength: number, suffix = "..."): string {
if (!Number.isInteger(maxLength) || maxLength < 1) {
throw new RangeError("maxLength must be a positive integer");
}
if (suffix.length >= maxLength) {
throw new RangeError("suffix must be shorter than maxLength");
}
if (input.length <= maxLength) return input;
return `${input.slice(0, maxLength - suffix.length)}${suffix}`;
}
export function interpolate(template: string, values: Record<string, string | number>): string {
return template.replace(/\{\{\s*([\w.-]+)\s*\}\}/g, (match, key: string) => {
return Object.hasOwn(values, key) ? String(values[key]) : match;
});
}
export function byteLength(input: string): number {
return new TextEncoder().encode(input).length;
}
测试要覆盖正常值、边界值、异常和 Unicode。向 Claude Code 提要求时,不要只写“加测试”,而是写清楚“重音字符、未知占位符、非法长度、UTF-8 字节数都要测试”。
import { describe, expect, it } from "vitest";
import { byteLength, interpolate, slugify, truncate } from "./index";
describe("slugify", () => {
it("turns a title into an npm-friendly slug", () => {
expect(slugify("Hello npm Package!")).toBe("hello-npm-package");
});
it("removes accents before replacing separators", () => {
expect(slugify("Crème brûlée utils")).toBe("creme-brulee-utils");
});
});
describe("truncate", () => {
it("keeps short text unchanged", () => {
expect(truncate("short", 10)).toBe("short");
});
it("adds a suffix inside the requested length", () => {
expect(truncate("Claude Code package", 12)).toBe("Claude Co...");
});
it("rejects invalid lengths", () => {
expect(() => truncate("abc", 2)).toThrow(RangeError);
});
});
describe("interpolate", () => {
it("replaces known placeholders and keeps unknown ones", () => {
expect(interpolate("Hi {{ name }}, ship {{pkg}} {{missing}}", {
name: "Masa",
pkg: "@acme/string-kit",
})).toBe("Hi Masa, ship @acme/string-kit {{missing}}");
});
});
describe("byteLength", () => {
it("counts UTF-8 bytes", () => {
expect(byteLength("npm")).toBe(3);
expect(byteLength("日本語")).toBe(9);
});
});
配置 tsup 和 README 生成
tsup 的配置保持简单。outExtension 把 ESM 输出成 .js,把 CJS 输出成 .cjs,这和 package.json 的入口一致。这里关闭 sourcemap,是为了避免发布包中意外带出内部源码映射;如果项目需要调试映射,要在发布前明确权衡。
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm", "cjs"],
dts: true,
clean: true,
sourcemap: false,
minify: false,
target: "es2022",
outDir: "dist",
outExtension({ format }) {
return { js: format === "esm" ? ".js" : ".cjs" };
},
});
README 很容易和代码不同步。用一个小脚本生成安装和使用示例,可以让 Claude Code 在 API 改动时同步更新文档,也能让 CI 在打包前重新生成 README。
import { writeFile } from "node:fs/promises";
const fence = String.fromCharCode(96).repeat(3);
const readme = `# @acme/string-kit
Small TypeScript string utilities packaged with tsup.
## Install
${fence}bash
npm install @acme/string-kit
${fence}
## Usage
${fence}ts
import { slugify, truncate } from "@acme/string-kit";
console.log(slugify("Hello npm Package!"));
console.log(truncate("Claude Code package", 12));
${fence}
`;
await writeFile(new URL("../README.md", import.meta.url), readme);
用 npm pack 验证发布物
不要把 npm publish 当成第一次真正验证。npm pack --dry-run 会展示即将进入包里的文件。你要检查 README、LICENSE、类型声明、CJS/ESM 输出是否都在,测试文件和草稿是否不在。
npm run docs
npm test
npm run build
npm pack --dry-run
node -e "import('./dist/index.js').then((m)=>console.log(m.slugify('Hello ESM')))"
node -e "const m=require('./dist/index.cjs'); console.log(m.slugify('Hello CJS'))"
更严格的做法是把生成的 .tgz 安装到另一个目录,模拟真实用户环境。这样可以发现只在本地源码旁边才会成功的配置错误。
npm pack
mkdir ../string-kit-smoke
cd ../string-kit-smoke
npm init -y
npm install ../string-kit/acme-string-kit-0.1.0.tgz
node -e "import('@acme/string-kit').then((m)=>console.log(m.truncate('Claude Code package', 12)))"
这个模式至少有三个实用场景。第一,产品团队可以把重复的字符串格式化逻辑从多个应用中抽出来。第二,内容运营可以用同一套 truncate 和 interpolate 生成 description、卡片标题和发布说明。第三,设计系统或 CLI 可以把小工具独立发布,让应用按 SemVer 升级,而不是等待大仓库整体发布。
用 GitHub Actions 发布
CI 发布比手动发布更可审计。npm Trusted Publishing 可以通过 OIDC 从 GitHub Actions 发布,减少长期 npm token 的使用。先在 npm 后台配置 trusted publisher,再让 release 事件触发发布。仍然使用 token 的团队,也应该确认 2FA、provenance 和权限范围。
name: package
on:
push:
branches: [main]
pull_request:
release:
types: [published]
permissions:
contents: read
id-token: write
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
cache: npm
- run: npm ci
- run: npm run docs
- run: npm test
- run: npm run build
- run: npm pack --dry-run
publish:
if: github.event_name == 'release'
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
cache: npm
- run: npm ci
- run: npm run docs
- run: npm test
- run: npm run build
- run: npm publish --access public
如果需要更细的版本说明,可以结合 Claude Code 与 Changesets 版本管理。它能让 Claude Code 更容易判断 patch、minor、major 的边界。
常见陷阱
第一个陷阱是只写 main,不写 exports。很多旧教程仍然这样做,但现代 TypeScript 和 bundler 更需要明确入口。第二个陷阱是发布内容过宽,导致测试数据、草稿、截图或 sourcemap 进入包。第三个陷阱是 README 失真,函数已经改名,文档仍然展示旧 API。
第四个陷阱是过度承诺 Unicode 支持。本文的 truncate 使用 JavaScript 字符串长度,不等于用户看到的字符簇长度。如果要支持 emoji 安全截断,请单独设计 Intl.Segmenter 版本并写明兼容性。第五个陷阱是让 Claude Code 直接决定发布。包名、scope、2FA、Trusted Publishing、最终 release approval 应由人确认。
Claude Code 提示词模板
Create a TypeScript npm package.
Goal:
- Package name: @acme/string-kit
- Support both ESM import and CJS require
- Use tsup to emit dist/index.js, dist/index.cjs, and dist/index.d.ts
- Include README generation, Vitest tests, and npm pack verification
Constraints:
- Only touch package.json, tsconfig.json, tsup.config.ts, src, scripts, and .github/workflows
- Do not use pseudocode; the project must run after npm install
- Do not publish source maps or unnecessary test files in the package tarball
Acceptance criteria:
- npm test passes
- npm run build passes
- npm pack --dry-run output is summarized
- ESM import and CJS require smoke tests are shown
- List the human release checks before npm publish
CTA:把发布流程变成固定模板
npm 包发布不是一次性任务。版本、README、CI 的 Node.js 版本、依赖和 registry 权限都会变化。可以先领取 免费 Claude Code 速查表,把安全提示词和验证命令放在手边。需要可复用模板时,可以查看 ClaudeCodeLab 产品。如果团队要统一 CLAUDE.md、CI、发布权限和代码审查流程,可以从 Claude Code 培训与咨询 开始。
实际试用本文流程后,在 Windows 临时目录中完成了 npm install、npm test、npm run build、npm pack --dry-run,并通过 ESM/CJS 的 node -e 冒烟测试。Masa 的经验是,让 Claude Code 在发布前解释 npm pack 输出,最容易提前发现 README 过期、声明文件缺失、tarball 内容异常这三类问题。
总结
Claude Code 可以加速 npm 包创建,但发布契约必须由你写清楚。把 package.json、exports、类型、tsup、测试、README、npm pack 和 CI 连成一个流程,就能把 AI 生成的雏形推进到更接近公开质量的状态。发布前只问三件事:看过 dist 吗,看过 tarball 吗,用用户入口导入过吗。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
Claude Code权限安全阶梯:逐步放开访问而不失控
从只读到有限编辑、验证命令和部署检查的 Claude Code 权限升级流程。
Claude Code 小PR证据包:让小改动真正可审查
用差异、验证命令、公开URL、CTA路径和回滚说明,把Claude Code的小PR变得可审查。
Claude Code 提交前 Review Gate:同时检查差异、测试、公开 URL 和 CTA
提交前用 Claude Code 审查差异范围、build、公开 URL、Gumroad 链接、咨询 CTA、缺少测试和无关文件。