的透传、警告与模块判定)
Rolldown 行为解析ESM 中混用 CommonJS 变量exports/module的透传、警告与模块判定【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown导读本篇文章基于 rolldown 仓库中的 esbuild 兼容性测试用例no_warn_common_js_exports_in_esm_pass_throughdiff.md深入解析 rolldown 在单个源码文件中同时出现 ESM 语法import/export与 CommonJS 全局变量exports、module时的三类行为模块类型判定、警告诊断与打包输出差异。读完本文你将掌握 rolldown 内部如何区分一个文件是 ESM 还是 CJS、为什么 rolldown 会而 esbuild 默认不会对 ESM 中的exports/module使用发出COMMONJS_VARIABLE_IN_ESM警告以及这种混写代码在两种打包器下的最终产物差异。测试用例背景esbuild 兼容性测试体系该 diff 文件位于crates/rolldown/tests/esbuild/default/目录属于 rolldown 的 esbuild 对照测试esbuild-tests体系。这类测试的固定模式是为每个测试目录提供一个或多个入口文件和一个_config.json配置通过快照对比 esbuild 与 rolldown 各自的产物并用 diff 展示差异。本用例的入口配置_config.json声明了三个独立入口{ config: { input: [ { name: cjs-in-esm, import: cjs-in-esm.js }, { name: import-in-cjs, import: import-in-cjs.js }, { name: no-warnings-here, import: no-warnings-here.js } ] }, expectExecuted: false }其中expectExecuted: false表示这些入口只做打包验证、不实际执行。三个入口分别覆盖三种典型的混写形态入口文件源码内容语义特征cjs-in-esm.jsexport let foo 1; exports.foo 2; module.exports 3ESM 导出 直接使用 CJS 全局变量import-in-cjs.jsimport { foo } from bar; exports.foo foo; module.exports foo有 import但主体按 CJS 风格导出no-warnings-here.jsconsole.log(module, exports)无任何 ESM 语法仅使用 CJS 全局变量模块类型判定一个文件到底是 ESM 还是 CJS混写场景下打包器必须先回答一个根本问题这份源码按 ESM 还是按 CJS 语义处理判定结果直接决定后续的警告与代码生成。rolldown 的判定逻辑集中在 ast_scanner/mod.rs 的scan方法中核心规则如下若源码中出现export关键字self.esm_export_keyword有值则该文件被判定为ExportsKind::EsmESM 模块。此时如果扫描到module标识符或exports标识符的使用就分别对每个标识符产生一条COMMONJS_VARIABLE_IN_ESM警告ast_scanner/mod.rs。若没有 ESM 导出但源码使用了module/exports或存在顶层returnast_usage中包含ModuleOrExports或TopLevelReturn则该文件被判定为ExportsKind::CommonJsCJS 模块。若都没有则根据模块格式ModuleDefFormat推断.cjs/cts/CJS 风格 package.json 归为 CommonJS.mjs/mts/ESM 风格 package.json 归为 ESMUnknown时再依据是否存在import关键字进一步判断。将这一规则套用到本用例的三个入口上cjs-in-esm.js含export let foo→ 判定为ESM因此exports.foo与module.exports两处使用各触发一次警告import-in-cjs.js没有export但使用了exports与module→ 判定为CommonJS即使它顶部有一个import语句no-warnings-here.js无export也无import只使用module/exports→ 判定为CommonJS。警告机制源码剖析COMMONJS_VARIABLE_IN_ESM警告的诊断类型定义在 crates/rolldown_error/src/build_diagnostic/events/commonjs_variable_in_esm.rs结构体CommonJsVariableInEsm记录了文件名、源码、esm_export_spanexport关键字位置与cjs_export_ident_spanmodule或exports标识符位置。诊断消息为The CommonJS{variable}variable is treated as a global variable in an ECMAScript module and may not work as expected其中{variable}根据命中的是module还是exports自动填充commonjs_variable_in_esm.rs。渲染诊断时on_diagnostic会在源码上标注两处位置一处是 ESM 的export关键字并附注 This file is considered to be an ECMAScript module because of theexportkeyword here:另一处是 CJS 标识符本身commonjs_variable_in_esm.rs。测试快照 artifacts.snap 中可以看到实际的警告渲染结果例如对cjs-in-esm.js第 2 行[COMMONJS_VARIABLE_IN_ESM] The CommonJS exports variable is treated as a global variable in an ECMAScript module and may not work as expected ╭─[ cjs-in-esm.js:2:1 ] │ 1 │ export let foo 1 │ ───┬── │ ╰──── This file is considered to be an ECMAScript module because of the export keyword here: 2 │ exports.foo 2 │ ───┬─── │ ╰───── ───╯快照中cjs-in-esm.js因此产生了两条警告module与exports各一条而no-warnings-here.js虽然同样使用了module/exports却没有任何COMMONJS_VARIABLE_IN_ESM警告——这正是测试名 no_warn_common_js_exports_in_esm_pass_through 的含义警告只在ESM 中使用 CJS 变量时触发纯粹的 CJS 用法不会告警。import-in-cjs.js除 CJS 判定外还因bar无法解析而产生一条UNRESOLVED_IMPORT警告artifacts.snapbar被当作 external 依赖原样保留。在错误码体系层面CommonJsVariableInEsm是EventKind枚举的第 19 号event_kind.rs字符串标识为COMMONJS_VARIABLE_IN_ESMevent_kind.rs并通过位掩码注册在可配置的检查选项中checks_options.rs这意味着用户可以通过配置项开关该警告。三类场景的产物对比diff 逐行解读场景一cjs-in-esm —— ESM 中被判定为 ESM 的透传esbuild 产物export let foo 1; exports.foo 2; module.exports 3;rolldown 产物//#region cjs-in-esm.js let foo 1; exports.foo 2; module.exports 3; //#endregion export { foo };diff 的核心差异只有两点rolldown 将export let foo改写为let foo 文件末尾的export { foo }变量声明与导出分离并附加//#region///#endregion注释块标记模块来源。由于文件被判定为 ESMexports.foo 2与module.exports 3两行被原样透传到产物中——与测试名中的 pass through 呼应。两者对这段代码的语义理解一致ESM 模块差异仅体现在声明/导出改写与源码组织注释上。场景二import-in-cjs —— 有 import 却被判定为 CJS 的包裹esbuild 产物import { foo } from bar; exports.foo foo; module.exports foo;rolldown 产物import { t as __commonJSMin } from ./rolldown-runtime.js; import { foo } from bar; //#region import-in-cjs.js var require_import_in_cjs /* __PURE__ */ __commonJSMin(((exports, module) { exports.foo foo; module.exports foo; })); //#endregion export default require_import_in_cjs();这里出现质的差异esbuild 将exports/module当作自由变量透传而 rolldown 因判定该文件为 CommonJS把整个模块体包装进__commonJSMin运行时辅助函数生成惰性的require_import_in_cjs最后export default导出。注意import { foo } from bar仍保留在模块顶层——这是 ESM 语法与 CJS 语义共存时rolldown 采用的运行时包装策略。场景三no-warnings-here —— 纯 CJS 的同样包裹esbuild 产物console.log(module, exports);rolldown 产物import { t as __commonJSMin } from ./rolldown-runtime.js; //#region no-warnings-here.js var require_no_warnings_here /* __PURE__ */ __commonJSMin(((exports, module) { console.log(module, exports); })); //#endregion export default require_no_warnings_here();没有任何 ESM 语法的纯 CJS 文件同样被__commonJSMin包裹但不产生任何警告——验证了前面所说的警告触发条件。从 rolldown 的角度看一个被判定为 CommonJS 的模块在 ESM 输出格式下统一采用包裹 默认导出的处理路径。运行时辅助__commonJSMin 的实现原理三种产物中 rolldown 侧都会引入./rolldown-runtime.js的__commonJSMin别名t。其定义在 runtime-base.jsexport var __commonJSMin (cb, mod) () ( mod || (cb((mod { exports: {} }).exports, mod), cb null), mod.exports );这是一个典型的惰性 CJS 包装器首次调用时创建mod { exports: {} }将exports与mod传给回调cb执行模块体后续调用直接复用缓存的mod返回mod.exports。/* __PURE__ */注释则标记该 IIFE 调用是纯的便于后续压缩器如 terser/esbuild minify做死代码消除。在源码中__commonJSMin的引用点包括链接阶段的模块包装wrapping.rs与代码生成阶段对运行时符号的解析impl_visit_mut.rs并作为运行时辅助统一登记在生成的 runtime helper 清单中runtime_helper.rs。由 artifacts.snap 可见该运行时文件本身在产物中以隐藏模块[\0rolldown/runtime.js]形式存在。行为差异速览与工程启示维度esbuildrolldownESM 中使用exports/module默认无警告直接透传发出COMMONJS_VARIABLE_IN_ESM警告module、exports各一条代码仍透传有import但使用exports/module视为自由变量透传判定为 CommonJS用__commonJSMin包裹并以 default 导出无 ESM 语法的纯 CJS透传判定为 CommonJS同样包裹导出但不告警对迁移 esbuild 到 rolldown 的工程团队而言本用例的启示很直接在同一文件内混写 ESM 与 CJS 语法是危险的边缘情况。esbuild 采用宽松的透传哲学这也是测试名中 esbuild 侧 pass through 的由来而 rolldown 则通过COMMONJS_VARIABLE_IN_ESM警告主动提示这类代码在 ESM 语义下exports/module将退化为全局变量、行为可能不符合预期。如果你的代码库存在大量此类混写建议关注该警告的输出并借助 rolldown 的配置选项开关与ExportsKind判定结果逐步将文件迁移为纯粹的 ESM 或 CJS 形态。【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考