Agent Developer Roadmap
构建工具

Babel:把单个 JS/TS 文件从源码改写为目标 JS

核验日期:2026-07-07。示例版本来自 examples/build-tools/package.json@babel/core 8.0.1、@babel/cli 8.0.1、@babel/preset-env 8.0.2、@babel/preset-typescript 8.0.1。

需要掌握的问题

主题读者必须回答的问题原理与边界高频错误代码或观察证据来源
定位与固定成本Babel 在构建链路里负责哪一段,为什么不是打包器?Babel 读取单个 JS/TS/JSX 文件,经过 parserASTtraversegenerator 输出 JS 和 Source Map;它不构建浏览器 bundle、不处理 CSS 图、不做类型证明期望 Babel 输出 index.html、CSS 或 async chunkpnpm --filter @roadmap/build-tools-slice build:babel 只生成 .js.mapBabel 8 release
AST 流水线parse → AST → traverse/transform → generator 每步的输入输出是什么?@babel/parser 把源码解析成 Babel AST,visitor 在遍历中替换节点,@babel/generator 从 AST 打印代码和 mapparser 语法插件只“识别”语法,却误以为会转换语义自定义 visitor 改名示例、generator Source Map 选项@babel/parser@babel/traverse@babel/generator
visitor、preset、plugin 顺序多个插件和 preset 同时存在时谁先改 AST?plugin 是 visitor 集合;preset 是 plugin 组合。顺序改变会改变节点形态和后续 visitor 能否命中把 preset 写成“声明式无序集合”,导致插件看不到原始语法插件顺序最小例子和 BABEL_SHOW_CONFIG_FORBabel configuration
配置发现babel.config.*.babelrcpackage.json 在 monorepo 里何时生效?project-wide config 从 root 生效;file-relative config 随被编译文件向上查找,并受 package 边界影响;BABEL_SHOW_CONFIG_FOR 打印最终配置子包 .babelrc 未命中、CLI 从错误 cwd 启动、同一文件叠加两套配置BABEL_SHOW_CONFIG_FOR=... pnpm ...Babel config files
preset-env + Browserslisttarget 改变什么,什么仍要交给打包器或运行时?preset-env 根据目标环境映射启用 transforms,也可在 Babel 7 注入 core-js polyfill;Babel 8 把 polyfill 注入迁到单独插件认为 target 会自动补 Promise、自动打包模块、自动做类型检查.browserslistrcmodules: false 输出对照@babel/preset-env
四条边界语法转换、helper、polyfill、类型检查有什么区别?语法转换改 AST;helper 抽出复用逻辑;polyfill 提供运行时 API;类型检查由 tsc 或框架工具负责@babel/preset-typescript 删除类型后仍有运行时类型错误TS 类型删除、helper/polyfill 代码片段Babel 8 migration
Source Map源位置如何跨 Babel 输出保留?generator 根据节点位置、输入源码和上游 map 生成 version: 3 map;链路中任一阶段丢 map 都会影响调试生产泄露源码路径、map 指向 .js 而非 .ts、上游 map 未传入示例产物断言 .map.sources 包含 .ts@babel/generator
缓存与 monorepo为什么缓存命中会错,怎么让配置变化参与失效?Babel 自身转换结果取决于源码、Babel 版本、插件/preset、Browserslist、env、caller 与配置 root;monorepo 要固定 config rootCI 复用旧缓存、子包 Browserslist 改了但输出未变显式 --config-file、打印 config、缓存键清单Babel configuration
迁移与重大变化Babel 7.29 到 Babel 8 哪些变化会影响工程?Babel 8 ESM-only,要求 Node `^22.18.0>=24.11.0`,默认 target 改为 Browserslist defaults,polyfill 注入迁移,TypeScript preset 选项变化Node 20/旧 CommonJS 集成直接报错、默认不再输出 ES5/CJS
插件供应链与严重问题为什么“编译时插件”属于代码执行信任边界?Babel 配置和插件在开发机/CI 上以 Node 权限运行;历史 CVE-2023-45133 说明分析不可信代码时 path.evaluate() 曾可触发任意代码执行在服务端对用户上传源码运行自定义 Babel 分析、安装无审查插件CVE 复盘、锁版本、沙箱、最小权限 CICVE-2023-45133

内容边界说明:

