12k
All articles

如何避免向 npm 发布错误的文件

避免在 npm 包中发布意外文件或机密信息。了解 .npmignore、.gitignore 与 package.json 的 files 字段如何交互,并用 npm pack --dry-run 检查包内容。

OpenReplay Team
OpenReplay Team
如何避免向 npm 发布错误的文件

如果你的包中有 .npmignore 文件,npm 在决定哪些内容不打进 tarball 时就不再读取 .gitignore。即使 .npmignore 文件是空的,也是如此。

大多数人是在 tarball 体积远超预期,或者某个本不打算发布的点文件出现在 registry 上时,才发现这一点的。忽略规则看起来没问题,事实上也确实没问题——直到有人添加了第二个忽略文件。

本文介绍会让错误文件混入包中的各种陷阱:.gitignore 的回退机制、files 与 .npmignore 之间的相互作用、npm 固定的包含与排除列表,以及有问题的版本已经上线后该怎么办。阅读本文前,建议先阅读 How to Create and Publish an npm Package。

要点速览

  • 只要存在 .npmignore 文件(哪怕是空文件),npm 原本会应用到 tarball 上的所有 .gitignore 规则都会失效。
  • npm 会读取每个子目录中的 .npmignore 和 .gitignore,因此目录树深处的一个忽略文件就可能改变发布内容。
  • npm 的内置排除规则不涵盖 .env 文件、私钥或证书。
  • 如果密钥泄露到了 registry,首先要轮换密钥。撤销发布无法收回已被下载的副本。
  • Provenance(来源证明)记录的是构建某个版本所用的仓库、提交和工作流,并不能证明代码是安全的。

npmignore 陷阱:新增一个文件,所有 gitignore 规则全部失效

npm 决定哪些内容不打进包时,只会使用 .npmignore 或 .gitignore 中的一个,绝不会同时使用两者。npm 关于将文件排除在包外的规则非常简短:没有 .npmignore 时,npm 应用 .gitignore;一旦存在任何 .npmignore,.gitignore 就不再生效。两个文件永远不会合并。

最常见的诱因是构建产物。dist/ 被 gitignore 忽略,因此 npm 不会把它打进 tarball。于是你添加了一个 .npmignore,好让 dist/ 能够发布出去。结果 dist/ 确实发布了,但原先被 .gitignore 隐藏的其他所有内容也一并发布了:.env.local、coverage/ 和 test/fixtures/。

npm 并不只查看根目录。它会从包内任意文件夹中读取这两类忽略文件,因此嵌套文件夹中一个误放的 .npmignore,就可能在没人改动根目录配置的情况下改变发布内容。

前后对比:文件列表发生了变化

下面的脚本构建了一个完全由 .gitignore 负责过滤的包:

mkdir pkg-trap && cd pkg-trap
npm init -y >/dev/null
mkdir -p src dist test/fixtures coverage
echo 'module.exports = 1' > src/index.js
echo 'module.exports = 1' > dist/index.js
echo 'API_KEY=replace-me' > .env.local
echo '{}' > test/fixtures/big.json
echo 'x' > coverage/lcov.info
printf 'dist/\n.env.local\ncoverage/\ntest/fixtures/\n' > .gitignore

npm pack --dry-run

npm 列出以下内容(仅显示路径,已精简):

package.json
src/index.js

dist/ 没有包含在内,于是你添加了一行 .npmignore,以便发布构建产物并去掉源码:

echo 'src/' > .npmignore
npm pack --dry-run
  package.json
- src/index.js
+ dist/index.js
+ .env.local
+ coverage/lcov.info
+ test/fixtures/big.json

解决办法是把仍然需要的每一条规则都复制到 .npmignore 中:

printf 'src/\n.env*\ncoverage/\ntest/\n*.map\n' > .npmignore

files 与 .npmignore:白名单还是黑名单

package.json 中的 files 字段是白名单,而 .npmignore 是黑名单。如果两者同时使用,就没有任何一个文件能单独说明你的包里到底有什么。

机制模式被你遗忘的新文件
files白名单不会发布
.npmignore黑名单会被发布
.gitignore(回退)黑名单会被发布,除非它也被 gitignore 忽略

问题往往出在 files 与 .npmignore 的重叠之处。根据 package.json 文档,根目录中的 .npmignore 无法剔除 files 已经添加的内容,但子文件夹中的 .npmignore 可以:

{ "name": "my-lib", "version": "1.0.0", "main": "dist/index.js", "files": ["dist"] }
# dist/.npmignore
*.map

这里 files 引入了 dist/,嵌套的忽略文件又从中过滤掉了 sourcemap。于是一份清单由两个文件共同定义。那些会自行写入输出目录的构建工具会让情况更糟,所以如果你按照我们的 pnpm 指南操作,请检查最终进入 dist/ 的内容。最简单的原则是:只选一种机制,删掉另一种。

npm 始终包含和始终排除的文件

无论如何配置,有些文件总会被包含或排除。根据 package.json 文档,npm 始终包含 package.json、README、LICENSE 或 LICENCE、main 中指定的文件以及 bin 中指定的所有文件。npm 始终排除版本控制文件夹、.npmrc、node_modules 和编辑器产生的杂项文件。密钥类文件不在这份列表中。

