Node SSR 报错:ERR_MODULE_NOT_FOUND(antd/es + @ant-design/icons)排查与通用修复

3 阅读4分钟

Node SSR 报错:ERR_MODULE_NOT_FOUND(antd/es + @ant-design/icons)排查与通用修复指南

适用于:Node 16+(尤其 Node 20),SSR/CJS 环境,使用 antd 4.x 与 @ant-design/icons 4.x 典型错误:Cannot find module ... @ant-design/icons/es/components/Context 关键词:antd/es、@ant-design/icons、exports、external、SSR、pnpm

———

1. 错误现象(典型日志)

Error [ERR_MODULE_NOT_FOUND]: Cannot find module .../node_modules/@ant-design/icons/es/components/Context imported from .../node_modules/antd/es/config-provider/index.js Did you mean to import "@ant-design/icons/es/components/Context.js"?

关键点:

  • Node 在 SSR 运行时加载了 antd/es/config-provider
  • 其中又引用了 @ant-design/icons/es/components/Context(无 .js 后缀)
  • Node 20 的 ESM 解析不接受无后缀路径 → 抛错

———

2. 结论先行:为什么会爆

本质原因: 在 CJS/SSR 运行时,实际执行到了 antd 的 ESM 子路径 antd/es/*。 而 antd 的 ESM 文件内部存在 无后缀导入,Node ESM 解析严格导致失败。

———

3. 为什么“以前没报错,现在报错”?——最常见的触发条件

这里的关键不是 exports/sideEffects 本身,而是 dist 产物里保留了 antd/es/*。

常见触发变化

A. external 规则变了(最常见)

  • 旧规则:只 externalize 顶层包名(antd)
    • antd/es/* 不外置,Rollup 会解析/打包,运行时不会再走 Node ESM 解析
  • 新规则:外置子路径(antd/es/*)
    • dist 里保留 require('antd/es/config-provider')
    • SSR 运行时进入 Node ESM 解析 → 报错

B. 重新构建/发布

  • 源码仍是 antd/es/*
  • 以前构建/打包某种方式“隐式处理”掉了
  • 现在产物原样输出导致 SSR 报错

C. Node 版本变化

  • Node 20 的 ESM 子路径解析更严格

———

4. 为什么 exports / sideEffects 不是根因

exports 的作用

  • 只决定 入口解析(require / import 走哪个入口)
  • 不会改写 dist 内部的 import/require

sideEffects 的作用

  • 只影响 tree-shaking
  • 不会修改构建产物路径

✅ 结论: exports/sideEffects 不会让 dist/index.js 里“出现” antd/es 它们最多改变“入口文件选哪个”,但不会改变 dist 内部 import 语句。

———

5. 快速定位与确认(推荐命令)

用来确认是否仍在 dist 里保留了 antd/es

1) 验证 Node 直接 require antd/es 是否报错

node -e "require('antd/es/config-provider')"

2) 检查 dist 内是否还包含 antd/es

rg -n "antd/es" node_modules/.pnpm/**/node_modules/<你的包名>/dist

如果还能搜到 antd/es,就说明 dist 产物是根因。

———

6. 解决思路(推荐优先级)

✅ 方案 A(推荐,保留 ESM 优势)

保留源码 antd/es,但在 CJS 构建时重写为 antd/lib

优点:

  • 浏览器构建仍能走 ESM tree-shaking
  • SSR/CJS 不会触发 Node ESM 解析

核心做法: 在 Rollup CJS 构建中加一个 rewrite:

const rewriteAntdEsToLib = () => ({ name: 'rewrite-antd-es-to-lib', resolveId(source) { if (source === 'antd/es') { return { id: 'antd/lib', external: true }; } if (source.startsWith('antd/es/')) { return { id: source.replace('antd/es/', 'antd/lib/'), external: true }; } return null; }, });

然后确保 CJS 构建产物中不再出现 antd/es/*。

———

✅ 方案 B(简单稳定)

直接改源码,把 antd/es/* 改成 antd/lib/*

优点:

  • 实施最快
  • CJS/SSR 一定稳定

缺点:

  • ESM tree-shaking 变差
  • 前端打包可能稍微变大

———

✅ 方案 C(临时绕过,不推荐长期使用)

绕过 Node ESM 解析

  • 启动时加 --experimental-specifier-resolution=node
  • 或 patch antd ESM 文件补 .js 后缀

缺点:

  • 有兼容性风险
  • 不适合长期维护

———

7. 结合 exports 的推荐配置(可选)

确保入口解析更清晰,避免入口被误走:

"exports": { ".": { "types": "./dist/index.d.ts", "require": "./dist/index.js", "import": "./es/index.js", "default": "./dist/index.js" }, "./es/": "./es/", "./dist/": "./dist/" }

注意:这是入口配置,不是根因修复。 真正要修的是 dist 中 antd/es/* 的出现。

———

8. 常见误区 FAQ

Q1:我只改了 exports,为什么还是报错?

A:exports 只决定入口,不会改写 dist 内部引用。 只要 dist 里仍是 antd/es/*,SSR 就会报错。

Q2:我是 CJS,为什么会触发 ESM 错误?

A:因为 require('antd/es/...') 会进入 ESM 模块解析流程。

Q3:外部依赖 external 会导致这个问题吗?

A:会。 如果 external 规则包含了 antd/es/*,dist 会保留原样引用,SSR 就会触发 Node ESM 解析。

Q4:我以前没问题,现在才出?

A:通常是 external 规则变化、Node 版本变化、或重新构建导致 dist 暴露了 antd/es/*。

———

9. 建议的排查流程(通用版)

  1. 确认报错路径是否来自 antd/es
  2. 检查 dist 产物里是否出现 antd/es
  3. 如果有:
    • 修 dist(方案 A / B)
  4. 如果没有:
    • 检查 SSR 入口是否走错(exports / alias / bundle)

———

10. 一句话总结

真正的问题不是 exports,而是 CJS 产物里仍然引用了 antd/es/,触发 Node ESM 解析的无后缀路径错误。 修复目标:让 SSR/CJS 产物不再引用 antd/es/