{T}

Rollup 插件开发

本篇系统整理 Rollup 官方「插件开发」文档中的技术内容,并结合前两篇 《Rollup》与《Rollup 核心插件系统》做更偏“实践视角”的串联,帮助你从零编写、 调试并发布一个高质量的 Rollup 插件。


1. 插件概述

Rollup 插件本质上是一个普通的 JavaScript 对象,它:

  • 拥有若干属性(如 nameversion
  • 实现一个或多个构建阶段钩子
  • 实现一个或多个输出生成阶段钩子

一个典型的插件通常以“工厂函数”的形式对外导出:

  • 包作为一个 npm 包发布
  • 默认导出一个函数
  • 调用该函数可以传入插件特定的选项
  • 函数返回插件对象

插件可以用来完成:

  • 在打包前对源码进行转译或注入辅助代码
  • node_modules 中解析第三方依赖
  • 在构建结束后生成额外的产物(清单、报告等)
  • 对 chunk 进行二次处理、重写 import.meta 或文件 URL 等

在上一篇《Rollup 核心插件系统》里,我们从宏观上讲了“插件在生命周期的什么阶段能做什么事”; 本篇则对官方文档里每个钩子、上下文和惯例做更细粒度的技术说明。


2. 一个简单的插件示例

下面是官方文档中的一个经典示例:拦截对虚拟模块 virtual-module 的导入, 并为它返回一段“虚拟源码”,整个过程完全不访问文件系统。

js
// rollup-plugin-my-example.js
export default function myExample() {
  return {
    name: "my-example",

    resolveId(source) {
      if (source === "virtual-module") {
        return source
      }
      return null
    },

    load(id) {
      if (id === "virtual-module") {
        return 'export default "This is virtual!"'
      }
      return null
    }
  }
}

在 Rollup 配置中启用该插件,并将入口改为虚拟模块:

js
// rollup.config.js
import myExample from "./rollup-plugin-my-example.js"

export default {
  input: "virtual-module",
  plugins: [myExample()],
  output: [
    {
      file: "bundle.js",
      format: "es"
    }
  ]
}

关键点:

  • resolveId 负责“声明”:virtual-module 这个模块 ID 由本插件处理
  • load 返回该模块的源码字符串
  • 对使用方来说,这和从真实文件系统加载一个模块没有区别

这类“虚拟模块”技巧在很多插件中都很常见,例如:

  • 插件内部抽出公共 runtime 代码
  • 从配置生成一段虚拟的入口模块
  • 注入 polyfill 或调试工具等

3. 插件编写约定

为了让插件更易于在社区中被发现、理解与维护,官方文档给出了一系列约定:

  1. 命名约定

    • 插件名称建议以 rollup-plugin- 作为前缀
    • package.json 中添加 rollup-plugin 关键词,便于搜索
  2. 测试与异步 IO

    • 插件应该有测试,官方推荐使用支持 Promise 的测试框架(如 Mocha、AVA)
    • 在可能的情况下优先使用异步 API(例如 fs.promises.readFile),避免阻塞构建
  3. 文档与说明

    • 使用英文撰写插件的 README,以便更多开发者理解和使用
    • 明确说明插件的配置项、支持的 Rollup 版本以及典型使用场景
  4. Source Map

    • 如果插件会修改源码(通过 transform 钩子等),应当生成并返回正确的 Source Map
    • 这对调试、错误栈定位、性能分析等都非常重要
  5. 虚拟模块 ID 前缀

    • 若插件内部使用“虚拟模块”(如注入辅助函数、工具模块),推荐使用 \0 前缀
    • 如:\0my-plugin-runtime
    • 作用:
      • 防止其他插件尝试去解析这个 ID
      • 避免与真实文件路径冲突

4. 插件对象的核心属性

官方文档在“属性”章节重点强调了两个字段。

4.1 name

  • 类型:string
  • 作用:
    • 在日志、警告和错误信息中标识插件
    • 便于用户快速定位是哪一个插件报错

这是插件对象中唯一必填的字段。没有 name 的插件会导致调试体验非常糟糕。

4.2 version

  • 类型:string
  • 作用:
    • 主要用于插件之间的通信场景
    • 某些插件可能会根据另外一个插件的版本号决定行为

该字段通常直接与 package.json 中的版本保持一致。


5. 构建阶段钩子(Build Hooks)

为了与 Rollup 的构建流程交互,插件可以实现一组构建阶段钩子。 官方文档按“钩子类型”说明了它们的执行方式:

  • async:钩子可以返回 Promise;否则视为同步
  • first:若多个插件实现该钩子,按顺序执行,直到某个钩子返回非 null/undefined
  • sequential:所有插件的该钩子按顺序一个接一个执行
  • parallel:多个插件可以并行执行该钩子

除此之外,很多钩子既可以写成函数,也可以写成“带配置对象”的形式,详见后文。

下面按调用时机梳理每个构建阶段钩子的职责。

5.1 options

  • 类型:sequential

  • 调用时机:

    • 在每次构建开始前
    • 在 Rollup 解析完用户传入的基础配置后
  • 函数签名(简化):

    ts
    options(inputOptions)
  • 返回值:

    • 可以返回新的 inputOptions 对象,用于覆盖或扩展配置
    • 若返回 null/undefined,则沿用原有配置

典型用途:

  • 动态修改入口文件、外部依赖或插件列表
  • 根据自定义环境变量,注入额外配置

5.2 buildStart

  • 类型:sequential

  • 调用时机:

    • 构建正式开始时
    • 所有配置已经生效
  • 签名(简化):

    ts
    buildStart(inputOptions)

典型用途:

  • 初始化插件内部状态或缓存
  • 校验外部依赖、环境变量是否存在
  • 使用 this.addWatchFile 注册额外的监听文件

5.3 resolveId

  • 类型:first

  • 调用时机:

    • 遇到每一个 importrequire(通过 CommonJS 插件转换后)时
    • 也用于解析入口文件 ID
  • 签名(简化):

    ts
    resolveId(source, importer, options)

    其中:

    • source: 源码中出现的导入路径
    • importer: 当前模块的绝对路径(入口模块为 undefined
    • options.custom: 供插件间通信使用的自定义选项
  • 返回值:

    • null/undefined:交给下一个插件处理,最终由 Rollup 默认解析逻辑兜底
    • 字符串:解析后的模块 ID
    • 对象:{ id, external, moduleSideEffects, ... }

典型用途:

  • 自定义别名解析(路径映射)
  • 支持虚拟模块、内联模块等
  • 控制模块是否视为 external

5.4 resolveDynamicImport

  • 类型:first

  • 调用时机:

    • 遇到动态导入表达式 import(expr)
  • 签名(简化):

    ts
    resolveDynamicImport(specifier, importer, options)
  • 返回值:

    • 类似 resolveId,允许返回字符串/对象或 null

典型用途:

  • 对动态导入路径进行特殊处理
  • 限制或重写某些动态导入的目标

5.5 load

  • 类型:first

  • 调用时机:

    • 当 Rollup 需要获取某个已解析模块 ID 的源码时
  • 签名(简化):

    ts
    load(id)
  • 返回值:

    • null/undefined:交给下一个插件或默认文件读取逻辑
    • 字符串:模块源码
    • 对象:{ code, map }

典型用途:

  • 为虚拟模块返回源码
  • 从自定义存储(内存、远程接口等)读取模块内容

5.6 transform

  • 类型:sequential

  • 调用时机:

    • 每个模块被成功 load
  • 签名(简化):

    ts
    transform(code, id)
  • 返回值:

    • null/undefined:表示未修改代码
    • 字符串:新的代码
    • 对象:{ code, map, ast, moduleSideEffects, syntheticNamedExports }

典型用途:

  • 代码转译(如调用 Babel、TypeScript 编译器)
  • 注入辅助函数或运行时代码
  • 删除调试语句、替换常量、宏展开等

5.7 shouldTransformCachedModule

  • 类型:first

  • 调用时机:

    • 在使用缓存模块前,Rollup 会询问插件该模块是否需要重新执行 transform
  • 签名(简化):

    ts
    shouldTransformCachedModule(options)
  • 作用:

    • 针对 watch 模式或持久化缓存,插件可以精细控制缓存复用策略

5.8 moduleParsed

  • 类型:sequential

  • 调用时机:

    • 某个模块被解析为 AST 之后
  • 签名(简化):

    ts
    moduleParsed(moduleInfo)
  • moduleInfo 中包含:

    • 导入导出列表
    • 该模块的依赖关系
    • meta 字段可存放插件自定义元数据

典型用途:

  • 基于 AST 做静态分析
  • 为后续阶段准备统计信息(代码大小、依赖图、命名导出等)

5.9 onLog

  • 类型:sequential
  • 调用时机:
    • 当 Rollup 打算输出一条日志(包括错误、警告、信息)时

插件可以通过该钩子:

  • 过滤、重写或收集日志
  • 将日志转发到自定义的日志系统

5.10 buildEnd

  • 类型:sequential

  • 调用时机:

    • 构建结束时,无论成功与否都会调用
  • 签名(简化):

    ts
    buildEnd(error?)

典型用途:

  • 释放资源,关闭数据库连接、网络连接
  • 根据 error 参数输出构建失败的统计信息

5.11 watchChange

  • 类型:sequential

  • 调用时机:

    • --watch 模式下,当某个文件发生变更时
  • 签名(简化):

    ts
    watchChange(id, change)
  • change 一般包含:

    • event: "create" | "update" | "delete"

典型用途:

  • 清理插件内部缓存
  • 调整增量构建策略

5.12 closeWatcher

  • 类型:sequential
  • 调用时机:
    • watch 监听器关闭时
  • 典型用途:
    • 清理和 watch 相关的持久连接、临时文件等

6. 钩子对象形式与执行控制

官方文档特别说明:除直接提供函数外,很多钩子也可以以对象形式声明:

js
export default function resolveFirst() {
  return {
    name: "resolve-first",
    resolveId: {
      order: "pre",
      handler(source) {
        if (source === "external") {
          return { id: source, external: true }
        }
        return null
      }
    }
  }
}

当钩子是对象时,真正的处理逻辑写在 handler 字段里,额外支持:

  • order: "pre" | "post" | null

    • "pre":在实现同一钩子的所有插件之前执行
    • "post":在实现同一钩子的所有插件之后执行
    • null 或未设置:遵循 plugins 数组的默认顺序
  • sequential: boolean

    • 仅对 parallel 类型的钩子生效
    • 若为 true,则该插件的该钩子不会与其他插件并行,而是等待前序钩子完成后单独执行

这在需要严格顺序时非常有用,例如:

  • 多个 writeBundle 钩子需要串行执行且有依赖关系
  • 中间某个插件必须在其他插件前后精确插入一个步骤

7. 输出生成阶段钩子(Output Generation Hooks)

当 Rollup 开始从模块图生成最终 bundle 时,会进入“输出阶段”。插件可以在这一阶段:

  • 动态修改输出配置
  • 重写 chunk 内容或导入导出
  • 生成额外文件(如 manifest、报告)
  • 控制 bundle 写入磁盘前后的行为

下面按典型调用顺序介绍这些钩子。

7.1 outputOptions

  • 类型:sequential

  • 调用时机:

    • 对每一份输出配置执行一次
  • 签名(简化):

    ts
    outputOptions(outputOptions)
  • 返回值:

    • 可以返回新的 outputOptions,类似构建阶段的 options 钩子

典型用途:

  • 动态更改输出目录、文件名模式、格式等
  • 为多产物构建注入额外的输出设置

7.2 renderStart

  • 类型:sequential

  • 调用时机:

    • 某次输出流程开始之前
  • 签名(简化):

    ts
    renderStart(outputOptions, inputOptions)

典型用途:

  • 输出构建信息、版本号等
  • 初始化本次输出阶段需要的临时数据

这些钩子可以是字符串或返回字符串的函数,也可以是带 handler 的对象。

  • banner:插入在 bundle 代码的最开头
  • footer:插入在 bundle 代码的结尾
  • intro:插入在自执行函数或模块包装内部的开头
  • outro:插入在自执行函数或模块包装内部的结尾

常见用途:

  • 注入版权声明、构建信息
  • 插入包裹代码、运行时初始化逻辑等

7.4 augmentChunkHash

  • 类型:sequential

  • 调用时机:

    • Rollup 计算 chunk hash 的过程中
  • 签名(简化):

    ts
    augmentChunkHash(chunkInfo)
  • 返回值:

    • 可以返回一个字符串,将其混入 hash 计算

典型用途:

  • 当插件对 chunk 内容以外的因素敏感时,强制 hash 变化
  • 控制缓存失效策略(如插件配置变化时强制替换浏览器缓存)

7.5 renderChunk

  • 类型:sequential

  • 调用时机:

    • 对每一个生成的 chunk 执行
  • 签名(简化):

    ts
    renderChunk(code, chunk, options)
  • 返回值:

    • transform 类似,可以返回新的 { code, map }

典型用途:

  • 按 chunk 维度注入 runtime
  • 做 bundle 级别的压缩或重写(非模块级)

7.6 renderDynamicImport

  • 调用时机:
    • 生成最终代码时,决定 import() 在不同格式下如何呈现

通过该钩子可以:

  • 自定义不同输出格式下动态导入的实现方式
  • 与特定运行环境的模块加载机制集成

7.7 resolveFileUrl

  • 调用时机:

    • 生成 import.meta.ROLLUP_FILE_URL_* 相关代码时
  • 签名(简化):

    ts
    resolveFileUrl(options)
  • 其中 options 包含:

    • fileName
    • referenceId
    • chunkId 等信息

典型用途:

  • 针对不同部署环境(CDN、相对路径等)自定义资源 URL

7.8 resolveImportMeta

  • 调用时机:
    • 解析 import.meta.* 表达式时

典型用途:

  • import.meta.envimport.meta.url 等注入特定实现
  • 与运行时环境集成(如 SSR、定制运行时)

7.9 generateBundle

  • 类型:sequential

  • 调用时机:

    • Rollup 生成 bundle 元信息时
  • 签名(简化):

    ts
    generateBundle(outputOptions, bundle, isWrite)
  • bundle 是一个对象:

    • key 为文件名
    • value 为每个 chunk 或 asset 的描述信息

典型用途:

  • 遍历所有产物,生成清单文件、统计报告、HTML 模板等
  • 根据 isWrite 决定是“只读分析”还是“实际写文件”

7.10 writeBundle

  • 类型:sequential

  • 调用时机:

    • 所有文件已经写入磁盘之后
  • 签名与 generateBundle 类似:

    ts
    writeBundle(outputOptions, bundle)

典型用途:

  • 在文件真实存在于磁盘后执行额外操作:
    • 上传到远程服务器
    • 调用外部命令行工具
    • 清理中间产物

7.11 renderError

  • 调用时机:
    • 输出阶段发生错误且构建被中断时

典型用途:

  • 为输出阶段错误增加额外上下文
  • 将错误上报到监控系统

7.12 closeBundle

  • 调用时机:
    • 整个构建流程完全结束后(包括所有输出)

典型用途:

  • 关闭所有仍然打开的资源(网络连接、句柄等)
  • 做一次性清理或收尾日志

8. 插件上下文(this)常用方法

在每个钩子内部,this 指向插件上下文对象。官方文档列出了大量实用方法, 其中常用的包括:

  • this.addWatchFile(id)
    • --watch 模式下监听额外文件
  • this.getWatchFiles()
    • 获取当前被监听的所有文件列表
  • this.emitFile(descriptor)
    • 声明一个额外的文件或 chunk,例如
      • { type: "asset", name, source }
      • { type: "chunk", id, name }
  • this.setAssetSource(assetId, source)
    • 为通过 emitFile 声明的 asset 设置或更新内容
  • this.getFileName(fileId)
    • 根据 emitFile 返回的 fileId 获取最终生成的文件名
  • this.warn(message | RollupLog)
    • 打印非致命警告
  • this.error(message | RollupError)
    • 抛出构建错误并终止当前构建
  • this.info(message | RollupLog)
    • 输出信息级别日志
  • this.debug(message | RollupLog)
    • 输出调试级别日志
  • this.load(options)
    • 手动触发模块加载流程
  • this.parse(code, options?)
    • 将源码解析为 AST,使用与 Rollup 内部相同的解析器
  • this.resolve(source, importer, options?)
    • 在插件内部复用 Rollup 的模块解析逻辑
  • this.getModuleIds()
    • 返回一个遍历器,用于遍历所有已知模块 ID
  • this.getModuleInfo(id)
    • 获取单个模块的详细信息(包含 meta 字段)
  • this.getCombinedSourcemap()
    • 获取某个模块在所有 transform 链之后的合并 Source Map
  • this.meta
    • 包含插件运行时环境信息(如当前 Rollup 版本)

这些 API 是实现复杂插件(如分析工具、可视化工具、资源管理器)的基础。


9. 文件 URL 与 import.meta

官方文档针对“文件 URL”专门有一节说明,核心机制是:

  • 插件可以通过 this.emitFile 声明 asset 或 chunk
  • 在源码中使用 import.meta.ROLLUP_FILE_URL_referenceId
  • Rollup 在生成代码时会调用 resolveFileUrl 钩子,最终替换成实际 URL 字符串

典型流程示意:

js
// 在插件中
const imageId = this.emitFile({
  type: "asset",
  name: "logo.png",
  source: imageBuffer
})

// 在用户源码中(经由插件注入)
const logoUrl = import.meta.ROLLUP_FILE_URL_imageId

resolveFileUrl 钩子中,可以根据:

  • 当前输出目录
  • CDN 前缀
  • 是否 hash 命名

来生成不同形式的 URL,从而适配多种部署环境。

同时,resolveImportMeta 钩子允许插件自定义处理 import.meta.*

  • import.meta.env.* 映射到构建时环境变量
  • 替换或增强 import.meta.url 的行为

10. 转换器与代码转换(Transformers)

官方文档中把很多“只做源码转换”的插件统称为“转换器(transformers)”, 它们通常只实现 transform 钩子。

10.1 转换器的基本模式

一个典型的转换器插件大致结构如下:

js
export default function myTransformPlugin() {
  return {
    name: "my-transform",
    transform(code, id) {
      if (!id.endsWith(".js")) return null

      const transformed = someTransform(code)

      return {
        code: transformed.code,
        map: transformed.map
      }
    }
  }
}

关键点:

  • 通过 id 决定是否处理某个模块
  • 返回 { code, map } 保证 Source Map 正确串联

10.2 源代码转换示例

比如实现一个“移除所有 console.* 调用”的简单插件:

js
export default function stripConsole() {
  return {
    name: "strip-console",
    transform(code, id) {
      if (!id.endsWith(".js")) return null
      const next = code.replace(/\bconsole\.[a-zA-Z]+\([^;]*\);?\s*/g, "")
      if (next === code) return null
      return { code: next, map: null }
    }
  }
}

这类插件不依赖模块图或输出阶段,只基于源码字符串工作。

10.3 合成命名导出(Synthetic Named Exports)

对于那些只提供默认导出、没有显式命名导出的模块,Rollup 提供 syntheticNamedExports 概念;插件也可以在 transform 返回值中设置:

js
return {
  code,
  map,
  syntheticNamedExports: true
}

这样 Rollup 会尝试基于返回的代码为该模块创建“合成的命名导出”,以改善互操作性。


11. 插件间通信

在大型工程中,多种插件之间往往需要协作。官方文档总结了几种推荐的通信方式。

11.1 自定义解析器选项

this.resolveresolveId 钩子都支持一个 custom 字段,用于在解析时传递额外信息:

js
// 在插件 A 中
const resolved = await this.resolve(source, importer, {
  custom: {
    "my-plugin": { needRawId: true }
  }
})

在插件 B 的 resolveId 中可以读取:

js
resolveId(source, importer, options) {
  const custom = options.custom && options.custom["my-plugin"]
  if (custom && custom.needRawId) {
    // 按插件 A 的需求执行特殊逻辑
  }
}

这种方式适用于多个解析插件协同工作时传递额外上下文。

11.2 自定义模块元数据

每个模块的 moduleInfo.meta 字段可以挂载任意插件自定义数据:

js
// 在某个插件中
moduleParsed(info) {
  info.meta.myPlugin = {
    hasSpecialExport: checkSomething(info.ast)
  }
}

其他插件可以在后续钩子中通过 this.getModuleInfo(id) 获取这些信息,从而实现跨插件的数据共享。

11.3 插件对象上的自定义 API

插件对象本身也可以暴露一个 api 字段,供其他插件直接调用:

js
export default function corePlugin() {
  const state = new Map()

  return {
    name: "core-plugin",
    api: {
      register(id, info) {
        state.set(id, info)
      },
      get(id) {
        return state.get(id)
      }
    }
  }
}

其他插件可以在 Rollup 配置中拿到该插件实例后,通过 api 访问其中的方法。 这种模式适合插件之间存在明确依赖关系、且需要共享较复杂逻辑时使用。


12. 小结与实践建议

本篇将官方「插件开发」文档中的技术要点按“插件对象 → 钩子 → 上下文 → 文件 URL → 转换器 → 插件间通信”的顺序系统梳理,结合本系列前两篇,你可以用以下步骤 练习并掌握插件开发:

  1. 先实现一个只用 resolveIdload 的“虚拟模块”插件
  2. 再实现一个只用 transform 的简单转换器(如删除调试代码)
  3. 在此基础上尝试:
    • 使用 moduleParsed 做简单的 AST 分析
    • generateBundle 中基于 bundle 生成清单文件
    • 使用 import.meta.ROLLUP_FILE_URL_*resolveFileUrl 处理静态资源
  4. 最终,将这些能力组合成一个具有实际生产价值的插件,并按本节的约定 补全文档、测试与发布流程

掌握这些内容后,你就可以在 Rollup 之上搭建高度定制的构建流程,甚至为其他打包工具 (如 Vite)贡献插件生态。