ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从 npm publish 到 npx 使用:命令行工具发布全流程与高频报错排查

从 npm publish 到 npx 使用:命令行工具发布全流程与高频报错排查 上周我把自己写的第一个命令行工具发布到 npm 上了。从跑完npm publish --dry-run那一瞬间的“原来如此”到第二天同事在他电脑上敲了一句npx就直接跑起来整个过程踩了不少坑也把发布链路里几个容易忽略的细节彻底搞明白了。这篇内容就是把这次完整过程记录下来从dry-run预演、bin字段配置、registry 与镜像源切换、版本号管理到别人用npx一条命令运行你的包以及 Windows 下各种高频报错的排查方法。如果你正准备发布自己的第一个 npm 包或者在本地反复遇到npm.ps1无法加载、npx不被识别、ERESOLVE冲突这类问题这篇文章基本可以覆盖你接下来几小时会碰到的绝大多数坑。1. dry-run发布前的“无伤彩排”1.1 dry-run 到底干了什么很多人第一次发 npm 包都是直接npm publish结果要么把一堆不该上传的文件发上去了要么报错之后才想起本地应该先验证一下。npm publish --dry-run这个命令做的事情很简单把发布会经历的所有步骤完整跑一遍——打包、生成文件清单、计算包体积、检查 package.json 元信息但最后一步“真正上传到 registry”被拦截住了。我把它理解为“带妆彩排”。灯光、走位、台词全按正式演出来只是台下没有观众。这个预处理过程能给你一份非常关键的报告包里到底会带哪些文件、包有多大、文件名是什么。比如执行npm publish --dry-run输出里会有类似这样的内容npm notice npm notice greeting-cli0.1.0 npm notice Tarball Contents npm notice 1.2kB package.json npm notice 3.1kB lib/cli.js npm notice 1.4kB README.md npm notice Tarball Details npm notice name: greeting-cli npm notice version: 0.1.0 npm notice package size: 2.8 kB npm notice unpacked size: 5.7 kB npm notice total files: 3注意看Tarball Contents这一栏它列出来的就是将来别人npm install时能拿到的全部文件。这是一个很好的体检机会如果这里列出了一个不该出现的.env、node_modules或者一堆测试临时文件那说明你的发布配置有问题需要立刻停下来处理。1.2 用 files 字段管好你的发布内容第一次跑 dry-run 时我的包非常“干净”因为我在 package.json 里加了一个files白名单。files字段决定了哪些文件会被打包进发布产物它只接受数组形式比如{ name: greeting-cli, version: 0.1.0, files: [ lib, bin, README.md ] }有了这个白名单之后npm publish基本只会把lib和bin两个目录加上 README 带上去package.json 是自动包含的不需要写进去。相反如果你不用filesnpm 默认会打包几乎所有文件只排除.git、.svn、node_modules等少数几个路径这时你就得靠.npmignore来补刀。我个人的做法是优先用files白名单因为它更不容易漏。.npmignore是黑名单思维只适合项目里文件特别多、又不想一个个列白名单的情况。还有一个细节files白名单不能排除 package.json 和 README这两个是强制包含的。如果你 README 没写npm 会警告而且发布到 npm 官网后包主页会是一块空白非常劝退使用者。1.3 我在 dry-run 里揪出来的三个问题第一次跑 dry-run 我就发现三个问题很典型。第一个是bin字段没配好。我一开始只写了main字段指向入口文件dry-run 报告里完全没有 bin 信息。这意味着别人即使安装了包npx也找不到对应的可执行命令。后来我在 package.json 里补上bin才算真正变成一个“命令行工具”。第二个是 README 内容为空导致 warning。npm 会提示no readme data发布倒是能发但是 npm 官网的包详情页会非常难看。另外如果包名跟 README 里的用法对不上也会让人困惑。第三个是打包体积里出现了一个临时日志文件。因为测试时生成过debug.log又没有清理dry-run 报告把它列在 Tarball Contents 里。这个文件除了增加包体积没有任何作用而且可能包含本地路径等环境信息。用files白名单之后这类问题基本绝迹。这里我强烈建议在发布前把npm pack --dry-run也跑一次它会直接在本地生成一个.tgz文件你可以用压缩工具打开看看里面到底有什么。npm pack --dry-run和npm publish --dry-run的差异在于前者是“本地打包演练”后者是“完整发布流程演练”两者都能看到文件清单但publish --dry-run还会额外校验登录态、版本号、registry 等发布环节的配置。本地多看一眼 tarball就好比去餐厅吃饭前先看一眼后厨心里踏实。2. 把一个普通 JS 文件变成“命令行工具”2.1 package.json 里的 bin 字段很多人写了一个不错的 CLI 脚本但发到 npm 之后怎么都跑不起来原因几乎都出在bin字段上。bin字段的作用是把一个可执行文件“注册”成命令。它的写法有两种字符串简化版和对象完整版{ name: greeting-cli, bin: ./bin/cli.js }上面的写法等价于说安装这个包之后创建一个名为greeting-cli的命令它指向./bin/cli.js。如果你想自定义命令名比如包名叫greeting-cli但你想让用户敲的是hi就写成对象形式{ name: greeting-cli, bin: { hi: ./bin/cli.js } }npx执行时的行为其实很直接它会在临时安装的包的node_modules/.bin目录里找对应名字的命令然后执行。所以如果你不写bin字段npx greeting-cli就一定会报“命令找不到”。这个字段是 CLI 类 npm 包的生命线。2.2 第一行 shebang 为什么必须是 #!/usr/bin/env node在写bin/cli.js的时候第一行必须是#!/usr/bin/env node这一行叫 shebang它的作用是指定这个文件要用什么解释器来执行。没有它你在 Unix/Linux/macOS 上直接运行脚本时会得到 permission denied 或无法识别文件格式在 Windows 上 npm 生成 shim 的逻辑也会出问题。#!/usr/bin/env node的意思是去环境变量 PATH 里找到node可执行文件然后用它来运行当前脚本。为什么不直接写/usr/bin/node因为不同机器的 Node.js 安装路径千差万别用env去找才足够通用。这是 Node.js 生态里事实上的标准写法也算是跨平台的一种“软编码”实现。写完后执行这个文件还需要给它加上可执行权限chmod x bin/cli.js这一步在 Windows 上不需要但在 macOS/Linux 上很关键。忘了加权限本地node bin/cli.js还是能跑但npx在 Linux 服务器上执行时会直接报 EACCES。2.3 本地先跑通node cli.js 与 npm link发布之前最好在本地完整跑一遍这个命令。比如我写了一个简化版的问候工具#!/usr/bin/env node const args process.argv.slice(2); const name args[0] || friend; console.log(Hello, ${name}! Welcome to npm package publishing.);本地验证最直接的方式是node bin/cli.js npm能输出预期的Hello, npm! ...。但这只能证明逻辑对不能证明bin注册没问题。更好的方案是用npm link它会在全局 node_modules 下创建一个符号链接相当于提前把包“全局安装”了一次。npm link运行之后你可以在任意目录执行greeting-cli npm如果这能跑通就说明bin字段、shebang、文件路径这几个关键点都对了。npm link本质上模拟了别人安装你包之后的效果只是在你的机器上是链接到本地源码目录。改代码不用重新安装实时生效非常适合开发阶段验证。调试完之后记得npm unlink清理全局符号链接否则下次发布新版本测试容易搞混。3. 真的发布登录、registry、版本号与首发3.1 registry 和镜像源发布前先确认你对着哪个源这一步是我这次发布过程中第一道坎。我本地长期用镜像源来加速安装依赖但发布的时候忘了切回来结果 npm 直接拒绝了登录提示 registry 不是官方源。npm 的registry就是“包仓库地址”安装依赖和发布包都会访问它。很多同学因为网络速度和稳定性问题习惯临时或长期使用镜像源这是一个很现实的需求。但要记住一个重要原则安装依赖可以用镜像源发布包必须切回官方源。原因很朴素镜像源本质上是一个副本服务主要面向下载场景发布场景要写数据必须写回官方仓库否则其他开发者从官方源永远看不到你的包。先看一下当前配置npm config get registry输出如果是镜像源地址发布前需要切回官方源npm config set registry https://registry.npmjs.org/如果你不想全局改配置更推荐在当前项目放一个.npmrc文件里面写registryhttps://registry.npmjs.org/。这样发布专用不影响全局。我后来就是用这种方式避免在全局配置和项目配置之间来回折腾。3.2 npm login 与登录态发布前必须先登录npm adduser # 或者 npm login两者会引导你输入用户名、密码和邮箱npm 会在本地保存一个 token。登录完之后可以用npm whoami确认当前登录身份npm whoami如果输出你的用户名说明登录成功。如果显示ENEEDAUTH或者403基本就是没登录或 token 过期。要特别提醒一点npm login生成的 token 相当于一把钥匙。社区和一些企业内部源经常发生 token 泄露事件导致有人被恶意 publish 恶意版本。所以不要把~/.npmrc里的//registry.npmjs.org/:_authToken...这行内容提交到 git 仓库。如果不小心泄露去 npm 官网 settinngs 里删掉 token 重新生成一个。3.3 版本号别手动改交给 npm versionnpm 包的版本号遵循语义化版本规范semver格式是主版本号.次版本号.修订号。首次发布通常是1.0.0或者0.1.0。修复 bug 加修订号新增功能向后兼容加次版本号有不兼容的大改动才加主版本号。很多新手会直接打开 package.json 改 version比如改成1.0.1然后发布。这样不是不行但很容易出问题忘了改、改错文件、改完不提交 git导致包版本和代码版本对不上。更稳妥的做法是用命令触发版本变更npm version patch # 1.0.0 - 1.0.1 npm version minor # 1.0.0 - 1.1.0 npm version major # 1.0.0 - 2.0.0这条命令会自动修改 package.json 里的版本号并且如果你在 git 仓库里它还会顺便打一个 tag。之后再npm publish版本号就不会出错。版本号在 npm 生态里是一条不可回退的链同一个版本号只能发布一次重复发布会报403。所以宁可版本号大一点也不要覆盖一个已有版本。3.4 发布动作npm publish登录完、版本号确认好之后执行npm publish如果你的包名是带 scope 的比如myscope/greeting-cli默认是私有包发布时会报错要求加npm publish --access public发布成功后你会看到类似这样的输出 greeting-cli0.1.0然后去 npm 官网搜索你的包名就能看到包主页。到这里你的包已经进了官方仓库全世界任何一台装好 Node.js 的机器理论上都能安装。不过我第一次发布之后就立刻发现一个尴尬问题包名已经被别人注册了会怎样答案是你根本发不上去npm 会直接提示403 Registry returned 403。所以在写代码之前就可以用npm view 包名看这个名字是否已存在避免做完一堆工作才发现名字撞车。4. 别人怎么通过 npx 用上你的包4.1 npx 的运行机制npx 是 npm 自带的一个命令行工具它的核心能力是“不安装也能执行 npm 包里的命令”。这句话值得拆开讲。npx greeting-cli执行时npx 会先检查本地node_modules/.bin里有没有这个命令如果没有就去 registry 查找这个包找到之后临时下载到一个缓存目录把包里的 bin 命令执行完然后这个临时包就会交给缓存管理。看起来像是“用完即走”实际上它还是会下载的只是不需要你手动把它写进 package.json。如果你不想 npx 每次询问是否安装可以加--yesnpx --yes greeting-cli如果你用的是带 scope 的包执行方式要写完整npx --yes myscope/greeting-clinpx 和 npm 命令最核心的区别就在安装后的生命周期上。npm install -g greeting-cli是全局安装命令会长期常驻npx greeting-cli是临时执行更适合同一个命令偶尔用一次或者想要保证每次都用最新版本的场景。这也是为什么很多工具型 CLI 推荐用 npx 而不是全局安装的原因。4.2 实际演示在空项目里 npx 一把梭包发布成功之后我特意开了一个全新的目录假装自己是一个普通用户mkdir demo-project cd demo-project npx greeting-cli npm第一次执行时 npx 会下载包然后输出Need to install the following packages: greeting-cli0.1.0 Ok to proceed? (y)按 y 回车等几秒就看到Hello, npm! Welcome to npm package publishing.到这一步一个 npm 包从发布到被全球任何一台机器通过 npx 消费的链路就彻底闭环了。整个过程没有再经过本地源码我从一个空白目录用一条命令拿到了包里的可执行程序。这种“别人能用上你的包”的感觉和之前npm link本地调试是完全不同的。4.3 让 npx 体验更顺滑包名、描述与 README包发布之后第一个版本的 README 写得比较随意。后来我发现 README 不只是“看得懂”还能直接影响别人愿不愿意用你的包。npm 官网的包详情页会直接渲染 README一个结构干净的 README 应该包含这个包是干什么的、安装方式、最少可用示例、完整的 API 或命令参数说明、License。如果你用npx命令安装README 里最好直接把npx 包名的示例写在最前面因为这是最省事的使用方式。包描述package.json 里的description字段也很重要。npm 搜索结果的卡片上显示的就是它。写得含糊其辞不如直接写清楚“一个在终端里向指定名字打招呼的小工具通过 npx 即可运行”。另外keywords字段虽然不影响功能但会影响 npm 搜索命中率顺手填几个合适的关键词不是坏事。发布工具的最终目标不只是“技术上能跑”而是“别人愿意跑”。5. Windows 下的高发坑从 PowerShell 到 PATH这一节基本是热词里那堆报错的实战排查记录。我发布完包之后有位同事在 Windows 上使用连续踩了好几个经典环境坑我远程帮他排查的同时把这些问题整理成了速查表。5.1 PowerShell 禁止运行脚本npm.ps1 / npx.ps1 无法加载在 Windows 上跑npm或npx时出现这样的报错非常高频npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本。有关详细信息请参阅 about_Execution_Policies。问题出在 Windows PowerShell 的脚本执行策略Execution Policy。Node.js 安装包在 Windows 上提供的npm和npx入口其实有两种一种是.cmd批处理文件一种是.ps1PowerShell 脚本。终端如果默认使用 PowerShell它就会尝试去执行.ps1脚本弹出来这个安全策略报错。解决方案是用管理员权限打开 PowerShell执行Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUserRemoteSigned的含义是本地创建的脚本可以运行从互联网下载的脚本必须有可信签名。这个设置在安全性和便利性之间比较平衡。为什么会这样因为npm安装时生成的npm.ps1是本地文件符合 RemoteSigned 的运行条件所以能跑。如果改成Unrestricted虽然也不会有什么大问题但没有必要把安全门槛放得那么开。如果同事的机器上有管理员权限限制改用CurrentUser作用域就不用动系统设置Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser -Force改完之后重新打开终端再执行npm -v通常就能通过。5.2 npx 不是内部或外部命令PATH 环境变量缺失另一个高频报错是npx : 无法将“npx”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。或者命令行提示npx 不是内部或外部命令。这种情况说明系统找不到npx可执行文件通常是因为 Node.js 安装的时候没有把可执行目录写进 PATH或者 PATH 里之前配置的路径和实际安装路径不一致。尤其是拿绿色版、手动解压的 Node.js 包很容易漏掉 PATH 配置。解决办法是确认 Node.js 安装目录然后把这个目录加到 PATH。在 PowerShell 里可以执行where.exe node查看 node 的完整路径一般是类似C:\Program Files\nodejs\node.exe那么这个路径下的npx.cmd、npm.cmd就是我们要找的执行入口。然后把C:\Program Files\nodejs加入系统的 PATH 环境变量。Windows 上还有一种特殊情况PowerShell 对可执行文件的查找顺序是 PATH 里的目录顺序如果之前安装过旧版本 Node.js卸载时残留了无效的 PATH 路径也可能导致找不到命令。清理 PATH 里不存在的路径重新打开终端一般就正常了。5.3 其他高频报错速查表除了上面的 PowerShell 和 PATH我整理了这次实践中遇到过的其他几个常见报错以及对应的处理思路。报错信息关键词原因分析处理建议npm err! code ebusyWindows 上某个文件被编辑器或另一个进程占用npm 无法覆盖写入关闭 IDE、终端、占用该文件的程序后重试必要时重启电脑再装npm error code eunsupportedprotocol unsupported url type workspace:项目依赖里写了workspace:协议这个协议是 pnpm 的 workspace 功能npm 原生不支持改用 pnpm 安装或把依赖改成file:或具体版本号ERESOLVE overriding peer dependency本地项目的 peerDependencies 版本与当前安装的依赖版本冲突按提示调整 peer 依赖版本临时场景可加--legacy-peer-deps但要意识到它在绕过冲突检查npm warn deprecated node-domexception1.0.0: use your platforms native dome...某个依赖包声明废弃了旧依赖只是警告不影响安装关注即可后续升级使用该旧依赖的上层包cannot find native binding. npm has a bug related to optional dependencies原生模块如 node-sass、bcrypt编译失败或 optional 依赖未安装成功先确保 Node 版本与模块兼容Windows 需要装好 VS Build Tools 和 Python再执行npm rebuild重试cb() called never!且npm cache相关缓存或者并发问题清理缓存npm cache verify或npm cache clean --force后重装npm ERR! code EINTEGRITY下载包时校验值不一致通常是缓存损坏或镜像同步不完全清缓存后切换一次源重试安装运维过 Windows 环境的人都能体会到环境变量和脚本执行策略这类问题往往比业务代码更磨人。但好在这些都是确定性很高的环境故障排查思路一旦整理成清单下次遇到基本能 5 分钟内定位。6. 发布之后的收尾与迭代6.1 版本升级与再次发布发布完成并不代表结束。只要你继续维护这个包下一次修改后就需要走“改代码、更新版本、发布”的循环。我的习惯是在本地开发分支完成代码修改并测试通过用npm version patch自动提升版本号执行npm publish在 git 里提交代码并推送 tag这个小循环可以让每次发版都有据可查。尤其是在团队协作时如果每个人都是手动改版本号再发布很容易漏掉 git tag后续回滚时完全找不到对应代码版本。6.2 不再维护怎么办deprecate 与 unpublish如果某个包以后不再维护了千万不要直接unpublish。npm unpublish只能在发布后 72 小时内执行而且会把包从 registry 里完全删除所有依赖你包的项目都会因此出现安装失败。这会给使用者带来连锁影响非常不建议。更负责任的方式是执行npm deprecate greeting-cli This package is no longer maintained. Please use xxx instead.这样别人安装时会在终端看到一条明确的废弃提示但仍然能正常安装不会破坏依赖关系。这是 npm 生态里约定俗成的“退场礼仪”。6.3 dist-tag 与后续扩展npm 的dist-tag是很多人容易忽略的功能。每个包默认有一个latest标签npm publish会把当前版本默认打到latest上。如果你想发一个试验性版本不希望用户默认安装到它可以用npm publish --tag beta之后用户需要显式npm install 包名beta才能装到日常npm install 包名仍然拿到 latest。这个机制在功能预览、灰度发布、尝鲜版本场景下非常有用。等 Beta 版本测试稳定了再用npm dist-tag add 包名版本号 latest把最新版提为正式版。管理好 tag 之后你的包在用户那边的安装体验会稳定很多。最后分享一点我自己的感受。发布一个 npm 包最大的门槛并不在写代码而在流程与环境。我第一次发布时光处理 Windows 下的 PowerShell 策略、PATH 配置、registry 源这几个环境问题就花了接近一个小时真正写 CLI 代码的时间反而不长。发布后第一件事不要急着到处宣传先找一个干净环境用npx完整走一遍确认“别人视角”真的能用然后再分享出去。你发出去的不仅仅是一个包更是一段让别人少踩坑的体验。
返回列表