Skip to content

[Bug] @lobehub/editor 主入口任何 value import 都会在 Node 崩;headless 入口未导出 LITEXML_*_COMMAND #164

Description

@arvinxx

复现版本

@lobehub/editor@4.12.0

背景

在 LobeHub 的 page-agent 工具里需要在 server-side (Node) 跑 HeadlessEditor,期望复用 @lobechat/editor-runtime 里同一份 Lexical 调度逻辑(同时给 renderer 和 server 用)。落地时碰到两个上游问题,记录如下。

问题 1:主入口任何 value import 都在 Node 崩

只要在 Node 环境写

import { LITEXML_MODIFY_COMMAND } from '@lobehub/editor';

就会立刻 throw:

ReferenceError: document is not defined
 ❯ node_modules/.pnpm/@lobehub+editor@4.12.0_…/node_modules/@lobehub/editor/es/ReactSlashPlugin-DkEXwDgH.js:346:17
    344| //#endregion
    345| //#region node_modules/.pnpm/decode-named-character-reference@1.3.0/no…
    346| const element = document.createElement(\"i\");
       |                 ^
    347| /**
    348| * @param {string} value
 ❯ node_modules/.pnpm/@lobehub+editor@4.12.0_…/node_modules/@lobehub/editor/es/index.js:2:1

index.js 顶部直接 import { ... } from \"./ReactSlashPlugin-DkEXwDgH.js\",而那个 chunk 在模块顶层直接 document.createElement(\"i\")

package.json 里写了 \"sideEffects\": [\"**/*.css\"],理论上 tree-shake 能掉所有 JS 模块的副作用,但 bundling 把 LITEXML_*_COMMAND 这种纯常量和 ReactSlashPlugin 打到了一起 / chunk 顶层就有 DOM 调用,bundler 没法消除。结果是:主入口 ESM 实际上不是 side-effect free,只要 evaluate 就会触发 DOM。

期望行为

主入口至少满足下面之一:

  1. 模块顶层不触碰 document / window,DOM 调用 lazy 到运行时
  2. ReactSlashPlugin 等 DOM-only 的实现独立成 chunk,让纯常量 (LITEXML_*_COMMANDtype IEditor 等) 不会拖进来
  3. 主入口明确只为 browser,文档说明 Node 必须走 ./headless,并把 server 需要的所有 symbol 都从 ./headless 导出(见问题 2)

问题 2:./headless 入口未导出 LITEXML_*_COMMAND

es/headless.js 内部其实定义了:

const LITEXML_MODIFY_COMMAND = createCommand(\"LITEXML_MODIFY_COMMAND\");
const LITEXML_APPLY_COMMAND  = createCommand(\"LITEXML_APPLY_COMMAND\");

但模块底部 export 只有:

export { DEFAULT_HEADLESS_EDITOR_PLUGINS, HeadlessEditor, createHeadlessEditor };

d.ts 同样不导出这两个常量。

这导致 server 侧拿不到这两个 command 常量来对 headless.kernel 直接 dispatch,被迫只能走 headless.applyLiteXMLBatch(...) / headless.applyLiteXML({ action: 'apply', litexml }) 这种高层方法(不灵活、表达力弱于 dispatchCommand)。

期望行为

./headless 导出 LITEXML_MODIFY_COMMAND / LITEXML_APPLY_COMMAND(以及任何其他 server 侧需要 dispatch 的命令常量),让 server 能像 renderer 一样 kernel.dispatchCommand(LITEXML_*, payload)

问题 3 (派生):pnpm 双副本下命令 identity 不一致

即便强行从主入口导入并在 Node 用 try/catch 绕开 document 报错,还有一层语义陷阱:

createCommand(name) 返回 { type: name } —— 同名调两次产生两个不同对象

@lobehub/editor (主入口) 和 @lobehub/editor/headless 是两份独立 bundle,各自内部都 createCommand(\"LITEXML_APPLY_COMMAND\") —— pnpm 解析到的 module copy 不同时,两者的 LITEXML_APPLY_COMMAND 是不同对象。

后果:

import { LITEXML_APPLY_COMMAND } from '@lobehub/editor';        // 主入口的 symbol A
import { createHeadlessEditor } from '@lobehub/editor/headless'; // 内部注册的是 symbol B
const headless = createHeadlessEditor();
headless.kernel.dispatchCommand(LITEXML_APPLY_COMMAND, { litexml: ['<p>x</p>'] });
// dispatchCommand 返 false,无 handler 匹配,编辑器没动,没报错 —— 静默 no-op

这是相当隐蔽的 bug:工具调用流程返回 success,但内容没变,需要业务层加 hash diff 才能发现。

如果问题 2 修了(headless 也导出常量),并且约束 server 必须从 ./headless 导入,问题 3 在常见场景下也就消失了 —— 同一个 module 内部的 symbol 一致。

当前 workaround

我们在下游做了 strategy adapter:

// renderer 侧
runtime.setLiteXMLAdapter({
  applyBatch:  (editor, ops)     => editor.dispatchCommand(LITEXML_MODIFY_COMMAND, ops),
  applyReplace:(editor, litexml) => editor.dispatchCommand(LITEXML_APPLY_COMMAND, { litexml }),
});

// server 侧 —— 不导入主入口,走 headless 的高层方法绕过常量缺失 + identity 问题
runtime.setLiteXMLAdapter({
  applyBatch:  async (_e, ops)     => { await headless.applyLiteXMLBatch(ops); },
  applyReplace:async (_e, litexml) => { await headless.applyLiteXML({ action: 'apply', litexml }); },
});

能跑,但代码上多了一层不必要的间接。上游修了 1+2 之后这层 adapter 可以直接退役。

建议优先级

  • P0:问题 1(任何 server-side / SSR / build-tool 静态分析场景都会踩到)
  • P0:问题 2(让 server 能正常用 headless 而不用奇技淫巧)
  • P1:问题 3(修了 1+2 之后基本消失,剩下的可以靠文档"server 必须只从 ./headless 导入"约束)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions