如何创建和发布 npm 包
你已经用 npm install 安装过数百个包了。现在你想发布自己的包。这个过程看起来很简单,直到你发现过时的教程还在推荐 Node 12、纯 CommonJS 设置,以及将长期有效的令牌存储在 CI secrets 中。
本指南将带你使用现代化、安全的实践创建和发布 npm 包,这些方法在 2025 年仍然适用。我们将构建一个简单的工具,正确配置它,并使用 npm 可信发布(Trusted Publishing)来发布。
核心要点
- 使用 ESM 和正确的
exports字段,而不是传统的main字段来实现现代化的包解析 - 将 TypeScript 编译为 JavaScript 并生成声明文件,而不是直接发布原始 TypeScript
- 启用双因素认证,并通过 GitHub Actions OIDC 使用 npm 可信发布来实现安全的自动化发布
- 发布前始终使用
npm pack --dry-run验证包内容
前置要求
开始之前,请确保你具备:
- 已安装 Node.js 18 或更高版本
- 一个 GitHub 账号
- 作为使用者对 npm 有基本了解
初始化 ESM 优先的 npm 包设置
创建一个新目录并初始化项目:
mkdir use-toggle && cd use-toggle
npm init -y
git init
我们将构建一个名为 useToggle 的简单 React hook。这个示例演示了如何使用正确的入口点发布 TypeScript npm 包。
为现代化发布配置 package.json
用正确配置的版本替换你的 package.json:
{
"name": "@yourusername/use-toggle",
"version": "0.1.0",
"description": "A simple React hook for boolean toggle state",
"keywords": ["react", "hook", "toggle", "state"],
"license": "MIT",
"author": "Your Name",
"repository": {
"type": "git",
"url": "git+https://github.com/yourusername/use-toggle.git"
},
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": ["dist"],
"engines": {
"node": ">=18"
},
"peerDependencies": {
"react": ">=18"
},
"devDependencies": {
"typescript": "^5.4.0",
"react": "^18.0.0",
"@types/react": "^18.0.0"
},
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
}
}
ESM 优先的 npm 包设置的关键点:
type: "module"声明 ESM 为默认模块系统exports替代传统的main字段——这是现代 Node 解析入口点的方式files控制发布的内容(仅dist/)engines指定 Node 18+,避免传统兼容性问题
设置 TypeScript 编译
创建 tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"skipLibCheck": true
},
"include": ["src"]
}
创建 src/index.ts:
import { useState, useCallback } from 'react';
export function useToggle(initial = false): [boolean, () => void] {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue(v => !v), []);
return [value, toggle];
}
运行 npm run build 进行编译。你将得到 dist/index.js 和 dist/index.d.ts——JavaScript 加类型声明,而不是原始 TypeScript。
2025 年安全的 npm 包发布
创建启用 2FA 的 npm 账号
- 在 npmjs.com 注册
- 立即启用双因素认证(推荐使用 WebAuthn/密钥)
- 所有新包现在默认启用 2FA 强制要求
- 使用新的基于会话的登录模式,发布任何包——无论新旧——都需要在会话期间进行 2FA 验证
首次手动发布
npm login 现在会创建一个两小时有效的会话令牌。这些令牌:
- 不会出现在 npm UI 中
- 两小时后自动过期
- 无法在 CI/CD 中重复使用
- 发布前始终强制执行 2FA
通过已弃用的 CouchDB 端点进行身份验证的旧工具在过渡期间也会收到两小时有效的会话令牌。
首次发布:
npm login
npm publish --access public
对于免费账号上的作用域包,--access public 标志是必需的。
在 https://www.npmjs.com/package/@yourusername/use-toggle 验证你的包。
Discover how at OpenReplay.com.
使用 CI 的 npm 可信发布
旧教程会告诉你创建一个 NPM_TOKEN 并将其存储为仓库 secret。这种方法现在已经过时:经典令牌已被永久撤销,不再支持长期有效的 CI 令牌。
现代化的替代方案是通过 GitHub Actions OIDC 的 npm 可信发布。GitHub 向 npm 证明其身份,npm 自动颁发短期凭证——无需存储令牌。
配置可信发布
- 在 npmjs.com 上,进入你的包 → Settings → Publishing access
- 添加一个新的 “Trusted Publisher”,填写你的 GitHub 仓库详情
创建工作流
添加 .github/workflows/publish.yml:
name: Publish
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm publish --provenance --access public
--provenance 标志添加了加密证明,将你的包链接到其源代码仓库——这是 npm 在 2023 年引入的供应链安全特性。
如果无法使用 OIDC(例如自托管的 runner),可以使用以下命令创建短期细粒度访问令牌:
npm token create
细粒度令牌可以选择为 CI 发布绕过 2FA,且必须在 90 天内过期。
旧教程中会看到的内容
避免这些过时的模式:
- 没有
exports的"main": "index.js"——使用exports实现正确的 ESM 解析 - 针对 Node 10/12/14——支持 Node 18+,但新包应针对现代 LTS 版本(Node 20 或 22)
- 纯 CommonJS 包——ESM 是标准;优先发布 ESM
- 每个工作流中的永久
NPM_TOKEN——经典令牌已完全撤销;使用无令牌的可信发布
发布前验证
发布前始终在本地测试你的包:
npm pack --dry-run
这会准确显示将包含哪些文件。如果你看到 src/ 或 node_modules/,说明你的 files 字段需要调整。
总结
要在 2025 年创建和发布 npm 包:使用带有正确 exports 的 ESM,将 TypeScript 编译为带声明的 JavaScript,启用 2FA,并使用 npm 可信发布自动化发布。跳过传统模式——它们会给你和你的用户带来麻烦。
常见问题
发布编译后的 JavaScript 和声明文件。发布包含编译后的 JS 和生成的 .d.ts 文件的 dist 文件夹。这确保了与不同 TypeScript 版本和构建工具的兼容性。原始 TypeScript 会强制使用者编译你的代码,这可能导致版本冲突和更慢的构建。
对于你的代码打包和发布的包使用 dependencies。对于使用者必须提供的包使用 peerDependencies,比如 React hook 的 React。这可以防止最终 bundle 中出现大型库的重复副本。你的包期望使用者的版本,而不是打包自己的版本。
像 @username/package-name 这样的作用域包在 npm 上默认为私有。免费 npm 账号无法发布私有包,因此必须明确设置 --access public。此标志告诉 npm 公开发布包。你只需要在首次发布时使用它;后续版本会继承访问级别。
使用 npm deprecate @scope/package 警告用户而不删除包。使用 npm unpublish 完全取消发布仅限于发布后 72 小时内或下载量极少的包。对于废弃的包,推荐使用弃用功能,因为它会警告用户同时保留现有安装。
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.