From 275611dbf57936da4f21d00520c8112c2fb8d03a Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Sun, 7 Jun 2026 01:25:09 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20share=20LiteXM?= =?UTF-8?q?L=20command=20identities=20across=20bundles=20via=20a=20pure=20?= =?UTF-8?q?module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The package ships two independently-bundled entries — the browser build (`index`/`react`/`renderer`) and the node build (`headless`). Each previously inlined its own `createCommand(...)` call for the LiteXML commands, so a command obtained from one entry and dispatched onto an editor registered by the other silently no-ops (Lexical matches command listeners by object reference, not by the string label). Isolate every LiteXML command identity (+ the `DiffAction` enum) into a new side-effect-free `plugins/litexml/command/symbols.ts`, re-exported from the existing command modules so nothing else changes. Emit it as a dedicated chunk in the node build and expose it through a new `@lobehub/editor/litexml-commands` subpath, so: - both bundles import the same emitted `symbols.js` → one runtime identity; - the commands can be imported on the server without pulling the DOM-dependent editor bundle (the module only imports `createCommand` from `lexical`). This lets consumers dispatch `LITEXML_*_COMMAND` directly on a `HeadlessEditor` kernel instead of routing through wrapper methods. Co-Authored-By: Claude Opus 4.8 --- package.json | 4 ++ src/plugins/litexml/command/diffCommand.ts | 26 ++------ src/plugins/litexml/command/index.ts | 58 +++++----------- src/plugins/litexml/command/symbols.ts | 78 ++++++++++++++++++++++ tsdown.config.ts | 8 ++- 5 files changed, 110 insertions(+), 64 deletions(-) create mode 100644 src/plugins/litexml/command/symbols.ts diff --git a/package.json b/package.json index 4a032232..87c282d2 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,10 @@ "types": "./es/headless.d.ts", "import": "./es/headless.js" }, + "./litexml-commands": { + "types": "./es/plugins/litexml/command/symbols.d.ts", + "import": "./es/plugins/litexml/command/symbols.js" + }, "./react": { "types": "./es/react.d.ts", "import": "./es/react.js" diff --git a/src/plugins/litexml/command/diffCommand.ts b/src/plugins/litexml/command/diffCommand.ts index 1dc57614..3cc08094 100644 --- a/src/plugins/litexml/command/diffCommand.ts +++ b/src/plugins/litexml/command/diffCommand.ts @@ -1,26 +1,8 @@ import { mergeRegister } from '@lexical/utils'; -import { - $getNodeByKey, - $isElementNode, - COMMAND_PRIORITY_EDITOR, - LexicalEditor, - createCommand, -} from 'lexical'; +import { $getNodeByKey, $isElementNode, COMMAND_PRIORITY_EDITOR, LexicalEditor } from 'lexical'; import { DiffNode } from '../node/DiffNode'; - -export enum DiffAction { - Reject, - Accept, -} - -export const LITEXML_DIFFNODE_COMMAND = createCommand<{ action: DiffAction; nodeKey: string }>( - 'LITEXML_DIFFNODE_COMMAND', -); - -export const LITEXML_DIFFNODE_ALL_COMMAND = createCommand<{ action: DiffAction }>( - 'LITEXML_DIFFNODE_ALL_COMMAND', -); +import { DiffAction, LITEXML_DIFFNODE_ALL_COMMAND, LITEXML_DIFFNODE_COMMAND } from './symbols'; function doAction(node: DiffNode, action: DiffAction) { if (node.diffType === 'modify') { @@ -146,3 +128,7 @@ export function registerLiteXMLDiffCommand(editor: LexicalEditor) { ), ); } + +// Command identities and the `DiffAction` enum live in the side-effect-free +// `./symbols` module so they stay single-instance across the package bundles. +export { DiffAction, LITEXML_DIFFNODE_ALL_COMMAND, LITEXML_DIFFNODE_COMMAND } from './symbols'; diff --git a/src/plugins/litexml/command/index.ts b/src/plugins/litexml/command/index.ts index 4d559b24..d9d68a7d 100644 --- a/src/plugins/litexml/command/index.ts +++ b/src/plugins/litexml/command/index.ts @@ -10,7 +10,6 @@ import { COMMAND_PRIORITY_EDITOR, LexicalEditor, LexicalNode, - createCommand, } from 'lexical'; import { $closest } from '@/editor-kernel'; @@ -19,6 +18,12 @@ import { createDebugLogger } from '@/utils/debug'; import type LitexmlDataSource from '../data-source/litexml-data-source'; import { $createDiffNode, DiffNode } from '../node/DiffNode'; import { $cloneNode, $parseSerializedNodeImpl, charToId } from '../utils'; +import { + LITEXML_APPLY_COMMAND, + LITEXML_INSERT_COMMAND, + LITEXML_MODIFY_COMMAND, + LITEXML_REMOVE_COMMAND, +} from './symbols'; const logger = createDebugLogger('plugin', 'litexml'); @@ -138,48 +143,6 @@ function wrapBlockModify(oldBlock: LexicalNode, editor: LexicalEditor, changeFn: newBlock.replace(diffNode, false); } -export const LITEXML_MODIFY_COMMAND = createCommand< - Array< - | { - action: 'insert'; - beforeId: string; - litexml: string; - } - | { - action: 'insert'; - afterId: string; - litexml: string; - } - | { - action: 'remove'; - id: string; - } - | { - action: 'modify'; - litexml: string | string[]; - } - > ->('LITEXML_MODIFY_COMMAND'); - -export const LITEXML_APPLY_COMMAND = createCommand<{ delay?: boolean; litexml: string | string[] }>( - 'LITEXML_APPLY_COMMAND', -); -export const LITEXML_REMOVE_COMMAND = createCommand<{ delay?: boolean; id: string }>( - 'LITEXML_REMOVE_COMMAND', -); -export const LITEXML_INSERT_COMMAND = createCommand< - | { - beforeId: string; - delay?: boolean; - litexml: string; - } - | { - afterId: string; - delay?: boolean; - litexml: string; - } ->('LITEXML_INSERT_COMMAND'); - export function registerLiteXMLCommand(editor: LexicalEditor, dataSource: LitexmlDataSource) { return mergeRegister( editor.registerCommand( @@ -570,3 +533,12 @@ function handleInsert( } }); } + +// Command identities live in the side-effect-free `./symbols` module so they +// keep a single runtime identity across the package's browser/node bundles. +export { + LITEXML_APPLY_COMMAND, + LITEXML_INSERT_COMMAND, + LITEXML_MODIFY_COMMAND, + LITEXML_REMOVE_COMMAND, +} from './symbols'; diff --git a/src/plugins/litexml/command/symbols.ts b/src/plugins/litexml/command/symbols.ts new file mode 100644 index 00000000..cf722c61 --- /dev/null +++ b/src/plugins/litexml/command/symbols.ts @@ -0,0 +1,78 @@ +import { createCommand } from 'lexical'; + +/** + * LiteXML command identities. + * + * These symbols are intentionally isolated in a side-effect-free module so they + * keep a SINGLE runtime identity across every entry of this package. + * + * Lexical's `dispatchCommand` matches command listeners by object reference, not + * by the string label. The package ships two independently-bundled entries — the + * browser build (`index` / `react` / `renderer`) and the node build (`headless`) + * — and if each entry inlined its own `createCommand(...)` call, dispatching a + * command obtained from one entry onto an editor registered by the other would + * silently no-op (different object identities, same label). + * + * By keeping every command in this one module — exposed verbatim through + * `@lobehub/editor/litexml-commands` and emitted as a shared chunk by both + * builds — a single object backs the command in any runtime, and the module is + * pure enough to be imported on the server without pulling in the DOM-dependent + * editor bundle. + */ + +export enum DiffAction { + Reject, + Accept, +} + +export const LITEXML_MODIFY_COMMAND = createCommand< + Array< + | { + action: 'insert'; + beforeId: string; + litexml: string; + } + | { + action: 'insert'; + afterId: string; + litexml: string; + } + | { + action: 'remove'; + id: string; + } + | { + action: 'modify'; + litexml: string | string[]; + } + > +>('LITEXML_MODIFY_COMMAND'); + +export const LITEXML_APPLY_COMMAND = createCommand<{ delay?: boolean; litexml: string | string[] }>( + 'LITEXML_APPLY_COMMAND', +); + +export const LITEXML_REMOVE_COMMAND = createCommand<{ delay?: boolean; id: string }>( + 'LITEXML_REMOVE_COMMAND', +); + +export const LITEXML_INSERT_COMMAND = createCommand< + | { + beforeId: string; + delay?: boolean; + litexml: string; + } + | { + afterId: string; + delay?: boolean; + litexml: string; + } +>('LITEXML_INSERT_COMMAND'); + +export const LITEXML_DIFFNODE_COMMAND = createCommand<{ action: DiffAction; nodeKey: string }>( + 'LITEXML_DIFFNODE_COMMAND', +); + +export const LITEXML_DIFFNODE_ALL_COMMAND = createCommand<{ action: DiffAction }>( + 'LITEXML_DIFFNODE_ALL_COMMAND', +); diff --git a/tsdown.config.ts b/tsdown.config.ts index b8405f2a..a31e584a 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -14,7 +14,13 @@ export default defineConfig([ ...commonConfig, clean: true, entry: { - headless: 'src/headless/index.ts', + 'headless': 'src/headless/index.ts', + // Emit the LiteXML command identities as their own chunk so the bundled + // node build references them instead of inlining a second copy. Both this + // entry and the unbundled browser build resolve to the same emitted + // `es/plugins/litexml/command/symbols.js`, giving the commands a single + // runtime identity (and a DOM-free import via `./litexml-commands`). + 'plugins/litexml/command/symbols': 'src/plugins/litexml/command/symbols.ts', }, outExtensions: () => ({ dts: '.d.ts', js: '.js' }), platform: 'node',