Rollup 打包工具

107 阅读10分钟

Rollup 是一款基于 ES 模块(ESM)的 JavaScript 打包工具,专注于构建高性能、轻量的库或应用,尤其适合库的开发。

本文后续示例 rollup 版本4.59.0

rullop 核心特点

  1. 基于 ESM:原生支持 ES6 模块语法(import/export),对模块依赖处理更高效。
  2. 强大的 Tree-shaking:静态分析代码,移除未被引用的 exports,减少打包体积(Webpack 也支持,但 Rollup 更早原生实现)。
  3. 简洁的输出:默认生成无冗余代码的 bundle,更接近手写代码,可读性高。
  4. 多输出格式:支持输出多种模块格式,如 es(ES 模块)、cjs(CommonJS)、umd(通用模块定义)、iife(立即执行函数)等,方便库在不同环境使用。
  5. 插件生态:通过插件扩展功能(如处理 CSS、图片、转换 TypeScript 等),但生态规模小于 Webpack。

Rollup 打包核心流程

Rollup 的打包过程可分为 初始化 → 构建 → 输出 三大模块,整体流程如下:

image.png

rollup 基本配置

rollup-4.52.5/src/rollup/rollup.ts

function rollup(rawInputOptions: RollupOptions): Promise<RollupBuild> {
  return rollupInternal(rawInputOptions, null);
}
interface RollupOptions extends InputOptions {
  output?: OutputOptions | OutputOptions[] | undefined;
}
interface InputOptions {
	cache?: boolean | RollupCache | undefined; // 启用打包缓存
	context?: string | undefined; // 定义模块的 this 上下文
	experimentalCacheExpiry?: number | undefined; // 缓存过期时间(秒)
	experimentalLogSideEffects?: boolean | undefined; // 记录模块的副作用信息
	external?: ExternalOption | undefined; // 标记「不打包」的依赖
	fs?: RollupFsModule | undefined; // 自定义文件系统模块
	input?: InputOption | undefined; // 指定打包入口文件
	jsx?: false | JsxPreset | JsxOptions | undefined; // 内置 JSX 处理配置
	logLevel?: LogLevelOption | undefined; // 控制日志输出级别
  // 让绝对路径的外部依赖转为相对路径
	makeAbsoluteExternalsRelative?: boolean | 'ifRelativeSource' | undefined;
  // 限制并行文件操作数量
	maxParallelFileOps?: number | undefined;
  // 为特定模块自定义 this 上下文
	moduleContext?: ((id: string) => string | NullValue) | Record<string, string> | undefined;
	onLog?: LogHandlerWithDefault | undefined;
	onwarn?: WarningHandlerWithDefault | undefined; // 自定义警告处理逻辑
	perf?: boolean | undefined; // 输出性能分析数据
	plugins?: InputPluginOption | undefined;
  // 保留导出签名
	preserveEntrySignatures?: PreserveEntrySignaturesOption | undefined;
	preserveSymlinks?: boolean | undefined; // 保留符号链接(软链接)
  // 为缺失的导出自动生成空垫片
	shimMissingExports?: boolean | undefined;
	strictDeprecations?: boolean | undefined; // 严格模式(弃用 API 直接报错)
  // 控制 Tree-shaking 行为
	treeshake?: boolean | TreeshakingPreset | TreeshakingOptions | undefined;
  // 配置 watch 模式(热更新)
	watch?: WatcherOptions | false | undefined;
}

input 配置

type InputOption = string | string[] | Record<string, string>;

external 配置

type ExternalOption =
	| (string | RegExp)[]
	| string
	| RegExp
	| ((source: string, importer: string | undefined, isResolved: boolean) => boolean | NullValue);

treeshake 配置

注意:Tree Shaking 仅对 ES 模块(import/export)有效,CommonJS 模块(require)因动态特性无法被摇树。

treeshake?: boolean | TreeshakingPreset | TreeshakingOptions | undefined;