始终包含始终排除(不完整列表)除非显式指定,否则不会排除
package.json、README(及其变体)、LICENSE / LICENCE、main 文件、bin 文件.git、.npmrc、node_modules(打包依赖除外)、.DS_Store、npm-debug.log、config.gypi、.*.swp、*.orig、package-lock.json.env、.env.local、*.pem、*.key、fixtures、coverage

为最右列的文件加一道防护:

npm pack --dry-run 2>&1 | grep -E '\.env|\.pem$|\.key$' && { echo "STOP: secrets in tarball"; exit 1; }

每次发布前运行 npm pack —dry-run

npm pack --dry-run 会打印出 npm 将要发布的确切文件列表,而不会真正生成 tarball,因此能发现上述所有陷阱。每当你修改任何忽略文件、files 字段或构建配置后,都应运行它,并在 CI 中将其放在 npm publish 之前。基本工作流请参阅 How to Create and Publish an npm Package。

npm pack --dry-run

已经发布了错误文件该怎么办

如果密钥泄露到了 registry,首先要轮换它,并将其永久视为已公开。撤销发布某个版本并不会删除已被下载或镜像的副本。之后,再清理 registry:

  1. 在凭据提供方处轮换凭据。
  2. 删除或标记有问题的版本。根据 npm 的撤销发布政策,只要没有其他公开包依赖该版本,你可以在发布后 72 小时内撤销发布。超过这个期限后,只有满足政策条件的包才能撤销发布。否则,请使用 npm deprecate。
  3. 发布一个干净的补丁版本。已经使用过的版本号永远不能再次使用,即使该版本已被撤销发布。
# Within 72 hours, subject to npm's unpublish policy
npm unpublish my-lib@1.4.2

# Otherwise
npm deprecate my-lib@1.4.2 "Contains a leaked file. Upgrade to 1.4.3."

# Fix the ignore rules, confirm, republish
npm pack --dry-run
npm version patch
npm publish

Provenance 证明的是来源,而非安全性

npm provenance 声明记录了构建某个包版本所用的仓库、提交和 CI 工作流。它既不能说明该提交中的代码是安全的,也不能说明 tarball 中只包含你预期的文件。在 GitHub Actions 中,工作流需要 id-token: write 权限:

name: publish
on:
  release:
    types: [published]
permissions:
  contents: read
  id-token: write
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          registry-url: https://registry.npmjs.org
      - run: npm ci
      - run: npm pack --dry-run
      - run: npm publish

该工作流假定你已经配置好了 trusted publishing(可信发布)。可信发布要求 npm CLI 11.5.1 或更高版本以及 Node 22.14.0 或更高版本,并且会自动生成 provenance,因此该任务既不需要 --provenance,也不需要 NODE_AUTH_TOKEN。如果改用令牌发布,npm publish --provenance 需要 npm 9.5.0 或更高版本。无论哪种方式,package.json 中的 repository 字段都必须与发布所用的仓库完全一致(区分大小写),并且该仓库必须是公开的。如果 npm ci 因 peer 依赖冲突而失败,请参阅 How to Fix npm ERESOLVE Errors。关于 2FA 和可信发布的配置,请参阅 npm Security Best Practices。

总结

npm 的忽略规则出问题时不会报错。仅仅添加一个文件,就会悄无声息地改变被发布的文件。在 files 和 .npmignore 中只选其一并删除另一个,不要在子目录中放置忽略文件,并在 CI 中每次 npm publish 之前运行 npm pack --dry-run 并配合密钥 grep 检查。

常见问题

npm pack --dry-run 和 npm publish --dry-run 有什么区别?

两者都会报告哪些文件将被打进 tarball,且不会上传任何内容,因此都能发现忽略文件的错误。npm pack --dry-run 是检查文件内容更简单的方式。npm publish --dry-run 则是对发布命令本身的演练。在当前的 npm 版本中,它还会向 registry 查询该版本是否已存在,若已存在则失败;但未登录只会产生警告。演练通过并不能证明上传一定会成功,因为登录状态和 2FA 只有在真正上传时才会被验证。

如何查看已发布版本中包含哪些文件?

使用 registry 规格运行 npm pack,例如 npm pack my-lib@1.4.2。npm 会获取已发布的 tarball,将其以 my-lib-1.4.2.tgz 的名称保存到当前目录,并打印其中的文件名。可以用 tar -tzf my-lib-1.4.2.tgz 列出其内容。如需获取 tarball 的 URL 以便配合 curl 或其他工具使用,请运行 npm view my-lib@1.4.2 dist.tarball。

npm deprecate 能阻止别人下载含有泄露文件的版本吗?

不能。npm deprecate 会在 registry 条目中添加一条警告信息,npm 会向安装该版本的任何人显示这条信息,但 tarball 仍然可以下载。任何锁定了该确切版本的人仍会拿到泄露的文件。只有撤销发布才能将该版本从 registry 中移除,而这受到 npm 撤销发布政策的限制。如果之后想取消弃用标记,可以再次运行 npm deprecate,并将消息设为空字符串。

package.json 中的 files 字段可以使用 glob 模式吗?

可以。你可以列出普通文件名、文件夹,或者 * 和 **/* 这样的 glob 模式,文件夹条目会引入其中的所有内容。其语法相当于反过来的 .gitignore:匹配的模式会把文件加入 tarball,而不是将其排除。npm 始终包含的文件(如 package.json、README、LICENSE 和 main 文件)无论是否出现在该数组中都会被发布。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.