分类本文落点
编程语言与类型系统只解释 JS/TS 语法如何被解析、转换和删除类型;不教学语言语法本身,也不把删除类型当类型检查
Browser APIs 与持久化能力覆盖 ESM 输出、动态导入语法保留、polyfill 与 Source Map;不讨论 IndexedDB、Storage 和权限模型
UI 框架与元框架只在 JSX/TS 插件边界处说明入口;组件状态、渲染和水合不属于 Babel 职责
构建工具与运行时完整覆盖输入、解析、转换、配置、缓存、插件、产物、调试和迁移
后端、数据与 Agent 技术只覆盖构建配置/插件以开发者权限执行的供应链边界;请求生命周期、数据库一致性和 Agent 评测不适用

Babel 先付固定成本,再换跨环境输出

Babel 负责把一份 JavaScript 或 TypeScript 源文件转换成另一份 JavaScript,并不包办整个应用构建。它的固定成本来自三处:

  1. 每个文件都要 parse 成 AST,再走 visitor,再由 generator 打印。
  2. 插件和 preset 的顺序会改变 AST 形态,配置不是纯注释。
  3. 输出是否适合浏览器,还取决于后续打包器、运行时 API 和部署策略。

因此 Babel 适合放在这些位置:

场景Babel 负责不负责
应用源码进入 webpack/Rollup/ViteJSX、TypeScript、语法降级、少量语义转换模块图、CSS、chunk、HTML、dev server
npm 包源码发布输出较低语法等级、保留 ESM/CJS 策略的一部分判断消费者环境、生成 package exports
codemod 或静态分析读 AST、改 AST、打印回源码运行代码、证明类型正确
浏览器兼容补齐语法转换、按配置插入 polyfill 入口或引用保证所有 Web API 存在、处理加载顺序

严重问题的最小定位方式:

症状根因观察命令修正
产物没有 index.html 或 CSSBabel 不是 bundlerfind examples/build-tools/dist/babel -maxdepth 2 -type f后面接 webpack/Vite,或使用原生 ESM 发布策略
浏览器仍报 Promise is not defined语法 target 不等于运行时 API polyfill搜索产物是否有 core-js/modules/es.promise明确引入 polyfill 策略,不把 target 当 API 保证
CI 和本地输出不同Browserslist、env 或 config root 不一致BABEL_SHOW_CONFIG_FOR=src/main.ts ...固定 --config-file、统一 Browserslist 和 Node 版本

从源码到输出:parser、AST、traverse、generator

Babel 的核心流程是单文件流水线。输入是字符串,输出仍是字符串;中间状态是 AST。

一个最小插件足以说明这条链:

// rename-plugin.mjs
export default function renamePlugin() {
  return {
    name: 'rename-demo',
    visitor: {
      Identifier(path) {
        if (path.node.name === 'oldName') path.node.name = 'newName';
      },
    },
  };
}

如果输入是:

const oldName = 1;
console.log(oldName);

预期结果是标识符都变成 newName,但导入解析、CSS、HTML 都不会出现,因为插件只看当前 AST。

这几个边界要记牢:

概念输入输出边界
parser源码字符串和 parser optionsBabel ASTparser plugin 只允许语法被识别,不负责把语法降级
traverseAST 和 visitor被访问或替换后的 ASTvisitor 必须处理作用域、节点类型和遍历时机
generatorAST、格式选项、输入源码代码、map、raw mappings空格和换行不保证原样保留;Source Map 需要显式开启

错误完整流程:

症状根因观察命令局部修正
SyntaxError: Unexpected tokenparser 没启用对应语法,或源码超出 Babel 当前支持@babel/parser 单独 parse 该文件,缩小到最小语法加对应 parser/plugin/preset,或先用上游工具处理
visitor 不触发前一个 plugin 已经把节点改成另一种类型在 visitor 里打印 path.type,或交换 plugin 顺序把依赖原始语法的 plugin 放到更早的位置
输出格式完全变了generator 不是格式化器,AST 不保存所有排版细节比较 AST 而不是比较空格如果是 codemod,开启 retainLines 或后接 formatter

visitor、plugin、preset 顺序决定看到哪棵 AST

plugin 是一组 visitor。preset 是一组 plugin 的组合。Babel 配置看起来像 JSON,但执行结果依赖顺序。官方 Plugin Ordering 给出的规则是:top-level plugins 先于 presets 执行;plugins 按数组从前到后执行;presets 反向执行,也就是数组最后一个 preset 先运行。

// babel.config.mjs
export default {
  plugins: [
    './plugins/inspect-before-transform.mjs',
    './plugins/replace-feature-flag.mjs',
  ],
  presets: [
    ['@babel/preset-env', { targets: { chrome: '109' }, modules: false }],
    '@babel/preset-typescript',
  ],
};

