Cloudflare Pages 构建报错:别急着加 external
Cloudflare Pages 构建失败时,日志会提示把模块加到 build.rolldownOptions.external。 这次真正的问题不是配置,而是 pnpm 没装上项目直接使用的包,以及浏览器需要的 WASM 包。
约一千九百字·读约六分钟 · English
最近部署Astro 7的项目站点部署到 Cloudflare Pages 时,构建失败了。
本地跑 pnpm run build 没问题,推到 Pages 就挂。日志最后给了一句:
If you do want to externalize this module explicitly add it to
`build.rolldownOptions.external`
这句话很容易让人误会:好像只要去 astro.config.ts 加一段 external,问题就解决了。
这次不是这样。external 只是日志给的备选方案。真正的问题是:构建工具找不到一个包,而这个包刚好被浏览器端的代码用到了。
下面记一下我怎么查、最后怎么改。
先看完整报错,不要只看最后一行
把日志往上翻,通常会看到类似内容:
[vite]: Rolldown failed to resolve import "SOME_MODULE" from "SOME_FILE".
This is most likely unintended because it can break your application at runtime.
If you do want to externalize this module explicitly add it to
`build.rolldownOptions.external`
真正重要的是第一行:
SOME_MODULE:找不到的包名SOME_FILE:是哪一个文件在引用它
最后的 external 是说:如果你确定这个包不该被打进当前代码,可以把它排除掉。Vite 确实支持通过 build.rolldownOptions 配置底层打包器,但这不是“报错就加”的万能开关。
先找到缺的是哪个包,再决定怎么修。不要一上来就加 external。
这次调用栈里出现了 @vitejs/plugin-react。于是我把注意力放到带 client:load 的 React 组件上:是不是有一段只能在服务器跑的代码,被浏览器端间接引用了。
为什么本地没事,Cloudflare Pages 却失败了
我本地之前一直用 npm。Cloudflare Pages 则用 pnpm 做一次全新安装。
这两个包管理器的目录结构不一样。npm 常常把很多间接依赖放到顶层 node_modules。所以即使项目没有明确安装某个包,本地代码也可能刚好能找到它。
pnpm 默认不会这样做。项目根目录里只会直接放你自己声明过的依赖。代码用了一个包,但 package.json 里没写它,问题就会立刻露出来。
比如项目里有这样的代码:
import { codeToHtml } from 'shiki'
如果 shiki 不在当前项目的 dependencies 里,本地能跑只是碰巧。可能是 Astro、某个插件,或者旧的 node_modules 把它带出来了。到了 Pages 的干净环境里,这个包没有安装,构建自然就失败了。
所以第一个修复很简单:代码直接 import 的包,就直接写进自己的 package.json。
pnpm add shiki
Cloudflare Pages 没有把项目弄坏。它只是比本地更早发现依赖没写全。
接着发现:浏览器还缺一个 WASM 包
补完直接依赖以后,我继续看报错来源,发现问题和 Markdown 高亮工具 Sätteri 有关。
Sätteri 在服务器上通常使用 native 版本,也就是为 Linux、macOS 或 Windows 单独准备的二进制文件。但代码被打进浏览器后,浏览器不能用这些 native 文件,需要改用 WASM 版本。可以把 WASM 理解成“能在浏览器中运行的一种编译产物”。
Sätteri 的浏览器版本是另一个包:@bruits/satteri-wasm32-wasi。它是一个可选依赖,包管理器会按当前系统决定要不要安装它。
问题就在这里。Cloudflare 的构建机是 Linux,pnpm 默认只安装 Linux 当前需要的包;浏览器需要的是 wasm32 版本,所以这个包没有被装下来。等 React 组件把高亮相关代码打进浏览器时,Rolldown 找不到它,又报了一次“找不到模块”。
这不是 Cloudflare 少装了系统库,而是项目没有告诉 pnpm:除了当前机器的依赖,还需要下载浏览器用的 WASM 包。
如果你确实要在浏览器里使用 Sätteri,可以在项目根目录创建或修改 pnpm-workspace.yaml:
supportedArchitectures:
os:
- current
cpu:
- current
- wasm32
加上 wasm32 后,pnpm 会同时安装给浏览器准备的 WASM 包。Sätteri 的安装文档也给了这个配置方式。
还有一个更重要的问题:这段代码真的该在浏览器里跑吗
查到这里,还得确认一件事:Markdown 高亮到底要在浏览器做,还是在构建阶段做?
如果只是生成静态页面,通常没必要让浏览器再跑一遍高亮。把高亮、读文件、扫描目录这些操作放在 .astro 文件或服务端模块里会更简单。带 client:load 的 React 组件只负责浏览器里真正需要互动的部分。
可以用下面这个判断:
| 代码做的事 | 应该放在哪里 |
|---|---|
读文件、扫描目录、调用 node:fs | 服务端或构建阶段 |
| 生成 Markdown 高亮 HTML | 一般放在构建阶段 |
| 点击、输入、弹窗等用户交互 | React 客户端组件 |
如果浏览器端代码不小心引用了 node:fs、node:path 这类 Node.js 模块,不要靠 external 硬压过去。浏览器本来就没有这些 API。正确做法是把这部分逻辑移回服务端,或者拆成两个文件。
最后是怎么改的
这次我按下面的顺序处理,问题就清楚了。
1. 固定 pnpm 和构建命令
在 package.json 里写清楚项目使用哪个 pnpm 版本:
{
"packageManager": "[email protected]",
"engines": {
"node": ">=22",
"pnpm": ">=10"
},
"scripts": {
"build": "astro build"
}
}
Cloudflare Pages 用下面的设置即可。Astro 的构建输出目录是 dist。
| 配置项 | 值 |
|---|---|
| Framework preset | Astro |
| Install command | pnpm install --frozen-lockfile |
| Build command | pnpm run build |
| Build output directory | dist |
| Node.js version | 22 |
仓库里也只保留一种锁文件。如果决定用 pnpm,就保留 pnpm-lock.yaml,不要同时留下 package-lock.json,避免本地和 CI 用了不同的依赖版本。
2. 补齐项目直接用到的依赖
像 shiki 这种被源码直接导入的包,要放进 dependencies。不要依赖它“刚好是某个插件的依赖”。
3. 需要浏览器 WASM 时,告诉 pnpm 安装 wasm32
如果相关代码必须在浏览器运行,就按前面的 pnpm-workspace.yaml 配置加上 wasm32。如果它不需要在浏览器运行,更好的办法是把它移出 React 客户端组件。
4. external 放到最后再考虑
只有一种情况适合用 external:你已经确认这个包不该被打进当前 bundle。
例如,某个 Node.js 工具被错误地从客户端引入。此时可以把它当作额外保护,但前提仍然是先修好导入关系:
// astro.config.ts
export default defineConfig({
vite: {
build: {
rolldownOptions: {
external: [/^node:/],
},
},
},
})
如果你把浏览器真正需要的包排除掉,构建也许会成功,但页面运行时还是会报错。所以,external 不是“找不到包”的修复方案。
下次遇到同样的报错,可以这样查
-
把完整日志保存下来。重点看
failed to resolve import那一行,不要只截最后的external提示。 -
本地做一次干净安装。删除
node_modules后再执行:pnpm install --frozen-lockfile pnpm run build -
检查找不到的包。它是不是被你的代码直接
import了?如果是,就加到dependencies。 -
检查引用它的文件。这个文件是不是 React 客户端组件?如果是,确认里面没有读文件、Node.js API 等服务器代码。
-
如果报错和 WASM 或平台包有关,检查
wasm32。浏览器可能需要和 Linux 构建机不同的包。 -
最后才考虑
external。只有当这个包本来就不应该被打进浏览器时才用。
总结
这次 Cloudflare Pages 失败,表面上看像少了一段 Vite 配置,实际上是两个很普通的问题:项目没有声明自己直接使用的依赖;浏览器需要的 WASM 包也没有安装。
下次看到 build.rolldownOptions.external,先别急着改配置。先看清楚:缺哪个包、谁在引用它、这段代码是不是应该在浏览器运行。这三个问题查明白,基本就知道该怎么修了。
Mttao GitHub ↗
探索技术与生活的智慧