cloudflare

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:fsnode: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 presetAstro
Install commandpnpm install --frozen-lockfile
Build commandpnpm run build
Build output directorydist
Node.js version22

仓库里也只保留一种锁文件。如果决定用 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 不是“找不到包”的修复方案。

下次遇到同样的报错,可以这样查

  1. 把完整日志保存下来。重点看 failed to resolve import 那一行,不要只截最后的 external 提示。

  2. 本地做一次干净安装。删除 node_modules 后再执行:

    pnpm install --frozen-lockfile
    pnpm run build
  3. 检查找不到的包。它是不是被你的代码直接 import 了?如果是,就加到 dependencies

  4. 检查引用它的文件。这个文件是不是 React 客户端组件?如果是,确认里面没有读文件、Node.js API 等服务器代码。

  5. 如果报错和 WASM 或平台包有关,检查 wasm32。浏览器可能需要和 Linux 构建机不同的包。

  6. 最后才考虑 external。只有当这个包本来就不应该被打进浏览器时才用。

总结

这次 Cloudflare Pages 失败,表面上看像少了一段 Vite 配置,实际上是两个很普通的问题:项目没有声明自己直接使用的依赖;浏览器需要的 WASM 包也没有安装。

下次看到 build.rolldownOptions.external,先别急着改配置。先看清楚:缺哪个包、谁在引用它、这段代码是不是应该在浏览器运行。这三个问题查明白,基本就知道该怎么修了。

Mttao

Mttao GitHub ↗

探索技术与生活的智慧

相关文章

/ 评论