type TreeshakingPreset = 'smallest' | 'safest' | 'recommended';
  • recommended,平衡体积和安全性,保留必要副作用
  • smallest,极致摇树,尽可能剔除代码(可能误删副作用)
  • safest,保守摇树,避免误删代码(体积稍大)
interface NormalizedTreeshakingOptions {
	// 尊重代码中的 /*#__PURE__*/ 注释(标记无副作用的函数)
	annotations: boolean;
	// 是否修正「变量声明前使用」的逻辑
	correctVarValueBeforeDeclaration: boolean;
	// 手动标记的纯函数列表
	manualPureFunctions: readonly string[];
	// 控制模块级别的副作用判断
	moduleSideEffects: HasModuleSideEffects;
	// 属性读取是否有副作用
	propertyReadSideEffects: boolean | 'always';
	// try/catch 块是否禁用摇树(关闭可优化死代码剔除)
	tryCatchDeoptimization: boolean;
	// 全局变量操作是否有副作用
	unknownGlobalSideEffects: boolean;
}

jsx 配置

jsx 配置项,是 Rollup v4.20+ 新增的实验性内置 JSX 处理能力

  • false, 禁用 Rollup 内置 JSX 处理(默认值,兼容历史行为)。
jsx?: false | JsxPreset | JsxOptions | undefined; // 内置 JSX 处理配置
  • JsxPreset,使用内置预设快速配置,无需自定义参数
type JsxPreset = 'react' | 'react-jsx' | 'preserve' | 'preserve-react';
  • JsxOptions,精细化配置 JSX 编译规则(优先级最高)
type JsxOptions = Partial<NormalizedJsxOptions> & {
	preset?: JsxPreset | undefined;
};

interface NormalizedJsxAutomaticOptions {
    // JSX 编译后的工厂函数
	factory: string;
    // JSX 运行时导入源(如 'react' / 'preact')
	importSource: string | null;
    // 最终生效的 JSX 运行时导入源
	jsxImportSource: string;
    // 固定值:标识当前 JSX 运行时模式
	mode: 'automatic';
}

watch 配置

watch?: WatcherOptions | false | undefined;
interface WatcherOptions {
	// 允许「输入文件(源码)」放在「输出目录(dist)」中
	allowInputInsideOutputPath?: boolean | undefined;
	// 文件变更后,延迟 N 毫秒再触发重新打包
	buildDelay?: number | undefined;
	// 传递给底层监听库 chokidar 的配置
	chokidar?: ChokidarOptions | undefined;
	// 重新打包时是否清空终端屏幕
	clearScreen?: boolean | undefined;
	// 排除不需要监听的文件 / 目录
	exclude?: string | RegExp | (string | RegExp)[] | undefined;
	// 指定需要监听的文件 / 目录
	include?: string | RegExp | (string | RegExp)[] | undefined;
	// 重新打包时是否跳过「写入文件到磁盘」
	skipWrite?: boolean | undefined;
	// 文件失效(变更 / 删除)时的回调函数
	onInvalidate?: ((id: string) => void) | undefined;
}
interface ChokidarOptions {
	// 是否每次触发事件时都调用 fs.stat() 获取文件信息
	alwaysStat?: boolean | undefined;
	// 处理「原子写入」的文件事件
	atomic?: boolean | number | undefined;
	// 等待文件「写入完成」后再触发事件
	awaitWriteFinish?:
	 | {
	     pollInterval?: number | undefined; // 检查文件大小的间隔(ms),默认 100
	     stabilityThreshold?: number | undefined; // 文件大小稳定的时间(ms),默认 2000
	 }
	 | boolean
	 | undefined;
	// 监听二进制文件的轮询间隔(ms)
	binaryInterval?: number | undefined;
	// 监听的工作目录(相对路径的基准)
	cwd?: string | undefined;
	// 监听目录的深度(递归层级)
	depth?: number | undefined;
	// 是否禁用 glob 路径解析
	disableGlobbing?: boolean | undefined;
	// 是否跟随符号链接
	followSymlinks?: boolean | undefined;
	// 是否忽略初始扫描
	ignoreInitial?: boolean | undefined;
	// 是否忽略文件权限错误
	ignorePermissionErrors?: boolean | undefined;
	// 忽略不需要监听的文件 / 目录
	ignored?: any | undefined;
	// 轮询间隔(ms),仅 usePolling: true 时生效
	interval?: number | undefined;
	// 监听进程是否持续运行(不退出)
	persistent?: boolean | undefined;
	// 是否使用 macOS 原生的 fsevents 模块
	useFsEvents?: boolean | undefined;
	// 是否使用轮询模式
	usePolling?: boolean | undefined;
}

