Rolldown 非 ESM 输出格式(CJS/IIFE/UMD)全解析:Top Level Await 与import.meta的降级处理指南
【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown
本指南聚焦 Rolldown 在输出非 ESM(CJS / IIFE / UMD)产物时的两大核心限制——Top Level Await 不支持与
import.meta语义差异——完整讲解 Rolldown 的报错、Polyfill、替换与告警行为,并给出 IIFE/UMD 下 polyfillimport.meta.url的可落地配置方案,帮助你写出可预测、可移植的非 ESM 构建产物。
一、非 ESM 输出格式概述
Rolldown 作为 Rollup 兼容 API 的 Rust bundler,默认输出能力以 ESM 为基线,但同时支持把模块图降级输出为传统格式。当前仓库中,输出格式在 output_format.rs 中定义为四类:
pub enum OutputFormat { Esm, Cjs, Iife, Umd, }对应的内部字符串值与 JS 侧InternalModuleFormat对齐("es"/"cjs"/"iife"/"umd")。其中:
keep_esm_import_export_syntax()仅对Esm返回true,即只有 ESM 产物会保留import/export语法;should_call_runtime_require()对Cjs | Umd | Iife返回false,意味着这几种格式下模块初始化依赖注入的运行时require路径不同;- 源码类型(SourceType)上,
Esm对应mjs,Cjs对应cjs,而Iife/Umd以cjs语义附加script标记。
由于 ESM 的部分能力(顶层 await、import.meta)在 CJS/IIFE/UMD 语境下没有原生对应物,Rolldown 的处理策略分两种:直接报错(无法安全降级的语法)与Polyfill / 替换 / 告警(可以降级但语义会发生变化的语法)。下文逐一展开。
二、Top Level Await:非 ESM 下直接报错
核心事实:Top Level Await(TLA)在非 ESM 输出格式中不被支持。当输出格式不是 ESM 时,一旦模块图中出现顶层await,Rolldown 会直接输出错误,而不是静默降级。
这条规则的判定发生在链接阶段。在 compute_tla.rs 中,compute_tla会从每个模块出发做 DFS 遍历,通过module.ast_usage.contains(EcmaModuleAstUsage::TopLevelAwait)判断"模块自身是否含 TLA",并通过import_records沿ImportKind::Import边递归查找依赖中是否间接引入了 TLA 模块,最终把结果写入is_tla_or_contains_tla_dependency元数据:
fn find_tla_source(module_idx, module_table, visited) -> Option<ModuleIdx> { // 自身含 TLA let is_self_tla = module.as_normal().is_some_and(|m| m.ast_usage.contains(EcmaModuleAstUsage::TopLevelAwait)); // 否则沿 import 边递归查找依赖中的 TLA 来源 }这段逻辑同时构建了完整的"导入链"(ImportChainNote),当遇到require()一个含 TLA 的模块时,会生成BuildDiagnostic::require_tla诊断,把从入口到 TLA 来源模块的每一跳importer_stable_id、importee_stable_id、导入语句 span 都记录下来,方便你定位到底是哪条导入路径把 TLA 带进了产物。仓库还专门为cjs_reexport_barrel_tla_fallback、strict_execution_order等场景准备了回归测试用例(见 tests/rolldown/function/experimental 目录)。
实操建议:如果你的代码库使用了 TLA 且目标产物是 CJS/IIFE/UMD,请先确认是否真的需要 TLA——常见替代方案包括:
- 把顶层
await收敛进async function并在模块初始化时显式调用; - 使用动态
import()在运行时按需加载(动态导入在非 ESM 格式下会被 Rolldown 转换为对应的运行时加载逻辑); - 保留 ESM 产物(如
"type": "module"的 Node 环境)以原生支持 TLA。
三、import.meta:语法错误与自动降级
核心事实:import.meta在非 ESM 格式中本身属于语法错误(CJS 语境下没有import.meta对象)。为避免产物直接语法报错,Rolldown 会把import.meta替换为其他值——但替换的具体策略因属性而异。
从源码看,import.meta的重写逻辑集中在 module_finalizers/mod.rs 与 module_finalizers/impl_visit_mut.rs 的try_rewrite_import_meta_prop_expr中,它统一处理import.meta.xxx、import.meta['xxx']、import.meta?.xxx、import.meta?.['xxx']四种访问形式。
3.1 广为人知的import.meta属性(CJS 下 Polyfill)
Rolldown 对以下三个"广为人知"(well-known)属性提供支持:
import.meta.urlimport.meta.dirnameimport.meta.filename
仅在输出格式为 CJS 时,这三个属性会被 Polyfill:
import.meta.url→ 重写为require('url').pathToFileURL(__filename).href(见 impl_visit_mut.rs 中can_polyfill_import_meta_url的实现);import.meta.dirname→ 重写为__dirname;import.meta.filename→ 重写为__filename。
对应的判定条件在can_polyfill_import_meta_url中写得很明确:
fn can_polyfill_import_meta_url(&self) -> bool { matches!( (self.ctx.options.platform, &self.ctx.options.format), (Platform::Node, OutputFormat::Cjs) ) }也就是说,只有在platform: 'node'且format: 'cjs'的组合下才会启用这套 Polyfill;__dirname/__filename的替换也复用同一个开关。这保证了 polyfill 生成的require('url')调用在 Node CJS 运行环境中必然可用。
在其他格式(IIFE / UMD,以及非 Node 平台的 CJS)下,这三个属性与其余属性一视同仁,走下文 3.3 节的"替换为{}+ 告警"路径。
3.2 IIFE 与 UMD 下 Polyfillimport.meta.url的配置方案
与 Rollup 的差异:Rollup 支持在 IIFE 和 UMD 格式中直接 Polyfillimport.meta.url,但Rolldown 目前不支持这一内置行为(源码中can_polyfill_import_meta_url仅覆盖 Node + CJS 即为证据)。如果你确实需要在 IIFE / UMD 产物中获得文件 URL,官方文档给出的方案是利用transform.define+output.intro自行注入。
下面两个配置直接取自文档,可复制到rolldown.config.ts中实际使用。
IIFE 场景:利用document.currentScript读取当前脚本地址,回退到new URL('main.js', document.baseURI):
import { defineConfig } from 'rolldown'; const importMetaUrlPolyfillVariableName = '__import_meta_url__'; export default defineConfig({ transform: { define: { 'import.meta.url': importMetaUrlPolyfillVariableName, }, }, output: { format: 'iife', intro: "var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;" + `var ${importMetaUrlPolyfillVariableName} = (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('main.js', document.baseURI).href)`, }, });UMD 场景:在 IIFE 逻辑基础上,额外兼容 Node(require('url').pathToFileURL(__filename).href)与浏览器全局(location.href)两种运行时:
import { defineConfig } from 'rolldown'; const importMetaUrlPolyfillVariableName = '__import_meta_url__'; export default defineConfig({ transform: { define: { 'import.meta.url': importMetaUrlPolyfillVariableName, }, }, output: { format: 'umd', intro: "var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;" + `var ${importMetaUrlPolyfillVariableName} = (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('main.js', document.baseURI).href))`, }, });两个配置的要点说明:
transform.define:把import.meta.url在 AST 层面替换为自定义的__import_meta_url__变量,从而绕开 Rolldown 对import.meta的默认降级路径;output.intro:在产物开头注入该变量的初始化代码,按运行环境选择最可靠的取值方式(currentScript.src→location.href→pathToFileURL(__filename));'u' + 'rl'的写法是刻意为之,避免 UMD 头部自身的模块探测逻辑被require('url')干扰;main.js需替换为你实际的入口文件名,也可用其他静态资源名配合document.baseURI推导。
3.3 其他属性与import.meta对象本身:替换为{}并告警
核心事实:对于上述 well-known 属性之外的自定义属性(如import.meta.env、import.meta.hot之外的用户自定义字段),以及import.meta对象本身,Rolldown 在非 ESM 输出下会将其整体替换为{}。
由于这种替换不保留原值(读取任何属性都会得到undefined),Rolldown 会**发出告警(warning)**提示你注意。告警实现在 empty_import_meta.rs,其标题为:
`import.meta` may not be a valid syntax with the `{format}` output format.并附带 label 说明:"Thisimport.metawill be replaced with an empty object ({}) automatically."
如果这是你期望的行为,可以通过transform.define: { 'import.meta': {} }显式声明,从而抑制该告警。如果import.meta必须保留原语义,则需要把输出格式改回esm。
3.4 告警的三种细分来源
从EmptyImportMetaKind枚举(定义于 empty_import_meta.rs)可以看到,{}替换的来源被细分为三类,以便给出更精确的诊断提示:
| Kind | 来源 | 诊断提示 |
|---|---|---|
Plain | 用户手写的裸import.meta或非url属性访问 | 提示可加define: { 'import.meta': {} }抑制,或改用esm保留 |
Url | 用户手写的import.meta.url | 额外附上 Rollup 式 polyfill 方案的文档指引 |
RolldownFileUrl | 由import.meta.ROLLDOWN_FILE_URL_*展开产生的new URL(..., import.meta.url).href | 明确提示"生成的import.meta.url会变成undefined" |
其中RolldownFileUrl是 Rolldown 特有机制:当插件通过resolveFileUrlhook 提供文件 URL 替换时,import.meta.ROLLDOWN_FILE_URL_<referenceId>会被展开为new URL({相对资源路径}, import.meta.url).href,而其中生成的import.meta.url在非 ESM(且非 Node+CJS)下同样会塌缩为{}——因此告警会指向原始引用处并提示 polyfill 方案。这一展开逻辑与surviving_import_meta_spans的记录机制都集中在 module_finalizers/mod.rs 中,源码注释还专门解释了为何同一个节点会被访问两次、以及如何用"首次插入优先"去重。
四、决策速查表
| 场景 | ESM | CJS (Node) | IIFE / UMD |
|---|---|---|---|
| Top Level Await | ✅ 原生支持 | ❌ 编译报错 | ❌ 编译报错 |
import.meta.url/dirname/filename | ✅ 原生支持 | ✅ 自动 Polyfill(require('url').pathToFileURL(__filename)/__dirname/__filename) | ⚠️ 替换为{}+ 告警(可用transform.define+output.intro自行 Polyfill) |
其他import.meta属性 / 对象本身 | ✅ 原生支持 | ⚠️ 替换为{}+ 告警(可用define: { 'import.meta': {} }抑制) | ⚠️ 替换为{}+ 告警 |
import.meta.ROLLDOWN_FILE_URL_*展开结果 | ✅ 保留import.meta.url | ✅(Node+CJS)正常 | ⚠️ 生成的import.meta.url塌缩为undefined,告警指向原引用 |
五、总结
Rolldown 对非 ESM 输出格式的处理遵循清晰的分级策略:TLA 无法安全降级 → 直接报错并给出完整导入链;import.meta可以降级 → 按"Node+CJS 自动 Polyfill、其他场景替换为{}并告警"处理。实际工程中最常遇到的三个坑分别是:
- 顶层
await进入 CJS/IIFE/UMD 产物——需要先改代码结构; - IIFE/UMD 下
import.meta.url静默变成undefined——务必按上文配置transform.define+output.intro注入 polyfill; - 对
{}替换行为感到意外——可通过define: { 'import.meta': {} }显式声明并抑制告警,或改用esm输出。
深入理解这两条降级路径(对应源码 compute_tla.rs 与 module_finalizers/mod.rs),你就能准确预测任意非 ESM 构建的产物行为,写出同时兼容浏览器<script>直引、CommonJS 与 AMD/UMD 生态的可靠构建配置。
【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考