Waveterm 的 Monaco 0.52 → 0.53+ ESM 迁移实战:去除 AMD Loader、接入模块 Worker 与 Vite 分包优化
【免费下载链接】wavetermAn open-source, AI-integrated, cross-platform terminal for seamless workflows项目地址: https://gitcode.com/GitHub_Trending/wa/waveterm
Waveterm 是一个开源、集成 AI、跨平台的终端应用,其前端代码编辑器基于 Monaco Editor 构建。随着 Monaco 0.53 起逐步废弃 AMD 构建,Waveterm 需要在 Vite/Electron 技术栈下完成从 0.52.x(配合@monaco-editor/loader+ AMD 路径映射)到 0.53+ ESM 构建的迁移。本文以仓库内迁移规划文档 aiprompts/monaco-v0.53.md 为主线,结合仓库当前的 Monaco 实现源码,完整讲解迁移动机、逐步骤实施、Worker 接线、分包瘦身、Electron 兼容与回滚预案,帮助读者在自己的 Vite/Electron 项目中复现这套方案。
需要说明的是,该规划文档标注为 “Deferred to next release”(推迟到下一版本),而当前仓库实际已落地了 ESM 方案:
package.json中monaco-editor已升级到^0.55.1并引入monaco-yaml,@monaco-editor/loader已移除。因此本文将同时呈现"规划文档中的标准迁移步骤"与"仓库中已生效的真实实现",两者互为印证。
为什么需要这次迁移
Monaco 0.53 开始弃用 AMD 构建。在旧方案中,代码通过loader.config({ paths: { vs: "monaco" } })做 AMD 路径映射,再依赖@monaco-editor/loader的init()动态加载min/vs/*下的 AMD 产物,最后借助viteStaticCopy把node_modules/monaco-editor/min/vs/*原样拷贝到构建输出。这套链路存在明显问题:
- AMD 路径映射变得脆弱:
paths: { vs: "monaco" }依赖运行时拷贝的目录结构,一旦打包路径或版本内部目录变化就难以排查; - 模块 Worker 需要显式接线:ESM 构建采用 module workers,无法再依赖旧 loader 自动拉起 Worker,必须由应用层通过
MonacoEnvironment.getWorker显式创建; - 捆绑更干净、更利于 CSP/Electron:ESM 方案让 Vite 可以直接参与 Monaco 的模块图(tree-shaking、分包),不再需要大量兼容性 shim,Worker 以独立 chunk 产出,也更容易适配 Electron 打包后的
file://环境与更严格的 CSP 策略。
从仓库现状看,这次迁移已经完成:在 frontend/app/monaco/monaco-env.ts 中已看不到任何loader.config/@monaco-editor/loader引用,取而代之的是window.MonacoEnvironment+ ESM Worker 的直接接线。
高层迁移计划
规划文档给出的整体路线如下:
- 移除 AMD/Loader 相关代码:卸载
@monaco-editor/loader;删除viteStaticCopy对min/vs/*的拷贝;删除loader.config/init调用。 - 安装 Monaco ≥0.53 并接线 ESM Worker:通过
MonacoEnvironment.getWorker为各语言返回模块 Worker。 - 保持主包精简:对 Monaco 设置做懒加载(lazy-load),可选地将 Monaco 单独拆分为独立 chunk。
- Electron/构建适配:在 Vite 配置中保证
base: './',使打包应用中的 Worker URL 在file://下可解析。
对应到当前仓库,前两步已经落地:package.json中的依赖为"monaco-editor": "^0.55.1"与"monaco-yaml": "^5.5.1"(见 package.json);electron.vite.config.ts的 renderer 配置中,manualChunks已把node_modules/monaco与node_modules/@monaco单独拆为monacochunk(见 electron.vite.config.ts),并配置了optimizeDeps.include: ["monaco-yaml/yaml.worker.js"]以预先优化 YAML Worker 依赖。
迁移步骤详解
1) 依赖变更
按规划文档,迁移周期内的依赖操作如下:
# next cycle: npm rm @monaco-editor/loader npm i monaco-editor@^0.53仓库当前已升级到更高版本^0.55.1,同时新增monaco-yaml@^5.5.1用于 YAML 语言服务与 schema 校验,这正是规划文档"Open questions"中"是否需要 JSON/CSS/HTML Worker 进默认包"的实践答案——仓库选择保留全部主要语言(css/html/json/typescript/yaml)并叠加 YAML 支持。
2) 删除 AMD 时代构建配置
- 删除
viteStaticCopy({ targets: [{ src: "node_modules/monaco-editor/min/vs/*", dest: "monaco" }] })。 - 删除运行时初始化代码:
loader.config({ paths: { vs: "monaco" } }); await loader.init();仓库现在的 electron.vite.config.ts 中已不存在任何viteStaticCopy/vite-plugin-static-copy痕迹,Monaco 资源全部交由 Vite 原生模块图处理。
3) 新增 ESM 初始化模块(Worker 接线)
规划文档建议创建monaco-setup.ts,使用new URL(..., import.meta.url)方式创建模块 Worker:
// monaco-setup.ts import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; import "monaco-editor/esm/vs/editor/editor.all.css"; (self as any).MonacoEnvironment = { getWorker(_moduleId: string, label: string) { switch (label) { case "json": return new Worker(new URL("monaco-editor/esm/vs/language/json/json.worker.js", import.meta.url), { type: "module", }); case "css": return new Worker(new URL("monaco-editor/esm/vs/language/css/css.worker.js", import.meta.url), { type: "module", }); case "html": return new Worker(new URL("monaco-editor/esm/vs/language/html/html.worker.js", import.meta.url), { type: "module", }); case "typescript": case "javascript": return new Worker(new URL("monaco-editor/esm/vs/language/typescript/ts.worker.js", import.meta.url), { type: "module", }); default: return new Worker(new URL("monaco-editor/esm/vs/editor/editor.worker.js", import.meta.url), { type: "module" }); } }, }; export { monaco };仓库实际实现 frontend/app/monaco/monaco-env.ts 采用了同一思路、但更贴合 Vite 的另一种写法——?worker导入后缀。Vite 会把每个xxx.worker?worker模块编译为可直接new的 Worker 构造函数并自动产出独立 chunk,等价于文档中new URL(..., import.meta.url)的效果:
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker"; import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker"; import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker"; import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"; import ymlWorker from "./yamlworker?worker"; window.MonacoEnvironment = { getWorker(_, label) { if (label === "json") { return new jsonWorker(); } if (label === "css" || label === "scss" || label === "less") { return new cssWorker(); } if (label === "yaml" || label === "yml") { return new ymlWorker(); } if (label === "html" || label === "handlebars" || label === "razor") { return new htmlWorker(); } if (label === "typescript" || label === "javascript") { return new tsWorker(); } return new editorWorker(); }, };注意两点差异:
- 仓库把 Worker 文件声明在
MonacoEnvironment.getWorker之外,借助 Vite 的?worker静态导入让每个 Worker 成为独立 chunk,避免运行时动态构造 URL 的不确定性; getWorker(_, label)的_moduleId参数被忽略,仅以label分发,并且为scss/less复用了 css worker、为handlebars/razor复用了 html worker、为yaml/yml使用自定义的 yamlworker.js(对应monaco-yaml的 worker)。
monaco-env.ts还在loadMonaco()中完成了一系列一次性初始化(通过monacoConfigured标志保证幂等):
- 用
monaco.editor.defineTheme定义两套主题:wave-theme-dark(基于vs-dark,编辑器背景设为透明#00000000,便于透出终端背景层)与wave-theme-light(基于vs,背景#fefefe); - 调用
configureMonacoYaml(monaco, { validate: true, schemas: [] })启用 YAML 校验; monaco.typescript.typescriptDefaults.setDiagnosticsOptions({ noSemanticValidation: true })关闭 TS/JS 的默认语义校验(避免误报干扰终端场景);monaco.json.jsonDefaults.setDiagnosticsOptions(...)注册 JSON 诊断与 schema,schemas: MonacoSchemas来自 frontend/app/monaco/schemaendpoints.ts。
4) 在使用处懒加载
规划文档建议在编辑器 UI 挂载点做动态导入,避免启动时加载 Monaco:
// where the editor UI mounts const { monaco } = await import("./monaco-setup"); const editor = monaco.editor.create(container, { language: "javascript", value: "" });仓库采用"模块内幂等初始化 + React 生命周期挂载"的组合方式:frontend/app/monaco/monaco-react.tsx 导出了两个组件MonacoCodeEditor与MonacoDiffViewer:
- 两者在
useEffect挂载时调用loadMonaco(),随后用monaco.editor.create/monaco.editor.createDiffEditor创建实例; - 模型使用自定义 scheme:
monaco.Uri.parse("wave://editor/" + encodeURIComponent(path)),diff 视图则用wave://diff/...orig/wave://diff/...mod两个 URI 分别承载原始与修改内容; - 组件通过
ResizeObserver+ 100msdebounce触发editor.layout(),保证容器尺寸变化时编辑器及时重排; - 卸载时依次
setModel(null)、dispose(),并销毁模型,规避反复打开/关闭导致的内存增长(对应规划文档测试清单中的 "Hot paths" 项)。
组件被 frontend/app/view/codeeditor/codeeditor.tsx 与 frontend/app/view/codeeditor/diffviewer.tsx 引用,即 Waveterm 的代码编辑视图与差异对比视图。
5) 可选:将 Monaco 隔离为独立 chunk
规划文档给出vite.config.ts的manualChunks写法:
import { defineConfig } from "vite"; export default defineConfig({ base: "./", // important for Electron packaged apps build: { rollupOptions: { output: { manualChunks(id) { if (id.includes("node_modules/monaco-editor")) return "monaco"; }, }, }, }, });仓库在 electron.vite.config.ts 的 renderer 段落地了同样策略,且覆盖范围更广,把多个体积较大的第三方库都隔离为独立 chunk:
output: { manualChunks(id) { const p = id.replace(/\\/g, "/"); if (p.includes("node_modules/monaco") || p.includes("node_modules/@monaco")) return "monaco"; if (p.includes("node_modules/mermaid") || p.includes("node_modules/@mermaid")) return "mermaid"; if (p.includes("node_modules/katex") || p.includes("node_modules/@katex")) return "katex"; if (p.includes("node_modules/shiki") || p.includes("node_modules/@shiki")) return "shiki"; if (p.includes("node_modules/cytoscape") || p.includes("node_modules/@cytoscape")) return "cytoscape"; return undefined; }, },manualChunks匹配同时覆盖node_modules/monaco与node_modules/@monaco前缀,说明仓库同时安装了monaco-editor与@monaco-*相关包(monaco-yaml 等),统一归入monacochunk。规划文档特别提醒:通过new URL(..., import.meta.url)(或 Vite 的?worker)创建的 Worker 会被自动产出为独立 chunk,无需在manualChunks中手工处理。
包体积控制(按需取舍)
规划文档给出四个控制维度,仓库实现也逐一印证:
- 只 import
editor.api而非完整editor:monaco-env.ts中直接import * as monaco from "monaco-editor",配合语言 contribution 的显式esm/vs/language/*/monaco.contribution导入,只引入用到的语言特性; - 只保留用到的 Worker:仓库保留了 css/html/json/typescript/editor 五个核心 Worker 并新增 yaml worker,若你的项目不需要某种语言,直接删掉对应
?worker导入与其getWorker分支即可; - 用
import()懒加载 Monaco:仓库把初始化收敛在loadMonaco(),由组件挂载时触发,Monaco 相关 chunk 不会阻塞应用首屏; - 按需动态导入语言贡献:规划文档示例:
if (lang === "json") { await import("monaco-editor/esm/vs/language/json/monaco.contribution"); }这与仓库中"静态引入所有需要的 contribution"是同一机制的两端——静态引入保稳定,动态引入省体积,可按语言使用频率权衡。
另外,仓库的 JSON schema 校验是"体积控制"与"功能增强"结合的典范:frontend/app/monaco/schemaendpoints.ts 从仓库 schema 目录直接导入settings.json、connections.json、aipresets.json、backgrounds.json、waveai.json、widgets.json六个 schema 文件,构造出{ uri, fileMatch, schema }三元组数组,再注册到monaco.json.jsonDefaults。例如:
{ uri: "wave://schema/settings.json", fileMatch: ["*/WAVECONFIGPATH/settings.json"], schema: settingsSchema, }, { uri: "wave://schema/connections.json", fileMatch: ["*/WAVECONFIGPATH/connections.json"], schema: connectionsSchema, },这意味着 Waveterm 用户在编辑自己的settings.json、connections.json等配置文件时,编辑器内会直接获得基于官方 schema 的自动补全与实时校验。
Electron 特定事项
规划文档列出三条 Electron 关键约束,均已在仓库配置中体现:
base: './':electron.vite.config.ts使用electron-vite的defineConfig,其 renderer 构建天然面向相对路径输出(产物位于dist/frontend),配合打包后的file://协议,Worker 与资源 URL 均按相对路径解析,这正是规划文档强调base: './'的目的;{ type: 'module' }必须显式指定:Monaco ESM Worker 必须以 module 方式创建。仓库通过 Vite?worker后缀自动生成模块 Worker(Vite 内部即为new Worker(..., { type: "module" })),语义与规划文档一致;- 避免 blob URL、兼容严格 CSP:ESM Worker 以独立文件 chunk 形式产出,不走
blob:内联,因此在开启了严格 CSP 的 Electron 窗口中更容易被放行(仍可按规划文档 Open questions 提示,在正式环境确认worker-src指令)。
仓库另外还设置了optimizeDeps.include: ["monaco-yaml/yaml.worker.js"](见 electron.vite.config.ts),让 YAML Worker 在开发服务器预构建阶段即被优化,避免 dev 模式下首次请求时的二次编译与 404。
测试清单与验收
规划文档给出三组验收点,可直接作为迁移完成的标准:
- 开发环境(Dev):编辑器正常渲染;Worker 脚本无 404;语言服务生效(TS hover/诊断、JSON schema 补全)。对应仓库中的 schemaendpoints 注册逻辑,可在编辑 WAVECONFIGPATH 下的 JSON 文件时直接验证;
- 生产构建(Prod build):确认 Worker 文件已产出到
dist/frontend;打开打包后的 Electron 应用确认 Worker 正常加载,控制台不出现Cannot use import statement outside a module(该报错通常是 module Worker 被当作 classic Worker 创建所致); - 热路径(Hot paths):反复打开/关闭编辑器(含 diff 视图),观察内存不无限增长。仓库组件在卸载时
setModel(null)+dispose()模型与编辑器实例,即为应对此验收点。
回滚预案
规划文档为迁移失败提供了明确的回滚路径,仅两步即可回到 0.52 时代:
npm i monaco-editor@0.52.x npm i -D @monaco-editor/loader随后恢复viteStaticCopy对min/vs/*的拷贝块与loader.config/init调用。因为当前仓库已完整落地 ESM 方案并升级到^0.55.1,回滚预案主要面向仍在 0.52.x 上的其他分支或历史版本,属于标准的发布安全网。
开放问题与取舍建议
规划文档留下的两个开放问题,仓库已经给出了实际答案,可作为其他项目迁移时的决策参考:
- JSON/CSS/HTML Worker 是否进默认包?仓库选择全部保留,且新增了 YAML Worker——因为 Waveterm 的编辑器场景(配置编辑、代码编辑、diff 对比)需要完整语言服务;若你的项目只编辑单一语言,按需裁剪可进一步减包;
- 生产环境 CSP 是否有额外限制?仓库未引入 blob URL,全部依赖独立 chunk 的模块 Worker,从实现上规避了大部分 CSP 冲突;上线前仍建议在生产环境核对
script-src与worker-src指令。
速查清单
- Worker 接线:frontend/app/monaco/monaco-env.ts(
?worker导入 +MonacoEnvironment.getWorker+ 主题/诊断/YAML/JSON schema 初始化) - 编辑器与 Diff 组件:frontend/app/monaco/monaco-react.tsx(
MonacoCodeEditor/MonacoDiffViewer,wave://scheme 模型) - JSON schema 来源:frontend/app/monaco/schemaendpoints.ts + schema 目录下的六个 JSON schema
- Vite 分包与依赖预优化:electron.vite.config.ts(
manualChunks中monacochunk、optimizeDeps.include) - 依赖版本:package.json(
monaco-editor@^0.55.1、monaco-yaml@^5.5.1)
如果你正在维护一个 Vite + Electron 应用并受困于 Monaco 的 AMD/loader 链路,照此方案移除 loader、以MonacoEnvironment.getWorker接线 ESM Worker、用manualChunks隔离 Monaco chunk,再以base: './'适配打包产物,即可在保留全部语言服务能力的同时获得更干净、更易维护、更利于 CSP 的构建结果。
【免费下载链接】wavetermAn open-source, AI-integrated, cross-platform terminal for seamless workflows项目地址: https://gitcode.com/GitHub_Trending/wa/waveterm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考