output 配置

interface OutputOptions {
  
	amd?: AmdOptions | undefined; // 自定义 AMD 模块配置
  // 静态资源(CSS / 图片等)的命名
	assetFileNames?: string | ((chunkInfo: PreRenderedAsset) => string) | undefined;
	banner?: string | AddonFunction | undefined; // 产物头部添加内容
  // 动态导入 / 代码分割的 chunk 命名
	chunkFileNames?: string | ((chunkInfo: PreRenderedChunk) => string) | undefined;
	compact?: boolean | undefined; // 简单压缩代码
	// only required for bundle.write
	dir?: string | undefined; // 输出目录路径 (多文件输出)
  // 在 CJS 输出中使用原生 import(),而非 Rollup 兼容封装
	dynamicImportInCjs?: boolean | undefined;
  // 入口 chunk 的文件名规则
	entryFileNames?: string | ((chunkInfo: PreRenderedChunk) => string) | undefined;
	esModule?: boolean | 'if-default-prop' | undefined; // 控制 ES 模块标记
  // 实验性:避免生成过小的 chunk(优化网络请求)
	experimentalMinChunkSize?: number | undefined; // 最小 chunk 大小	
  // 控制导出模式
	exports?: 'default' | 'named' | 'none' | 'auto' | undefined;
	extend?: boolean | undefined; // 扩展全局变量
	/** @deprecated Use "externalImportAttributes" instead. */
	externalImportAssertions?: boolean | undefined; // 导入断言(已废弃)
	externalImportAttributes?: boolean | undefined; // 启用导入属性

  // 是否为 external 依赖生成 “活绑定”,true 更符合 ESM 规范
	externalLiveBindings?: boolean | undefined;
	// only required for bundle.write
	file?: string | undefined;  // 输出文件路径
	footer?: string | AddonFunction | undefined; // 产物尾部添加内容
  
	format?: ModuleFormat | undefined; // 指定输出模块格式
 	freeze?: boolean | undefined; // 是否使用 Object.freeze 冻结导出对象(默认 true)
  
  // 控制生成代码的版本:ES5、ES6、箭头函数等语法级别
	generatedCode?: GeneratedCodePreset | GeneratedCodeOptions | undefined;
  
	globals?: GlobalsOption | undefined; // UMD/CJS 中外部依赖的全局变量映射
	hashCharacters?: HashCharacters | undefined; // 自定义 content hash 用的字符集
  // 把间接依赖的 import 提升到顶层,减少嵌套、提升运行效率
	hoistTransitiveImports?: boolean | undefined;
  // 自定义导入属性 key,如 assert / with
	importAttributesKey?: ImportAttributesKey | undefined;
	indent?: string | boolean | undefined; // 缩进格式
  // 把动态导入直接内联,不做代码分割
	inlineDynamicImports?: boolean | undefined;
	interop?: InteropType | GetInterop | undefined; // 模块互操作方式
	intro?: string | AddonFunction | undefined; // 模块内部添加内容
  
