Rollup:从静态 ESM 图生成可发布的库产物
核验日期:2026-07-08。示例版本来自
examples/build-tools/package.json:rollup4.62.2、@rollup/plugin-typescript12.3.0、@rollup/plugin-html2.0.0、rollup-plugin-postcss4.0.2、@rollup/plugin-terser1.0.0。
需要掌握的问题
| 主题 | 读者必须回答的问题 | 原理与边界 | 高频错误 | 代码或观察证据 | 来源 |
|---|---|---|---|---|---|
| 静态 ESM 图 | Rollup 为什么适合库构建,和应用型打包器的主问题有什么不同? | Rollup 从 input 出发解析静态 import/export,建立可分析的 ESM 图,再生成 es、cjs、umd 等输出;它不提供完整开发服务器、框架 HMR 或类型检查 | 用 CommonJS 动态 require 期待完整 Tree Shaking;把 Rollup 当 Vite dev server | examples/build-tools/src/main.ts 的静态导入和动态导入 | Rollup ES Module Syntax |
| 插件构建钩子 | resolveId、load、transform、moduleParsed 各看见什么? | resolveId 决定模块 id,load 可提供源码,transform 改单模块代码和 map,moduleParsed 在 AST 被解析后观察模块信息 | 在 transform 里返回错误 Source Map,或在 moduleParsed 里试图改源码 | 最小插件钩子表和示例配置中的 manifest() 插件 | Plugin Development |
| 输出钩子 | renderChunk、generateBundle、writeBundle 的职责差异是什么? | renderChunk 改单个 chunk 代码,generateBundle 可看见完整 bundle 并增删资产,writeBundle 在文件写入后执行 | 在 renderChunk 使用最终 hash 文件名;在 writeBundle 才尝试改产物内容 | manifest() 在 generateBundle 中生成 manifest.json | Output Generation Hooks |
Tree Shaking 与 moduleSideEffects | 未使用导出何时会被删除,副作用如何保留? | Rollup 依赖静态 ESM、作用域分析和 treeshake.moduleSideEffects 判断模块是否可跳过;副作用模块必须被保留 | moduleSideEffects: false 删除 CSS、polyfill、注册代码;只看 bundle 体积不看运行语义 | 共享测试要求 SHOULD_NOT_REACH_BUNDLE 被移除、preserved 被保留 | treeshake.moduleSideEffects |
external 与 peer 依赖 | 库构建怎样决定哪些依赖留给消费者? | external 把 import 保留为外部引用;库的 peerDependencies 通常应 external,应用内小依赖可打包 | 把 React/Vue 等 peer 打进库,造成重复运行时和 hooks/context 不一致 | external 函数、peer 检查脚本、消费端安装测试 | external |
多入口与 package exports | 多入口库怎样让产物、声明文件和 exports 对齐? | input 可传对象或数组生成多个 entry chunks;package.json#exports 决定消费者能 import 哪些入口 | 产物有 dist/button.js,但 exports 没暴露;CJS/ESM 条件指错 | 多入口配置和 exports 对照表 | input |
dynamic import 与 manualChunks | 异步边界怎样进入 chunk graph,什么时候需要手动分块? | 动态导入形成 async chunk;manualChunks 可把模块归组,但会影响请求数、缓存和执行顺序 | HTML 直接加载 dynamic chunk;激进 manualChunks 把副作用提前或延后 | 共享示例 detail.ts 是 dynamic entry,HTML 只加载 initial JS | output.manualChunks |
| Source Map 链 | 插件 map 如何跨 TypeScript、PostCSS、terser 保持可追溯? | 每个 transform/输出阶段要消费上游 map 并返回新 map;最终 output.sourcemap 决定外部 map | 插件返回 code 但不返回 map,导致 DevTools 定位到生成代码 | artifact test 检查 JS map 包含 main.ts/detail.ts,CSS map 包含 styles.css | output.sourcemap |
| watch cache 与增量 | Rollup watch 复用什么,插件怎样登记额外依赖? | Rollup 可在 watch 中复用 bundle cache;插件读取外部文件时要登记 watch files,避免变化不触发 | 读取模板、manifest 输入或配置文件但没加入 watch,导致热重建旧结果 | 插件 this.addWatchFile、cache key 清单 | JavaScript API |
| 循环依赖 | 循环 ESM 为什么有时能工作,有时会输出 undefined 或顺序问题? | ESM live binding 支持循环,但初始化顺序仍受执行图影响;Rollup 会保持语义并给出警告 | 在循环模块顶层读取尚未初始化的绑定,或通过 chunk 拆分改变执行时机 | 最小循环依赖示例和 warning 处理策略 | Troubleshooting |
| 迁移与重大变化 | Rollup 2、3、4 哪些变化影响配置、插件和输出? | Rollup 3 改默认值和 CJS 动态导入行为,Rollup 4 引入 native optional dependency、Node 要求与输出钩子/hash 行为变化 | 没升级插件、依赖旧默认值、在 renderChunk 里读取最终 hash 文件名 | Rollup 2–4 迁移表 | Migrating to Rollup 4 |
内容边界说明:
| 分类 | 本文落点 |
|---|---|
| 编程语言与类型系统 | 只解释 ESM、dynamic import、TypeScript 删除和 CJS/ESM 输出格式如何影响构建;不教学 JS/TS 语法,也不把 Rollup 当类型检查器 |
| Browser APIs 与持久化能力 | 覆盖浏览器 <script type="module">、dynamic import、Source Map 和相对资源路径;IndexedDB、Storage、权限模型不属于 Rollup 职责 |
| UI 框架与元框架 | 只说明库构建中 peer 依赖和 package exports 的边界;组件调度、渲染和水合不在本文重复 |
| 构建工具与运行时 | 完整覆盖 input、resolve/load、transform、graph、Tree Shaking、chunk、output、cache、插件和产物验收 |
| 后端、数据与 Agent 技术 | 只覆盖配置/插件以 Node 权限运行、peer 发布和 CI 产物校验;请求生命周期、数据库一致性和 Agent 评测不适用 |
Rollup 的主问题:把静态 ESM 图变成可消费产物
Rollup 的强项不是“开发时最快刷新”,而是“尽量利用 ESM 的静态结构,生成语义清楚、可被消费者继续优化的产物”。这也是它长期适合库构建的原因:库作者通常需要控制 entry、external、输出格式和 package exports,而不是先追求开发服务器能力。
Rollup 和相邻工具的边界:
| 工具 | 主要输入 | 核心问题 | 不应期待它完成 |
|---|---|---|---|
| Babel/SWC | 单个 JS/TS 文件 | 语法转换、AST 改写、Source Map | 依赖图、HTML、CSS bundle、peer 发布边界 |
| Rollup | ESM entry 和插件 | 静态图、Tree Shaking、库输出、多格式发布 | Vite 式 dev server、HMR、框架路由 |
| Vite | HTML 入口和 dev 请求 | 开发服务器、按需 transform、生产构建 orchestration | 纯库发布策略、所有 SSR 框架语义 |
| webpack/Rspack | 应用 entry 和 runtime | 应用 chunk/runtime、Loader/Plugin 生态、长期缓存 | 默认生成最小库产物 |
严重问题完整流程:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
| 库体积异常大,消费者也打进一份 React/Vue | peer 依赖没有 external | 搜索产物 react.production、createContext 等运行时代码 | 从 peerDependencies 生成 external,消费端安装 peer |
用户 import { Button } from "pkg/button" 报错 | package exports 没暴露该入口,或 ESM/CJS 条件错 | node -e "import('pkg/button')" 和 node -e "require('pkg/button')" | 让 Rollup input、产物路径、声明文件和 exports 一一对应 |
| CSS 或注册代码消失 | moduleSideEffects: false 或包级副作用声明过激 | 搜索产物 CSS、全局注册标记、preserved | 给副作用模块保留 side effect,或用函数精确判断 |
resolveId/load/transform/moduleParsed:插件先定义模块事实
Rollup 插件不是“构建后脚本”。它们参与模块事实的创建。构建阶段最常见的四个钩子是:
| 钩子 | 输入 | 输出 | 适合做 | 不适合做 |
|---|---|---|---|---|
resolveId | source、importer、解析选项 | 模块 id、external 标记或 null | alias、虚拟模块、peer external、条件导出处理 | 读取并转换完整源码 |
load | 已解析 id | 源码字符串、AST、map 或 null | 生成虚拟模块、读取非标准资源 | 根据源码内容决定 chunk 结构 |
transform | 当前模块源码和 id | { code, map } 或字符串 | TS/JSX/CSS 转换、注入开发标记 | 访问最终文件名、改其它 chunk |
moduleParsed | 解析后的模块信息 | 无直接源码输出 | 收集 imports/exports、告警循环依赖或公共 API | 修改模块代码 |
一个最小虚拟模块插件:
export function virtualBuildInfo() {
const virtualId = 'virtual:build-info';
const resolvedId = '\0virtual:build-info';
return {
name: 'virtual-build-info',
resolveId(source) {
if (source === virtualId) return resolvedId;
return null;
},
load(id) {
if (id === resolvedId) {
return 'export const builtAt = "2026-07-08";';
}
return null;
},
transform(code, id) {
if (!id.endsWith('.ts')) return null;
return { code, map: null };
},
moduleParsed(info) {
if (info.isEntry) this.warn(`entry: ${info.id}`);
},
};
}错误完整流程:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
| alias 后同一模块进了两份 | resolveId 返回的 id 不稳定,或保留了不同 query/路径大小写 | 在插件中打印 source → id,并用 manifest/stats 搜索重复模块 | 规范化路径和 query;对包名使用同一入口条件 |
| DevTools 行号错 | transform 返回了改写代码但没有正确 map | 解析最终 .map.sources 和 mappings | 用 MagicString 或转换器生成 map,并向下游传递上游 map |
| 虚拟模块出现在磁盘路径里 | 虚拟 id 没使用 \0 前缀,和真实文件解析混淆 | 打印 id,搜索输出 source map | 按 Rollup 约定使用内部虚拟 id,并在 map 中提供可读 source |
输出钩子:renderChunk/generateBundle/writeBundle 决定最终产物
当模块图已经完成,Rollup 进入输出阶段。三个钩子职责差异很大:
| 钩子 | 运行时机 | 能看见什么 | 常见用途 | 严重误用 |
|---|---|---|---|---|
renderChunk | 每个 chunk 渲染时 | 单个 chunk 的 code、chunkInfo、output options | 给 chunk 加 banner、改 dynamic import 表达式、追加局部 map | 读取最终 hash 文件名;在这里生成全局 manifest |
generateBundle | 所有 chunk/asset 生成后、写入前 | 完整 bundle 对象 | 生成 manifest.json、改 HTML、删除或增加资产 | 依赖磁盘已写入;异步写外部文件不纳入 Rollup 错误 |
writeBundle | bundle.write() 写入磁盘后 | 已写出的 bundle 清单 | 上传产物、打印报告、触发后处理 | 再修改已写文件但不更新 map/hash/manifest |
共享示例的 manifest() 插件使用 generateBundle,因为它需要完整 bundle:
function manifest() {
return {
name: 'manifest',
generateBundle(_options, bundle) {
const entries = {};
for (const chunk of Object.values(bundle)) {
if (chunk.type !== 'chunk' || !chunk.facadeModuleId) continue;
entries[toSourceName(chunk.facadeModuleId)] = {
file: chunk.fileName,
src: toSourceName(chunk.facadeModuleId),
isEntry: chunk.isEntry,
isDynamicEntry: chunk.isDynamicEntry,
};
}
this.emitFile({
type: 'asset',
fileName: 'manifest.json',
source: `${JSON.stringify(entries, null, 2)}\n`,
});
},
};
}预期结果是 manifest.json 至少能描述 src/main.ts 的 entry 和 src/detail.ts 的 dynamic entry。它不是给浏览器直接加载的脚本,而是给测试、服务端模板或发布系统使用的机器证据。
Tree Shaking 与 moduleSideEffects:不是“删掉没被 import 的文本”
Rollup 的 Tree Shaking 依赖 ESM 的两个特点:导入导出在语法层可见,live binding 让导出关系可以被追踪。它会尝试删除未使用导出,但不能随便跳过有副作用的模块。
共享源码里有两个观察点:
// catalog.ts
export const removableMarker = 'SHOULD_NOT_REACH_BUNDLE';
// side-effect.ts
globalThis.__BUILD_TOOLS_SIDE_EFFECT__ = 'preserved';removableMarker 没被使用,生产 bundle 不应包含 SHOULD_NOT_REACH_BUNDLE。side-effect.ts 虽然没有被读取导出,但它改变了 globalThis,必须保留。示例配置选择保守策略:
export default {
treeshake: {
moduleSideEffects: true,
},
};取舍表:
| 策略 | 性能/体积 | 可靠性 | 使用难度 | 维护成本 | 适用规模 |
|---|---|---|---|---|---|
moduleSideEffects: true | 体积收益较保守 | 不易误删副作用 | 低 | 低 | 应用、小型示例、迁移初期 |
moduleSideEffects: false | 体积可能更小 | 极易误删 CSS、注册、polyfill | 高 | 高 | 只适合可证明纯模块集合 |
| 函数式判断 | 可兼顾体积和语义 | 取决于规则覆盖 | 中高 | 中高 | 大型库、monorepo、需要精确 external/side effect 策略 |
package sideEffects 数组 | 方便交给消费者复用 | 配错会影响所有打包器 | 中 | 中 | npm 库发布 |
严重问题完整流程:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
页面显示 副作用标记:undefined | side-effect.ts 被错误摇掉 | rg "preserved" examples/build-tools/dist/rollup | 保守使用 moduleSideEffects: true,或把副作用文件列入白名单 |
| 无用标记仍在产物里 | CommonJS、动态访问或 minifier/treeshake 未生效 | rg "SHOULD_NOT_REACH_BUNDLE" examples/build-tools/dist/rollup | 保留 ESM,减少动态导出访问,确认 production/minify |
| CSS 消失 | 把 CSS 导入当作无副作用模块 | 检查 index.html 是否有 stylesheet link | CSS 文件或 CSS 导入所在模块必须标记为有副作用 |
external、peer 依赖和库发布边界
应用构建通常倾向“能打进 bundle 就打进 bundle”。库构建相反:凡是消费者应该自己提供的运行时,尤其 peerDependencies,通常要 external。否则消费者会同时得到两份框架运行时,出现 context、hooks、样式单例或全局注册不一致。
最小 external 策略:
import pkg from './package.json' with { type: 'json' };
const peerNames = new Set(Object.keys(pkg.peerDependencies ?? {}));
export default {
input: {
index: 'src/index.ts',
button: 'src/button.ts',
},
external(id) {
const name = id.startsWith('@')
? id.split('/').slice(0, 2).join('/')
: id.split('/')[0];
return peerNames.has(name);
},
output: [
{ dir: 'dist/es', format: 'es', sourcemap: true },
{ dir: 'dist/cjs', format: 'cjs', sourcemap: true, exports: 'named' },
],
};external 的边界:
| 依赖类型 | 常见策略 | 判断依据 |
|---|---|---|
peerDependencies | 默认 external | 消费者必须共用单例或自行选择版本 |
| 小型纯工具函数 | 可打包 | 不暴露全局状态,升级风险低 |
| CSS runtime、polyfill | 谨慎 external | 需要明确加载顺序和浏览器支持 |
| Node 内置模块 | 浏览器库通常不能打包 | 应分清 browser/node 条件导出 |
严重问题完整流程:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
| React hooks 报多实例错误 | peer 被打进库 bundle | rg "react" dist/es dist/cjs 并检查 bundle 内容 | 从 peer 生成 external,发布前跑消费者 fixture |
浏览器端报 process is not defined | Node 条件或 external 指向了 Node-only 入口 | 查看 package exports condition 和产物 import | 配置 browser/import/default 条件,或拆分 browser/node 入口 |
| UMD/IIFE 消费时报全局变量不存在 | external 了依赖但没给 output.globals | 打开 UMD 产物看参数和全局访问 | 对 UMD/IIFE external 配 globals,或不发布该格式 |
多入口、ESM/CJS 与 package exports 必须一起设计
Rollup 的 input 可以是字符串、数组或对象。库构建推荐对象形式,因为它让入口名、文件名和 package exports 更容易对齐。
export default {
input: {
index: 'src/index.ts',
'components/button': 'src/components/button.ts',
},
output: [
{
dir: 'dist/es',
format: 'es',
entryFileNames: '[name].js',
sourcemap: true,
},
{
dir: 'dist/cjs',
format: 'cjs',
entryFileNames: '[name].cjs',
sourcemap: true,
},
],
};对应的 package exports:
{
"type": "module",
"exports": {
".": {
"import": "./dist/es/index.js",
"require": "./dist/cjs/index.cjs",
"types": "./dist/types/index.d.ts"
},
"./components/button": {
"import": "./dist/es/components/button.js",
"require": "./dist/cjs/components/button.cjs",
"types": "./dist/types/components/button.d.ts"
}
}
}package exports 的常见坑:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
| Node ESM 能 import,CJS require 失败 | require 条件指向 .js 且包为 "type": "module" | node -e "console.log(require('./pkg'))" | CJS 产物用 .cjs,exports require 指向 .cjs |
| 深层路径在本地能用,发布后不能用 | 文件存在但 exports 未暴露 | node -e "import('./pkg/components/button')" | 显式列出子路径,或使用经过审查的 pattern exports |
| 类型声明和 JS 入口不一致 | DTS 输出结构与 Rollup entryFileNames 不同 | tsc --traceResolution 或编辑器跳转 | 让声明生成目录跟 entry 命名同步 |
dynamic import、manualChunks 和 HTML 加载边界
main.ts 中的动态导入:
button.addEventListener('click', async () => {
const { renderDetail } = await import('./detail.ts');
renderDetail(detail, tool);
});它的含义不是“HTML 应该多加一个 <script>”。正确链路是:HTML 加载 initial JS,initial JS 运行到点击时再通过浏览器 dynamic import 请求 async chunk。
manualChunks 可以改变 chunk 归组:
export default {
output: {
manualChunks(id) {
if (id.includes('node_modules')) return 'vendor';
if (id.endsWith('/src/detail.ts')) return 'detail';
return null;
},
},
};取舍表:
| 方案 | 性能 | 可靠性 | 使用难度 | 维护成本 | 适用规模 |
|---|---|---|---|---|---|
| 让 Rollup 自动分块 | 请求数和共享拆分由工具决定 | 最少人为错误 | 低 | 低 | 小到中型库、示例 |
| 简单 vendor chunk | 长缓存可能更稳定 | vendor 过大影响首屏 | 中 | 中 | 应用型构建或文档站 |
精细 manualChunks | 可按路由/功能控制缓存 | 容易造成重复依赖和执行顺序问题 | 高 | 高 | 大型应用,需配合真实网络数据 |
严重问题完整流程:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
index.html 直接加载 detail.*.js | HTML 插件没有过滤 async chunk | rg "detail" examples/build-tools/dist/rollup/index.html | HTML 只注入 file.type === "chunk" && file.isEntry 的 JS |
| 点击后 async chunk 404 | 相对路径、base 或部署目录错 | 构建后静态预览并点击按钮 | 使用相对资源路径,部署时保留目录结构 |
改 manualChunks 后副作用顺序改变 | 把有副作用模块拆入提前/延后执行的 chunk | 在产物里搜索副作用模块并手动预览 | 不手动移动副作用模块,或增加运行时回归测试 |
Source Map 链:插件要把上游位置传下去
共享配置里有多段转换:TypeScript 插件处理 .ts,PostCSS 处理 CSS,terser 压缩 JS,Rollup 最终输出外部 Source Map。
export default {
plugins: [
typescript({ compilerOptions: { sourceMap: true, target: 'ES2022' } }),
postcss({ extract: 'assets/styles.css', minimize: true, sourceMap: true }),
terser(),
],
output: {
sourcemap: true,
},
};Source Map 的链路要求是:每个阶段要么不改代码,要么返回能映射回上游的 map。只要某个插件返回 map: null 但实际改了行列,后续 map 仍可能存在,却已经失真。
| 观察点 | 预期结果 |
|---|---|
主 JS .map | sources 能追溯到 main.ts |
async JS .map | sources 能追溯到 detail.ts |
CSS .map | sources 能追溯到 styles.css |
| HTML | 不泄露 file: 或工作区绝对路径 |
错误完整流程:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
| DevTools 定位到压缩后代码 | terser 或前置插件 map 链断裂 | 打开 .js.map 检查 sources 和 mappings | 确认每个转换插件启用 source map |
| CSS map 指向绝对路径 | CSS 插件使用本机路径作为 source | `rg "$PWD | file:" examples/build-tools/dist/rollup` |
| map 文件存在但浏览器 404 | JS 尾部 sourceMappingURL 和部署文件名不一致 | tail -n 2 assets/*.js | 保证 .map 与 JS 同版本发布,或使用 hidden map |
watch cache、循环依赖和构建时信任边界
Rollup watch 的快来自复用前一次构建图和插件状态,但它不会猜到插件读取了哪些外部文件。插件读模板、生成 HTML、读 .env 或读某个 JSON 时,需要登记依赖:
function htmlTemplate(templateFile) {
return {
name: 'html-template',
buildStart() {
this.addWatchFile(templateFile);
},
generateBundle() {
// 读取 templateFile 并 emit HTML。
},
};
}循环依赖不是 Rollup 独有问题。ESM live binding 支持循环引用,但初始化顺序仍然重要:
// a.js
import { b } from './b.js';
export const a = b + 1;
// b.js
import { a } from './a.js';
export const b = a + 1; // 读取尚未初始化的 a,可能触发 TDZ 或 undefined 风险处理原则:
| 问题 | 观察入口 | 修正 |
|---|---|---|
| watch 不响应模板变化 | 改模板文件,看 Rollup 是否 rebuild | this.addWatchFile,缓存 key 纳入模板内容或 mtime |
| 循环依赖 warning | 构建日志、onwarn 或 onLog | 抽出第三个无状态模块,避免顶层互读 |
| 插件泄露密钥 | 审查插件代码、CI 日志、产物搜索 process.env 值 | 最小 CI 权限,不在插件中处理秘密,锁定依赖 |
| cache 命中旧输出 | 修改 config/env 后产物不变 | 清 cache 对比;把 env/config/plugin version 计入 key |
共享示例:Rollup 构建浏览器入口
运行命令:
pnpm --filter @roadmap/build-tools-slice build:rollup输入:
examples/build-tools/src/main.ts
examples/build-tools/src/catalog.ts
examples/build-tools/src/detail.ts
examples/build-tools/src/side-effect.ts
examples/build-tools/src/styles.css预期结果:
examples/build-tools/dist/rollup/index.html
examples/build-tools/dist/rollup/manifest.json
examples/build-tools/dist/rollup/assets/*.js
examples/build-tools/dist/rollup/assets/*.css
examples/build-tools/dist/rollup/assets/*.js.map
examples/build-tools/dist/rollup/assets/*.css.map验证观察:
find examples/build-tools/dist/rollup -type f | sort
node --test --test-name-pattern rollup examples/build-tools/test/artifacts.test.mjs预期输出要点:
| 观察点 | 预期结果 |
|---|---|
index.html | 只通过相对 script type="module" 加载 entry JS,通过 link rel="stylesheet" 加载 CSS |
manifest.json | 有 src/main.ts entry,也有 src/detail.ts 的 isDynamicEntry: true entry |
| dynamic JS | 由浏览器 dynamic import 加载,不由 HTML 直接加载 |
| bundled JS | 包含 Babel 和 preserved,不包含 SHOULD_NOT_REACH_BUNDLE |
| Source Map | JS map 能追溯 main.ts/detail.ts,CSS map 能追溯 styles.css |
如果只看到 Rollup 命令退出 0,还不能证明产物正确;至少要检查 HTML 引用、manifest、async chunk、CSS、Source Map 和可观察副作用。
Rollup 2 到 4:迁移和重大变化
从 Rollup 2、3 迁移到 4,改配置文件扩展名只是表面工作。真正影响工程的是默认值、插件 API、输出 hash 和 native 包。
| 变化 | Rollup 2 常见状态 | Rollup 3/4 状态 | 迁移动作 |
|---|---|---|---|
| Node 版本 | 老项目可能仍在 Node 12/14/16 | Rollup 4 要求至少 Node 18 | CI、本地和发布脚本统一升级 |
| native optional dependency | 主要是 JS 包心智模型 | Rollup 4 安装平台相关 optional native 包,不支持平台可用 WASM 替代 | 不要在 CI 禁掉 optionalDependencies;镜像源要保留平台包 |
| 默认值 | 依赖旧 preserveEntrySignatures、interop 等行为 | Rollup 3/4 调整多个推荐默认值 | 先在 3.29.x 开 strictDeprecations,再升 4 |
| CJS 动态导入 | 旧输出可能把动态导入改写成 require 包装 | Rollup 3 起 CJS 输出默认保留 external dynamic import | 老 Node 目标才考虑 output.dynamicImportInCjs: false |
| 输出钩子/hash | 插件可能在 renderChunk 读取最终文件名 | Rollup 4 hash 基于 renderChunk 后内容,hook 中可能是占位符 | 需要最终文件名的逻辑放到 generateBundle |
| 文件名 placeholder | 旧插件可能使用 [ext] 等 placeholder | preserveModules + entryFileNames 下部分 placeholder 不再可用 | 按迁移指南调整命名函数 |
迁移严重问题完整流程:
| 症状 | 根因 | 观察命令 | 修正 |
|---|---|---|---|
| CI 安装成功但运行 Rollup 报平台包缺失 | npm/pnpm 配置跳过 optional dependency 或镜像缺包 | pnpm why rollup、检查 lockfile optional packages | 允许 optionalDependencies;不支持平台改 @rollup/wasm-node |
| 升级后 manifest 文件名是占位符 | 插件在 renderChunk 记录 hash 文件名 | 打印 chunk.fileName 所在钩子 | 把记录最终文件名移动到 generateBundle |
| CJS 消费端动态 import 行为变了 | output.dynamicImportInCjs 默认语义变化 | 对 CJS 产物执行消费者 fixture | 明确 Node 版本目标,必要时设置该选项 |
| 大型配置升级后报 deprecation | 直接从 Rollup 2 跳 4 | 开 strictDeprecations 在 Rollup 3.29.x 先跑 | 分阶段升级插件和配置 |