工程上用三个规则减少误判:

  1. 需要看“原始语法”的 plugin 放前面,例如收集 import、JSX 或 TS 节点。
  2. 不要以为把 @babel/preset-typescript 写在 presets 数组里就会早于 top-level plugin;top-level plugin 一定先跑。
  3. 需要看“已删除 TypeScript 类型”之后的 AST 时,把自定义 plugin 放进一个单独 preset,并通过 preset 反向顺序控制,或拆成第二次 Babel pass。

常见难点完整流程:

症状根因观察命令修正
自定义 plugin 在 TypeScript 类型节点上报错top-level plugin 先于 preset 执行;plugin 扫描 Identifier 时假设所有节点都是运行时 JS,没跳过 TS 类型节点给 visitor 打印 path.type,或在最小输入里只保留一行 type在 visitor 中跳过 TSType*path.isTypeScript() 命中的路径;如果必须在 strip 后运行,把 plugin 放入单独 preset/第二次 Babel pass
转换后 ESM 变 CommonJS,Tree Shaking 变差preset-envmodules 被设为 commonjs 或由 caller 误判搜索产物 require(exports.给 bundler 链路保留 modules: false,让打包器处理 ESM
插件顺序改动后 helper 重复多个阶段各自注入 helper,没有统一 @babel/runtime 策略搜索产物里重复 helper 名应用打包可交给 bundler 去重;库构建再评估 @babel/plugin-transform-runtime

配置发现:先知道 Babel 实际加载了什么

Babel 配置有两类心智模型:

配置位置适用场景monorepo 风险
babel.config.json/js/cjs/mjs/ctsproject-wide config,适合根配置覆盖多个包从子目录执行 CLI 时 root 不一致会导致未命中
.babelrc.*package.json#babelfile-relative config,适合包内局部配置package 边界、忽略规则和根配置叠加容易误判
CLI --config-file示例、CI、临时迁移路径错会完全换一套配置
BABEL_SHOW_CONFIG_FOR调试最终配置只观察一个目标文件,不代表全部文件

共享示例刻意使用显式配置:

pnpm --filter @roadmap/build-tools-slice build:babel

等价脚本来自 examples/build-tools/package.json

babel src --out-dir dist/babel --extensions .ts --config-file ./configs/babel.config.mjs --source-maps --delete-dir-on-start

examples/build-tools/configs/babel.config.mjs 的核心配置是:

export default {
  presets: [
    ['@babel/preset-env', { modules: false, targets: { chrome: '109' } }],
    '@babel/preset-typescript',
  ],
  sourceMaps: true,
};

配置难点完整流程:

症状根因观察命令修正
子包里 Babel “什么都没转”CLI cwd 和 config root 不一致BABEL_SHOW_CONFIG_FOR=examples/build-tools/src/main.ts pnpm --filter @roadmap/build-tools-slice build:babel用显式 --config-file 或统一在仓库根运行
某个文件应用了两套 presetproject-wide config 与 file-relative config 叠加BABEL_SHOW_CONFIG_FOR=<file> 看 config 链删除重复 .babelrc,或用 babelrc: false/overrides 收敛
CI 输出与本地不同envNameNODE_ENV、Browserslist env 不同在 CI 打印 BABEL_ENVNODE_ENV 和最终配置把环境写进脚本,而不是依赖机器默认值

preset-env + Browserslist:兼容组合不是打包策略

preset-env 的问题是:目标环境缺哪些语法支持?它根据目标环境映射启用 transform plugin。Browserslist 的问题是:你的用户环境组合是什么?它们一起决定“哪些语法要降级”。

{
  "browserslist": [
    "> 0.25%",
    "not dead"
  ]
}

如果直接在 Babel 配置里写 targets:

export default {
  targets: 'defaults',
  presets: [
    ['@babel/preset-env', { modules: false }],
  ],
};

边界是:

能力preset-env 会做不会做
语法转换可选链、class fields、private fields 等按 target 降级打包多个文件、生成 chunk
module syntax可按 modules 转成 CJS/AMD/SystemJS 或保留 ESM判断你的库应该发 ESM 还是 CJS
polyfillBabel 7 可通过 useBuiltIns/corejs 注入;Babel 8 迁到 babel-plugin-polyfill-corejs3自动补齐 DOM/Web API、网络权限或 CSS 特性
bugfixBabel 8 中 preset-env 会默认做 bugfix 类转换证明业务代码没有语义 bug

严重问题完整流程:

症状根因观察命令修正
老浏览器报 Map is not defined只做语法转换,没有 runtime polyfill搜索产物是否包含 core-js/modules/es.map应用入口统一引入 polyfill,或 Babel 8 使用 babel-plugin-polyfill-corejs3
打包器无法 tree shakeBabel 先把 ESM 转成 CommonJS`rg "require\(exports\." examples/build-tools/dist/babel`
默认升级 Babel 8 后产物不再是 ES5Babel 8 默认 target 改为 Browserslist defaults,不再默认降到 ES5/CJS对比 Babel 7 和 8 输出里的 arrow/class明确写 targets,不要依赖默认值

语法转换、helper、polyfill、类型检查是四条线

这四件事经常被混成“Babel 已经处理了”。

线示例输出变化负责工具
语法转换obj?.name 降级成条件访问改写当前文件代码Babel transform plugin
helperclass/private field 展开需要复用函数插入 helper 或从 runtime 引用Babel helper / @babel/runtime
polyfillPromiseMapArray.prototype.at注入或要求加载运行时模块core-jsbabel-plugin-polyfill-corejs3、应用入口
类型检查const n: number = 'x'Babel 只删除类型,错误不一定出现tsc --noEmit、框架 checker、IDE

TypeScript preset 的边界示例:

type Tool = { name: string };

export function label(tool: Tool): string {
  return tool.name.toUpperCase();
}

Babel 输出预期结果是删除 type Tool 和参数类型,保留运行时代码:

export function label(tool) {
  return tool.name.toUpperCase();
}

它不会证明 tool 一定有 name。如果调用方传入 {},运行时仍可能失败。

Source Map:让错误回到 .ts,也可能泄露源码

Babel 的 Source Map 来自 generator。它需要三类信息:AST 节点位置、原始源码、上游 inputSourceMap。共享示例开启 sourceMaps: true,CLI 也传了 --source-maps

Source Map 的工程取舍:

策略适用场景风险
external map线上错误需要映射,Sentry/浏览器可加载 .map公开 .map 可能暴露源码和路径
hidden map上传给错误平台,不让浏览器自动请求发布流程要保证 map 和 JS hash 匹配
inline map本地调试、fixture文件体积大,不适合普通生产
no map极高保密或极简发布事故定位成本上升

错误完整流程:

症状根因观察命令修正
DevTools 只能定位到生成后的 JS没生成 .mapsourceMappingURL 丢失tail -n 3 dist/babel/main.js开启 sourceMaps,确认发布保留 .map
map 指向错误文件名sourceFilename 或多输入 generator 参数缺失解析 .map.sources给 generator 传原始源码映射,或保留每文件一进一出
生产泄露工作区路径map sources 包含绝对路径`rg "$PWDfile:" dist/babel`

缓存与 monorepo:缓存键必须包含“会改变输出”的东西

Babel 常被上层工具缓存,例如 babel-loader、Jest、Vite/Rollup 插件或 CI 缓存。缓存命中是否正确,取决于 key 是否覆盖真实输入。

最小缓存键应包含:

const babelCacheKey = [
  process.version,
  '@babel/core@8.0.1',
  '@babel/preset-env@8.0.2',
  '@babel/preset-typescript@8.0.1',
  JSON.stringify(targets),
  JSON.stringify(browserslist),
  JSON.stringify(pluginAndPresetOptions),
  process.env.BABEL_ENV ?? process.env.NODE_ENV ?? 'development',
].join('|');

monorepo 中的懒办法是先不做自研缓存:让上层工具的官方缓存接手,并用 BABEL_SHOW_CONFIG_FOR 确认 config root。只有构建时间被 Babel 明确拖慢,再引入缓存键策略。

缓存难点完整流程:

症状根因观察命令修正
.browserslistrc 后产物不变缓存键漏 Browserslist清缓存前后比较输出把 Browserslist 文件纳入 cache dependency
子包升级 preset 后仍用旧输出缓存键只包含根 package 版本打印实际 @babel/core 和 preset 解析路径key 加入 lockfile hash 或 resolved package version
watch 模式漏外部配置变化loader/plugin 未登记外部依赖修改配置文件看是否触发 rebuild在上层 loader 中登记 config 和 Browserslist 依赖

共享示例:Babel 只转换 TS/JS 模块

运行命令:

pnpm --filter @roadmap/build-tools-slice build:babel

输入:

examples/build-tools/src/
  main.ts          # 浏览器入口,包含 CSS 导入和动态 import
  catalog.ts       # TypeScript 类型、可删除标记、工具列表
  detail.ts        # 动态导入目标
  side-effect.ts   # 必须保留的副作用
  styles.css       # Babel 不处理

预期结果:

examples/build-tools/dist/babel/
  catalog.js
  catalog.js.map
  detail.js
  detail.js.map
  main.js
  main.js.map
  side-effect.js
  side-effect.js.map

不会出现:

index.html
assets/*.css
assets/*.js
stats.json

观察命令:

find examples/build-tools/dist/babel -maxdepth 1 -type f | sort
rg "sourceMappingURL|export type|: Tool|import\\('./detail.ts'\\)" examples/build-tools/dist/babel

预期输出要点:

观察预期
文件列表只有四个 .js 和四个 .js.map
catalog.js保留 ESM export,删除 export type: Tool
main.jsCSS import 仍只是 JS 里的 import 文本,不会生成 CSS 产物
.mapversion 为 3,sources 指回对应 .ts

Babel 7.29 到 Babel 8:迁移不是只改版本号

Babel 官方把 8.0 定位为现代化 major。重大变化包括 ESM-only、Node 版本要求、默认 target、polyfill 注入和 TypeScript preset 选项。

变化Babel 7.29 常见状态Babel 8 状态迁移动作
包格式许多集成仍能 CommonJS require()Babel 包以 native ESM 发布配置改 .mjs 或 ESM;工具侧改动态 import()
Node 版本较旧 Node 仍可能跑 Babel 7要求 Node `^22.18.0
默认 target未配置时更容易输出老语法兼容结果默认使用 Browserslist defaults,不再默认 ES5/CJS显式写 targets,尤其是 legacy 浏览器
polyfillpreset-env useBuiltIns/corejstransform-runtime corejs 常见Babel 8 移除这些注入入口,推荐 babel-plugin-polyfill-corejs3先在 Babel 7 中迁移 polyfill 插件,再升 8
TypeScript presetisTSXallExtensions 组合常见Babel 8 移除 isTSXallExtensionsallowDeclareFields 配置项;.tsx 场景通常可删除前两项,非标准扩展名场景迁到 ignoreExtensions按文件扩展名场景删除或迁移 isTSX/allExtensions;从配置删除 allowDeclareFields,跑 TS/JSX fixture
loose/specpreset-env 里常见preset-env 移除,改 top-level assumptions先在 Babel 7.13+ 引入 assumptions

迁移严重问题完整流程:

症状根因观察命令修正
Error [ERR_REQUIRE_ESM]Babel 8 ESM-only,被旧 CJS 工具 require搜索自研脚本 require('@babel/core')改成 ESM 配置或 await import('@babel/core')
Node 20 CI 失败Babel 8 不支持该 Node 线node -v升到 Node 24 LTS 或官方支持版本
升级后 IE/旧 WebView 白屏默认 target 变现代比较 Babel 输出是否保留 class/arrow明确 targets,不要用默认值承诺 legacy
polyfill 入口不再生效Babel 8 移除 useBuiltIns/corejs 注入选项Babel 报 option removed 或产物无 core-js使用 babel-plugin-polyfill-corejs3 并固定 core-js 版本

插件供应链与历史任意代码执行风险

Babel plugin 和 config 是构建时 JavaScript。安装插件等同于允许它在开发机和 CI 上读文件、读环境变量、发网络请求、改输出。

CVE-2023-45133 的教训不是“普通转译都会 RCE”,而是:如果你在服务端对不可信源码运行 Babel 分析,并调用危险的静态求值能力,编译器 API 也会进入安全边界。官方复盘说明当时 path.evaluate() 曾可被精心构造的输入触发任意代码执行,后续通过 purity 检查和限制继承方法调用修复。

最低安全要求:

风险触发条件最小防线
恶意 plugin安装未知 Babel plugin/presetlockfile、来源审查、最小 CI token、升级审查
不可信源码分析服务端接收用户源码并跑 Babel visitor沙箱进程、禁用危险 API、限制 CPU/内存/网络
环境变量泄露plugin 读取 process.env 并写入产物或日志CI 最小权限、构建密钥隔离、不在 Babel plugin 中处理秘密
缓存污染共享缓存跨分支/跨环境复用key 包含 Node、Babel、config、Browserslist、env

局部修正示例:不要在不可信输入上调用 path.evaluate(),除非你已经把执行环境隔离。

export default function safeVisitor() {
  return {
    visitor: {
      Expression(path, state) {
        if (state.opts.untrustedInput) return;
        const result = path.evaluate();
        if (result.confident) {
          // 只在可信源码上使用静态求值结果。
        }
      },
    },
  };
}

资料入口

在 GitHub 上编辑此页

本页目录