	manualChunks?: ManualChunksOption | undefined; // 自定义代码分割
  // 压缩内部导出名称,缩短变量名
	minifyInternalExports?: boolean | undefined;
	name?: string | undefined; // UMD/iife 格式的全局变量名
	noConflict?: boolean | undefined; // 添加 noConflict 方法
	/** @deprecated This will be the new default in Rollup 5. */
  // 弃用提示:Rollup 5 会成为默认值,禁用自动分割
	onlyExplicitManualChunks?: boolean | undefined; // 仅使用显式手动分割
	outro?: string | AddonFunction | undefined; // 模块尾部添加内容
	paths?: OptionsPaths | undefined; // 自定义导入路径
	plugins?: OutputPluginOption | undefined;
	preserveModules?: boolean | undefined; // 保留原模块结构
	preserveModulesRoot?: string | undefined; // 保留模块结构的根目录
  // 是否从外部模块重新导出原型
	reexportProtoFromExternal?: boolean | undefined;
  // 清理非法文件名
	sanitizeFileName?: boolean | ((fileName: string) => string) | undefined;
  
  // 生成源码映射(Sourcemap)
	sourcemap?: boolean | 'inline' | 'hidden' | undefined;
	sourcemapBaseUrl?: string | undefined; // 源码映射的基础 URL
  
	sourcemapExcludeSources?: boolean | undefined; // 排除源码内容
	sourcemapFile?: string | undefined; // 指定 sourcemap 内部引用的源文件名
  // 自定义生成的 .map 文件名
	sourcemapFileNames?: 
  string | ((chunkInfo: PreRenderedChunk) => string) | undefined;
  // 让浏览器 devtools 忽略某些 sourcemap
	sourcemapIgnoreList?: boolean | SourcemapIgnoreListOption | undefined;
  // 转换源码路径
	sourcemapPathTransform?: SourcemapPathTransformOption | undefined;
	sourcemapDebugIds?: boolean | undefined; // 给 sourcemap 添加稳定 debug ID
  
	strict?: boolean | undefined; // 产物是否加 'use strict'(默认 true)
  // SystemJS 格式:使用空 setter,优化加载
	systemNullSetters?: boolean | undefined;
  
	validate?: boolean | undefined; // 对最终打包代码做校验,防止语法错误
	virtualDirname?: string | undefined; // 为虚拟模块设置 __dirname
}

format

type ModuleFormat = InternalModuleFormat | 'commonjs' | 'esm' | 'module' | 'systemjs';
type InternalModuleFormat = 'amd' | 'cjs' | 'es' | 'iife' | 'system' | 'umd';

manualChunks

manualChunks?: ManualChunksOption | undefined; // 自定义代码分割
// Record<string, string[]> 静态映射(chunk名 → 模块列表)
type ManualChunksOption = Record<string, string[]> | GetManualChunk;

// 动态函数(根据模块ID返回chunk名)
type GetManualChunk = (
id: string, // 当前模块的唯一ID(文件路径/模块名)
meta: ManualChunkMeta // 模块元信息(获取所有模块/模块详情)
) => string | NullValue;

interface ManualChunkMeta {
    // 获取所有参与打包的模块ID
	getModuleIds: () => IterableIterator<string>;
	getModuleInfo: GetModuleInfo;
}

type GetModuleInfo = (moduleId: string) => ModuleInfo | null;
interface ModuleInfo extends ModuleOptions {
	ast: ProgramNode | null; // 模块的抽象语法树(AST)
	code: string | null; // 模块处理后的代码(插件转换后)
	dynamicImporters: readonly string[]; // 动态导入当前模块的模块ID(import())
	dynamicallyImportedIdResolutions: readonly ResolvedId[]; // 动态导入模块的解析详情
	dynamicallyImportedIds: readonly string[]; // 当前模块动态导入的模块ID
	exportedBindings: Record<string, string[]> | null; // 导出名对应的变量名(解决重命名)
	exports: string[] | null; // 当前模块的所有导出名称
	safeVariableNames: Record<string, string> | null; // 安全变量名(避免冲突)
	hasDefaultExport: boolean | null; // 是否有默认导出
	id: string; // 模块唯一ID(绝对路径/裸模块名)
	implicitlyLoadedAfterOneOf: readonly string[]; // 隐式加载依赖(高级)
	implicitlyLoadedBefore: readonly string[]; // 隐式被依赖(高级)
	importedIdResolutions: readonly ResolvedId[]; // 导入模块的解析详情(路径/是否外部)
	importedIds: readonly string[]; // 当前模块导入的所有模块ID
	importers: readonly string[]; // 引用当前模块的所有模块ID(反向依赖)
	isEntry: boolean;  // 是否是入口模块
	isExternal: boolean;  // 是否是外部依赖(如external配置的模块)
	isIncluded: boolean | null; // 是否被包含在最终产物中
}
interface ModuleOptions {
	// 模块属性
	attributes: Record<string, string>;
	// 插件自定义元数据(插件间共享数据)
	meta: CustomPluginOptions;
	// 模块级副作用控制(Tree Shaking 核心)
	moduleSideEffects: boolean | 'no-treeshake';
	// 合成命名导出(兼容 CommonJS/默认导出)
	syntheticNamedExports: boolean | string;
}

