Skip to content

Commit f7d70c1

Browse files
arvinxxclaude
andauthored
♻️ refactor: share LiteXML command identities across bundles via a pure module (#168)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a54d13a commit f7d70c1

5 files changed

Lines changed: 110 additions & 64 deletions

File tree

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@
3434
"types": "./es/headless.d.ts",
3535
"import": "./es/headless.js"
3636
},
37+
"./litexml-commands": {
38+
"types": "./es/plugins/litexml/command/symbols.d.ts",
39+
"import": "./es/plugins/litexml/command/symbols.js"
40+
},
3741
"./react": {
3842
"types": "./es/react.d.ts",
3943
"import": "./es/react.js"

src/plugins/litexml/command/diffCommand.ts

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,8 @@
11
import { mergeRegister } from '@lexical/utils';
2-
import {
3-
$getNodeByKey,
4-
$isElementNode,
5-
COMMAND_PRIORITY_EDITOR,
6-
LexicalEditor,
7-
createCommand,
8-
} from 'lexical';
2+
import { $getNodeByKey, $isElementNode, COMMAND_PRIORITY_EDITOR, LexicalEditor } from 'lexical';
93

104
import { DiffNode } from '../node/DiffNode';
11-
12-
export enum DiffAction {
13-
Reject,
14-
Accept,
15-
}
16-
17-
export const LITEXML_DIFFNODE_COMMAND = createCommand<{ action: DiffAction; nodeKey: string }>(
18-
'LITEXML_DIFFNODE_COMMAND',
19-
);
20-
21-
export const LITEXML_DIFFNODE_ALL_COMMAND = createCommand<{ action: DiffAction }>(
22-
'LITEXML_DIFFNODE_ALL_COMMAND',
23-
);
5+
import { DiffAction, LITEXML_DIFFNODE_ALL_COMMAND, LITEXML_DIFFNODE_COMMAND } from './symbols';
246

257
function doAction(node: DiffNode, action: DiffAction) {
268
if (node.diffType === 'modify') {
@@ -146,3 +128,7 @@ export function registerLiteXMLDiffCommand(editor: LexicalEditor) {
146128
),
147129
);
148130
}
131+
132+
// Command identities and the `DiffAction` enum live in the side-effect-free
133+
// `./symbols` module so they stay single-instance across the package bundles.
134+
export { DiffAction, LITEXML_DIFFNODE_ALL_COMMAND, LITEXML_DIFFNODE_COMMAND } from './symbols';

src/plugins/litexml/command/index.ts

Lines changed: 15 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
COMMAND_PRIORITY_EDITOR,
1111
LexicalEditor,
1212
LexicalNode,
13-
createCommand,
1413
} from 'lexical';
1514

1615
import { $closest } from '@/editor-kernel';
@@ -19,6 +18,12 @@ import { createDebugLogger } from '@/utils/debug';
1918
import type LitexmlDataSource from '../data-source/litexml-data-source';
2019
import { $createDiffNode, DiffNode } from '../node/DiffNode';
2120
import { $cloneNode, $parseSerializedNodeImpl, charToId } from '../utils';
21+
import {
22+
LITEXML_APPLY_COMMAND,
23+
LITEXML_INSERT_COMMAND,
24+
LITEXML_MODIFY_COMMAND,
25+
LITEXML_REMOVE_COMMAND,
26+
} from './symbols';
2227

2328
const logger = createDebugLogger('plugin', 'litexml');
2429

@@ -138,48 +143,6 @@ function wrapBlockModify(oldBlock: LexicalNode, editor: LexicalEditor, changeFn:
138143
newBlock.replace(diffNode, false);
139144
}
140145