hashCharacters

hashCharacters?: HashCharacters | undefined; // 自定义 content hash 用的字符集
type HashCharacters = 'base64' | 'base36' | 'hex';
  • base64,字符集(A-Z、a-z、0-9、+、/) 最短(6 位≈hex 8 位)
  • base36,字符集(0-9、a-z(小写字母)) 中等(7 位≈hex 8 位)
  • hex,字符集(0-9、a-f(小写十六进制)) 最长(8 位)

image.png

plugins 配置

type InputPluginOption = 
	MaybePromise< // 支持同步/异步插件(Promise 包裹)
	Plugin | // 单个合法插件实例(核心)
	NullValue | // null/undefined(无插件)
	false | // false(禁用该插件)
	InputPluginOption[] // 嵌套数组(支持多层插件列表)
	>;

应用

第一步: 安装 rollup 依赖

# 安装
npm install rollup --save-dev

第二步 配置文件 rollup.config.js(ts、cjs、mjs)

执行命令

rollup -c

vue3 项目使用 rollup 构建

import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import replace from '@rollup/plugin-replace'
import vue from 'rollup-plugin-vue'
import postcss from 'rollup-plugin-postcss'
import html from '@rollup/plugin-html'
import alias from '@rollup/plugin-alias'
import copy from 'rollup-plugin-copy'
import esbuild from 'rollup-plugin-esbuild'
import path from 'path'
import { fileURLToPath } from 'url'
import fs from 'fs'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

export default {
  input: 'src/main.ts',
  output: {
    dir: 'dist-rollup',
    format: 'esm',
    entryFileNames: 'assets/[name]-[hash].js',
    chunkFileNames: 'assets/[name]-[hash].js',
    assetFileNames: 'assets/[name]-[hash].[ext]',
    manualChunks(id) {
      // Vue 核心库
      if (id.includes('node_modules/vue/') || id.includes('node_modules/vue-router/')) {
        return 'vue-vendor'
      }
      // TDesign UI 组件库
      if (id.includes('node_modules/tdesign-vue-next/')) {
        return 'tdesign'
      }
      // wangEditor 富文本编辑器核心
      if (id.includes('node_modules/@wangeditor/editor/')) {
        return 'wangeditor'
      }
      // wangEditor Vue 组件
      if (id.includes('node_modules/@wangeditor/editor-for-vue/')) {
        return 'wangeditor-vue'
      }
      // 其他第三方库
      if (id.includes('node_modules/')) {
        return 'vendor'
      }
    }
  },
  plugins: [
    alias({
      entries: [
        { find: '@', replacement: path.resolve(__dirname, 'src') }
      ]
    }),
    replace({
      preventAssignment: true,
      'process.env.NODE_ENV': JSON.stringify('production')
    }),
    resolve({
      extensions: ['.js', '.ts', '.vue', '.json'],
      browser: true
    }),
    commonjs(),
    vue({
      preprocessStyles: true,
      target: 3.3
    }),
    esbuild({
      include: /\.[jt]s$/,
      exclude: /node_modules/,
      sourceMap: false,
      minify: true,
      target: 'es2020',
      tsconfig: './tsconfig.json',
      loaders: {
        '.ts': 'ts'
      }
    }),
    postcss({
      extract: true,
      minimize: true,
      sourceMap: false
    }),
    html({
      template: ({ files, publicPath }) => {
        const htmlContent = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8')

        // 生成 CSS link 标签
        const cssLinks = (files.css || [])
          .map(file => `<link rel="stylesheet" href="${publicPath}${file.fileName}">`)
          .join('\n    ')

        // 生成 JS script 标签
        const jsScripts = (files.js || [])
          .map(file => `<script type="module" src="${publicPath}${file.fileName}"></script>`)
          .join('\n    ')

        // 注入到 HTML 中
        let result = htmlContent

        // 在 </head> 前注入 CSS
        if (cssLinks) {
          result = result.replace('</head>', `    ${cssLinks}\n  </head>`)
        }

        // 替换原始的 script 标签
        if (jsScripts) {
          result = result.replace(
            /<script\s+type="module"\s+src="\/src\/main\.ts"><\/script>/,
            jsScripts
          )
        }

        return result
      },
      fileName: 'index.html'
    }),
    copy({
      targets: [
        { src: 'public/*', dest: 'dist-rollup' }
      ]
    })
  ],
  external: []
}

利用 vite prevow 预览

vue3-td-vite3/dist-rollup/index.html

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue3 + TDesign + Vite5</title>
      <link rel="stylesheet" href="main-YZQn53Mw.css">
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="assets/vendor-BV7q6UrS.js"></script>
    <script type="module" src="assets/vue-vendor-BmtzWZcf.js"></script>
    <script type="module" src="assets/tdesign-CY37z0cm.js"></script>
    <script type="module" src="assets/wangeditor-BMB8Vv3g.js"></script>
    <script type="module" src="assets/wangeditor-vue-D_SInJX7.js"></script>
    <script type="module" src="assets/main-YZQn53Mw.js"></script>
    <script type="module" src="assets/UserInfo-94zOu32Z.js"></script>
    <script type="module" src="assets/ArticleList-CKU63nuZ.js"></script>
    <script type="module" src="assets/CreateArticle-BB2385U9.js"></script>
    <script type="module" src="assets/RoleInfo-WOuHoth4.js"></script>
    <script type="module" src="assets/LogInfo-CSYpO3pO.js"></script>
    <script type="module" src="assets/TestList-CA1IAjrw.js"></script>
    <script type="module" src="assets/PromoteArticleList-BaTVAbme.js"></script>
    <script type="module" src="assets/CreatePromoteArticle-RTZKBl_x.js"></script>
  </body>
</html>

html 只注入首屏代码

import html from '@rollup/plugin-html'

    html({
      template: ({ files, publicPath }) => {
        const htmlContent = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8')

        // 生成 CSS link 标签
        const cssLinks = (files.css || [])
          .map(file => `<link rel="stylesheet" href="${publicPath}${file.fileName}">`)
          .join('\n    ')

        // 只注入入口文件和 vendor 文件,过滤掉懒加载的页面组件
        const entryAndVendorPatterns = [
          /main-[\w-]+\.js$/,      // 入口文件
          /vue-vendor-[\w-]+\.js$/, // Vue 核心
          /tdesign-[\w-]+\.js$/,    // TDesign UI
          /wangeditor-[\w-]+\.js$/, // wangEditor
          /wangeditor-vue-[\w-]+\.js$/, // wangEditor Vue
          /vendor-[\w-]+\.js$/     // 其他第三方库
        ]

        const jsScripts = (files.js || [])
          .filter(file => entryAndVendorPatterns.some(pattern => pattern.test(file.fileName)))
          .map(file => `<script type="module" src="${publicPath}${file.fileName}"></script>`)
          .join('\n    ')

        // 注入到 HTML 中
        let result = htmlContent

        // 在 </head> 前注入 CSS
        if (cssLinks) {
          result = result.replace('</head>', `    ${cssLinks}\n  </head>`)
        }

        // 替换原始的 script 标签
        if (jsScripts) {
          result = result.replace(
            /<script\s+type="module"\s+src="\/src\/main\.ts"><\/script>/,
            jsScripts
          )
        }

        return result
      },
      fileName: 'index.html'
    }),