141-
export const LITEXML_MODIFY_COMMAND = createCommand<
142-
Array<
143-
| {
144-
action: 'insert';
145-
beforeId: string;
146-
litexml: string;
147-
}
148-
| {
149-
action: 'insert';
150-
afterId: string;
151-
litexml: string;
152-
}
153-
| {
154-
action: 'remove';
155-
id: string;
156-
}
157-
| {
158-
action: 'modify';
159-
litexml: string | string[];
160-
}
161-
>
162-
>('LITEXML_MODIFY_COMMAND');
163-
164-
export const LITEXML_APPLY_COMMAND = createCommand<{ delay?: boolean; litexml: string | string[] }>(
165-
'LITEXML_APPLY_COMMAND',
166-
);
167-
export const LITEXML_REMOVE_COMMAND = createCommand<{ delay?: boolean; id: string }>(
168-
'LITEXML_REMOVE_COMMAND',
169-
);
170-
export const LITEXML_INSERT_COMMAND = createCommand<
171-
| {
172-
beforeId: string;
173-
delay?: boolean;
174-
litexml: string;
175-
}
176-
| {
177-
afterId: string;
178-
delay?: boolean;
179-
litexml: string;
180-
}
181-
>('LITEXML_INSERT_COMMAND');
182-
183146
export function registerLiteXMLCommand(editor: LexicalEditor, dataSource: LitexmlDataSource) {
184147
return mergeRegister(
185148
editor.registerCommand(
@@ -570,3 +533,12 @@ function handleInsert(
570533
}
571534
});
572535
}
536+
537+
// Command identities live in the side-effect-free `./symbols` module so they
538+
// keep a single runtime identity across the package's browser/node bundles.
539+
export {
540+
LITEXML_APPLY_COMMAND,
541+
LITEXML_INSERT_COMMAND,
542+
LITEXML_MODIFY_COMMAND,
543+
LITEXML_REMOVE_COMMAND,
544+
} from './symbols';
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { createCommand } from 'lexical';
2+
3+
/**
4+
* LiteXML command identities.
5+
*
6+
* These symbols are intentionally isolated in a side-effect-free module so they
7+
* keep a SINGLE runtime identity across every entry of this package.
8+
*
9+
* Lexical's `dispatchCommand` matches command listeners by object reference, not
10+
* by the string label. The package ships two independently-bundled entries — the
11+
* browser build (`index` / `react` / `renderer`) and the node build (`headless`)
12+
* — and if each entry inlined its own `createCommand(...)` call, dispatching a
13+
* command obtained from one entry onto an editor registered by the other would
14+
* silently no-op (different object identities, same label).
15+
*
16+
* By keeping every command in this one module — exposed verbatim through
17+
* `@lobehub/editor/litexml-commands` and emitted as a shared chunk by both
18+
* builds — a single object backs the command in any runtime, and the module is
19+
* pure enough to be imported on the server without pulling in the DOM-dependent
20+
* editor bundle.
21+
*/
22+
23+
export enum DiffAction {
24+
Reject,
25+
Accept,
26+
}
27+
28+
export const LITEXML_MODIFY_COMMAND = createCommand<
29+
Array<
30+
| {
31+
action: 'insert';
32+
beforeId: string;
33+
litexml: string;
34+
}
35+
| {
36+
action: 'insert';
37+
afterId: string;
38+
litexml: string;
39+
}
40+
| {
41+
action: 'remove';
42+
id: string;
43+
}
44+
| {
45+
action: 'modify';
46+
litexml: string | string[];
47+
}
48+
>
49+
>('LITEXML_MODIFY_COMMAND');
50+
51+
export const LITEXML_APPLY_COMMAND = createCommand<{ delay?: boolean; litexml: string | string[] }>(
52+
'LITEXML_APPLY_COMMAND',
53+
);
54+
55+
export const LITEXML_REMOVE_COMMAND = createCommand<{ delay?: boolean; id: string }>(
56+
'LITEXML_REMOVE_COMMAND',
57+
);
58+
59+
export const LITEXML_INSERT_COMMAND = createCommand<
60+
| {
61+
beforeId: string;
62+
delay?: boolean;
63+
litexml: string;
64+
}
65+
| {
66+
afterId: string;
67+
delay?: boolean;
68+
litexml: string;
69+
}
70+
>('LITEXML_INSERT_COMMAND');
71+
72+
export const LITEXML_DIFFNODE_COMMAND = createCommand<{ action: DiffAction; nodeKey: string }>(
73+
'LITEXML_DIFFNODE_COMMAND',
74+
);
75+
76+
export const LITEXML_DIFFNODE_ALL_COMMAND = createCommand<{ action: DiffAction }>(
77+
'LITEXML_DIFFNODE_ALL_COMMAND',
78+
);

tsdown.config.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ export default defineConfig([
1414
...commonConfig,
1515
clean: true,
1616
entry: {
17-
headless: 'src/headless/index.ts',
17+
'headless': 'src/headless/index.ts',
18+
// Emit the LiteXML command identities as their own chunk so the bundled
19+
// node build references them instead of inlining a second copy. Both this
20+
// entry and the unbundled browser build resolve to the same emitted
21+
// `es/plugins/litexml/command/symbols.js`, giving the commands a single
22+
// runtime identity (and a DOM-free import via `./litexml-commands`).
23+
'plugins/litexml/command/symbols': 'src/plugins/litexml/command/symbols.ts',
1824
},
1925
outExtensions: () => ({ dts: '.d.ts', js: '.js' }),
2026
platform: 'node',

0 commit comments

Comments
 (0)