源码

rollupInternal 函数

graph

image.png

normalizeEntryModules

image.png

moduleLoader

image.png

moduleLoader 原型方法

image.png

loadEntryModule / resolveId

image.png

resolveId / resolveIdViaPlugins 执行结果

image.png

loadEntryModule / getResolvedIdWithDefaults

image.png

loadEntryModule / fetchModule

创建 module

image.png

image.png

moduleLoader / modulesById

image.png

transform

image.png

build 函数

/**
 * 构建Rollup项目
 * @param inputOptions Rollup配置选项,已经合并完毕的 Rollup 配置(来自配置文件或命令行)
 * @param warnings 警告收集器
 * @param silent 是否静默模式
 * @returns 
 */
export default async function build(
	inputOptions: MergedRollupOptions,
	warnings: BatchWarnings,
	silent = false
): Promise<unknown> {
	//  决定输出模式
	const outputOptions = inputOptions.output;
	// 当输出配置既没有 file 也没有 dir 时,走 stdout 模式
	const useStdout = !outputOptions[0].file && !outputOptions[0].dir;
	const start = Date.now();
	const files = useStdout ? ['stdout'] : outputOptions.map(t => relativeId(t.file || t.dir!));

	// 打印构建起始信息
	if (!silent) {
		// 兼容 input 的三种形态:字符串、数组、对象(命名入口)
		let inputFiles: string | undefined;
		if (typeof inputOptions.input === 'string') {
			inputFiles = inputOptions.input;
		} else if (Array.isArray(inputOptions.input)) {
			inputFiles = inputOptions.input.join(', ');
		} else if (typeof inputOptions.input === 'object' && inputOptions.input !== null) {
			inputFiles = Object.values(inputOptions.input).join(', ');
		}
		stderr(cyan(`\n${bold(inputFiles!)}${bold(files.join(', '))}...`));
	}

	// 创建 Bundle
	// await using 是 ES2024 的 Explicit Resource Management 语法
	// 在作用域结束时会自动调用 bundle.close(),释放资源。
	await using bundle = await rollup(inputOptions as any);
	if (useStdout) {
		const output = outputOptions[0];

		// sourcemap 限制:stdout 模式只允许 inline
		if (output.sourcemap && output.sourcemap !== 'inline') {
			handleError(logOnlyInlineSourcemapsForStdout());
		}
		// 用 bundle.generate()(仅生成、不写盘)
		const { output: outputs } = await bundle.generate(output);
		for (const file of outputs) {
                // 多 chunk 时用 //→ filename: 分隔
                if (outputs.length > 1) process.stdout.write(`\n${cyan(bold(`//→ ${file.fileName}:`))}\n`);
                // file.type === 'asset' 时写 source(如图片/字体),否则写 code
                process.stdout.write(file.type === 'asset' ? file.source : file.code);
		}
		if (!silent) {
			warnings.flush();
		}
		// return 提前结束,不走后面的文件写入逻辑
		return;
	}

	// 并行写入:多个 output 用 Promise.all 并发执行 bundle.write,提升效率
	await Promise.all(outputOptions.map(bundle.write));
	if (!silent) {
		warnings.flush();
		stderr(green(`created ${bold(files.join(', '))} in ${bold(ms(Date.now() - start))}`));
		if (bundle && bundle.getTimings) {
			printTimings(bundle.getTimings());
		}
	}
}

build.generate

生成产物(仅在内存中,不写入文件)

async generate(rawOutputOptions: OutputOptions) {
    if (result.closed) return error(logAlreadyClosed());
    return handleGenerateWrite(false, inputOptions, unsetInputOptions, rawOutputOptions, graph);
},

build.write

生成并写入产物到文件

async write(rawOutputOptions: OutputOptions) {
    if (result.closed) return error(logAlreadyClosed());

    return handleGenerateWrite(true, inputOptions, unsetInputOptions, rawOutputOptions, graph);
}

其他

  1. 官网
  2. rollup 配置