@astryxdesign/richtext 0.6.0-canary.f574abd → 0.6.1-canary.09decfb

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * @file RichTextEditor.tsx
3
- * @input Uses React, useId, Lexical (lexical + @lexical/react), Field,
4
- * VisuallyHidden, useInputStatusIcon, mergeProps, design tokens
3
+ * @input Uses React, useId, Lexical (lexical + @lexical/react, composed through
4
+ * LexicalExtensionComposer), Field, VisuallyHidden, useInputStatusIcon,
5
+ * mergeProps, design tokens
5
6
  * @output Exports an accessibly labelled RichTextEditor component with a flush
6
7
  * top toolbar slot and configurable editable-surface minimum height, RichTextEditorProps,
7
8
  * RichTextEditorStatus, RichTextEditorStatusType, RichTextEditorSize
@@ -1 +1 @@
1
- {"version":3,"file":"RichTextEditor.d.ts","sourceRoot":"","sources":["../src/RichTextEditor.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"RichTextEditor.d.ts","sourceRoot":"","sources":["../src/RichTextEditor.tsx"],"names":[],"mappings":"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"}
@@ -4,8 +4,9 @@
4
4
 
5
5
  /**
6
6
  * @file RichTextEditor.tsx
7
- * @input Uses React, useId, Lexical (lexical + @lexical/react), Field,
8
- * VisuallyHidden, useInputStatusIcon, mergeProps, design tokens
7
+ * @input Uses React, useId, Lexical (lexical + @lexical/react, composed through
8
+ * LexicalExtensionComposer), Field, VisuallyHidden, useInputStatusIcon,
9
+ * mergeProps, design tokens
9
10
  * @output Exports an accessibly labelled RichTextEditor component with a flush
10
11
  * top toolbar slot and configurable editable-surface minimum height, RichTextEditorProps,
11
12
  * RichTextEditorStatus, RichTextEditorStatusType, RichTextEditorSize
@@ -34,7 +35,7 @@ import { useInputStatusIcon } from '@astryxdesign/core/hooks';
34
35
  import { VisuallyHidden } from '@astryxdesign/core/VisuallyHidden';
35
36
  import { mergeProps, themeProps } from '@astryxdesign/core/utils';
36
37
  import { useSize } from '@astryxdesign/core/SizeContext';
37
- import { LexicalComposer } from '@lexical/react/LexicalComposer';
38
+ import { LexicalExtensionComposer } from '@lexical/react/LexicalExtensionComposer';
38
39
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
39
40
  import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
40
41
  import { ContentEditable } from '@lexical/react/LexicalContentEditable';
@@ -48,7 +49,7 @@ import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
48
49
  import { TRANSFORMERS, $convertToMarkdownString } from '@lexical/markdown';
49
50
  import { $generateHtmlFromNodes } from '@lexical/html';
50
51
  import { DEFAULT_NODES } from "./editorNodes.js";
51
- import { BLUR_COMMAND, COMMAND_PRIORITY_LOW, KEY_DOWN_COMMAND, KEY_ESCAPE_COMMAND, KEY_TAB_COMMAND, mergeRegister } from 'lexical';
52
+ import { BLUR_COMMAND, COMMAND_PRIORITY_LOW, defineExtension, KEY_DOWN_COMMAND, KEY_ESCAPE_COMMAND, KEY_TAB_COMMAND, mergeRegister } from 'lexical';
52
53
 
53
54
  /**
54
55
  * Serialized state for an empty editor: a root containing a single empty
@@ -248,17 +249,36 @@ export const RichTextEditor = /*#__PURE__*/forwardRef(function RichTextEditor({
248
249
  // array each time; memoize on the prop identity instead. (React Compiler
249
250
  // isn't running the transform in this repo, so this isn't auto-memoized.)
250
251
  const markdownTransformers = useMemo(() => [...transformers], [transformers]);
251
- const initialConfig = {
252
- namespace,
253
- theme: themeRef.current,
254
- editable,
255
- editorState: defaultValue ?? undefined,
256
- nodes: nodes ? [...DEFAULT_NODES, ...nodes] : [...DEFAULT_NODES],
257
- onError(error) {
258
- // Surface errors to the host app rather than swallowing them.
259
- throw error;
260
- }
261
- };
252
+
253
+ // The extension replaces LexicalComposer's `initialConfig`: it carries the
254
+ // same editor configuration (namespace, theme, nodes, editability, initial
255
+ // state) in the shape LexicalBuilder consumes.
256
+ //
257
+ // LexicalExtensionComposer re-creates the editor whenever the extension's
258
+ // identity changes, whereas LexicalComposer read `initialConfig` exactly once
259
+ // on mount. Build it on first render and keep it, so the editor's lifetime —
260
+ // and the content it holds — is unaffected by later renders (a consumer
261
+ // passing an inline `nodes={[...]}` array would otherwise blow away the
262
+ // editor's content on every render).
263
+ const extensionRef = useRef(null);
264
+ if (extensionRef.current === null) {
265
+ extensionRef.current = defineExtension({
266
+ name: '@astryxdesign/richtext/RichTextEditor',
267
+ namespace,
268
+ theme: themeRef.current,
269
+ editable,
270
+ nodes: nodes ? [...DEFAULT_NODES, ...nodes] : [...DEFAULT_NODES],
271
+ // `undefined` (not `null`) leaves Lexical's default initializer in place,
272
+ // which seeds the empty document with one paragraph — what
273
+ // LexicalComposer did when no `editorState` was given. `null` would mean
274
+ // "start from a genuinely empty root".
275
+ $initialEditorState: defaultValue ?? undefined,
276
+ onError(error) {
277
+ // Surface errors to the host app rather than swallowing them.
278
+ throw error;
279
+ }
280
+ });
281
+ }
262
282
  const hasTabEscapeHint = editable && tabEscapeHint !== '';
263
283
  const hasToolbar = toolbar != null && typeof toolbar !== 'boolean';
264
284
  const {
@@ -295,8 +315,9 @@ export const RichTextEditor = /*#__PURE__*/forwardRef(function RichTextEditor({
295
315
  size,
296
316
  status: status?.type ?? null
297
317
  }), stylex.props(inputWrapperStyles.base, styles.wrapper, (isDisabled || isReadOnly) && inputWrapperStyles.disabled, isDisabled && styles.disabled, status && inputStatusBorderStyles[status.type], status && !isDisabled && !isReadOnly && inputStatusHoverShadowStyles[status.type], status && inputStatusFocusWithinStyles[status.type], xstyle), className, style),
298
- children: [/*#__PURE__*/_jsxs(LexicalComposer, {
299
- initialConfig: initialConfig,
318
+ children: [/*#__PURE__*/_jsxs(LexicalExtensionComposer, {
319
+ extension: extensionRef.current,
320
+ contentEditable: null,
300
321
  children: [hasToolbar ? toolbar : null, /*#__PURE__*/_jsxs("div", {
301
322
  ...stylex.props(styles.editorBody, hasToolbar && styles.editorBodyWithToolbar, !!statusIcon && styles.editorBodyWithStatus, editorBodySizeStyles[size]),
302
323
  children: [/*#__PURE__*/_jsxs("div", {
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @file RichTextView.tsx
3
- * @input Uses React, Lexical (lexical + @lexical/react), design tokens
3
+ * @input Uses React, Lexical (lexical + @lexical/react, composed through
4
+ * LexicalExtensionComposer), design tokens
4
5
  * @output Exports RichTextView component and RichTextViewProps
5
6
  * @position Read-only renderer for serialized Lexical editor state; experimental
6
7
  * (richtext), exported from @astryxdesign/richtext
@@ -1 +1 @@
1
- {"version":3,"file":"RichTextView.d.ts","sourceRoot":"","sources":["../src/RichTextView.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AAEH,OAAO,EAA8B,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAGlE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAclD,OAAO,KAAK,EAAC,KAAK,EAAE,WAAW,EAAqB,MAAM,SAAS,CAAC;AAmBpE,MAAM,WAAW,iBAAkB,SAAQ,SAAS;IAClD;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;IAC1C;;;OAGG;IACH,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACtC;;;;OAIG;IACH,aAAa,CAAC,EAAE,SAAS,CAAC;CAC3B;AA2CD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,KAAK,EACL,OAAO,EACP,SAAyB,EACzB,YAAY,EACZ,aAAoB,EACpB,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,IAAI,EACR,EAAE,iBAAiB,+BAyEnB;yBApFe,YAAY"}
1
+ {"version":3,"file":"RichTextView.d.ts","sourceRoot":"","sources":["../src/RichTextView.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAA8B,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAGlE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAWlD,OAAO,KAAK,EAEV,KAAK,EACL,WAAW,EAEZ,MAAM,SAAS,CAAC;AAoBjB,MAAM,WAAW,iBAAkB,SAAQ,SAAS;IAClD;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;IAC1C;;;OAGG;IACH,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACtC;;;;OAIG;IACH,aAAa,CAAC,EAAE,SAAS,CAAC;CAC3B;AAyCD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,KAAK,EACL,OAAO,EACP,SAAyB,EACzB,YAAY,EACZ,aAAoB,EACpB,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,IAAI,EACR,EAAE,iBAAiB,+BAyFnB;yBApGe,YAAY"}
@@ -4,7 +4,8 @@
4
4
 
5
5
  /**
6
6
  * @file RichTextView.tsx
7
- * @input Uses React, Lexical (lexical + @lexical/react), design tokens
7
+ * @input Uses React, Lexical (lexical + @lexical/react, composed through
8
+ * LexicalExtensionComposer), design tokens
8
9
  * @output Exports RichTextView component and RichTextViewProps
9
10
  * @position Read-only renderer for serialized Lexical editor state; experimental
10
11
  * (richtext), exported from @astryxdesign/richtext
@@ -17,7 +18,7 @@
17
18
  import { useEffect, useRef, useState } from 'react';
18
19
  import * as stylex from '@stylexjs/stylex';
19
20
  import { sharedEditorTheme } from "./editorTheme.js";
20
- import { LexicalComposer } from '@lexical/react/LexicalComposer';
21
+ import { LexicalExtensionComposer } from '@lexical/react/LexicalExtensionComposer';
21
22
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
22
23
  import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
23
24
  import { ContentEditable } from '@lexical/react/LexicalContentEditable';
@@ -26,6 +27,7 @@ import { ListNode, ListItemNode } from '@lexical/list';
26
27
  import { HeadingNode, QuoteNode } from '@lexical/rich-text';
27
28
  import { LinkNode, AutoLinkNode } from '@lexical/link';
28
29
  import { CodeNode, CodeHighlightNode } from '@lexical/code';
30
+ import { defineExtension } from 'lexical';
29
31
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
30
32
  const styles = {
31
33
  root: {
@@ -37,30 +39,28 @@ const DEFAULT_NODES = [HeadingNode, QuoteNode, ListNode, ListItemNode, LinkNode,
37
39
  /**
38
40
  * Keeps the rendered content in sync with the `value` prop after mount.
39
41
  *
40
- * `LexicalComposer`'s `initialConfig.editorState` is only read once on mount, so
41
- * a plain `<RichTextView value={changingValue} />` would freeze at its first
42
+ * The editor's initial state is seeded once, when the composer mounts, so a
43
+ * plain `<RichTextView value={changingValue} />` would freeze at its first
42
44
  * value — the content would never update when `value` changed. This plugin runs
43
45
  * inside the composer context and re-applies `value` whenever it changes, so the
44
46
  * read-only view stays reactive (e.g. previewing content edited elsewhere).
45
47
  *
46
- * The initial `value` is already applied via `initialConfig.editorState`, so we
47
- * skip the first run to avoid a redundant re-parse on mount.
48
+ * The initial `value` is already applied via the extension's
49
+ * `$initialEditorState`, so we skip the first run to avoid a redundant re-parse
50
+ * on mount.
48
51
  *
49
52
  * This mirrors the canonical Lexical pattern for applying externally-sourced
50
53
  * serialized state after mount: the Lexical Playground's ActionsPlugin does the
51
54
  * same `editor.setEditorState(editor.parseEditorState(...))` from inside a
52
55
  * plugin (see facebook/lexical
53
56
  * packages/lexical-playground/src/plugins/ActionsPlugin/index.tsx). It is
54
- * necessary because `LexicalComposer` builds the editor in a `useMemo(..., [])`
55
- * and reads `initialConfig.editorState` exactly once on init
56
- * (packages/lexical-react/src/LexicalComposer.tsx), so a changed prop cannot
57
- * re-seed the editor on its own. A read-only view has no history, so we skip the
58
- * Playground's accompanying CLEAR_HISTORY_COMMAND.
57
+ * necessary because the composer builds the editor once and reads the initial
58
+ * state exactly once at that point, so a changed prop cannot re-seed the editor
59
+ * on its own. A read-only view has no history, so we skip the Playground's
60
+ * accompanying CLEAR_HISTORY_COMMAND.
59
61
  *
60
62
  * `parseEditorState` / `setEditorState` are methods on the editor instance, so
61
- * this avoids a top-level `lexical` *value* import (which would force the
62
- * sandbox's Next build to transpile lexical's raw `src/*.ts` and fail — see the
63
- * matching note in RichTextEditor.tsx).
63
+ * this plugin needs no module-level Lexical imports of its own.
64
64
  */
65
65
  function SyncValuePlugin({
66
66
  value
@@ -103,6 +103,12 @@ export function RichTextView({
103
103
  if (themeRef.current === null) {
104
104
  themeRef.current = sharedEditorTheme();
105
105
  }
106
+
107
+ // Built on first render (and again after an error, below) rather than per
108
+ // render: LexicalExtensionComposer re-creates the editor whenever the
109
+ // extension's identity changes, and `value` updates are applied in place by
110
+ // SyncValuePlugin instead.
111
+ const extensionRef = useRef(null);
106
112
  const [hasError, setHasError] = useState(false);
107
113
 
108
114
  // Reset the error state when the value changes so a corrected value recovers.
@@ -119,8 +125,9 @@ export function RichTextView({
119
125
  };
120
126
 
121
127
  // Validate `value` parses as JSON before handing it to Lexical. Malformed
122
- // JSON would otherwise throw synchronously during LexicalComposer init and
123
- // escape any error boundary, taking down the host on the render path.
128
+ // JSON would otherwise throw synchronously while the composer builds the
129
+ // editor and escape any error boundary, taking down the host on the render
130
+ // path.
124
131
  if (!hasError) {
125
132
  try {
126
133
  JSON.parse(value);
@@ -129,6 +136,10 @@ export function RichTextView({
129
136
  }
130
137
  }
131
138
  if (hasError) {
139
+ // The composer subtree is unmounted while the fallback renders. Drop the
140
+ // extension so that recovering from the error builds a fresh one, seeded
141
+ // with the corrected `value`.
142
+ extensionRef.current = null;
132
143
  return /*#__PURE__*/_jsx("div", {
133
144
  ...stylex.props(styles.root, xstyle),
134
145
  className: className,
@@ -137,23 +148,27 @@ export function RichTextView({
137
148
  children: errorFallback
138
149
  });
139
150
  }
140
- const initialConfig = {
141
- namespace,
142
- theme: themeRef.current,
143
- editable: false,
144
- editorState: value,
145
- nodes: nodes ? [...DEFAULT_NODES, ...nodes] : [...DEFAULT_NODES],
146
- // A read-only view renders persisted content; a bad node/schema should not
147
- // crash the host. Surface it via onParseError + fallback instead of re-throwing.
148
- onError: handleError
149
- };
151
+ if (extensionRef.current === null) {
152
+ extensionRef.current = defineExtension({
153
+ name: '@astryxdesign/richtext/RichTextView',
154
+ namespace,
155
+ theme: themeRef.current,
156
+ editable: false,
157
+ nodes: nodes ? [...DEFAULT_NODES, ...nodes] : [...DEFAULT_NODES],
158
+ $initialEditorState: value,
159
+ // A read-only view renders persisted content; a bad node/schema should not
160
+ // crash the host. Surface it via onParseError + fallback instead of re-throwing.
161
+ onError: handleError
162
+ });
163
+ }
150
164
  return /*#__PURE__*/_jsx("div", {
151
165
  ...stylex.props(styles.root, xstyle),
152
166
  className: className,
153
167
  style: style,
154
168
  ...rest,
155
- children: /*#__PURE__*/_jsxs(LexicalComposer, {
156
- initialConfig: initialConfig,
169
+ children: /*#__PURE__*/_jsxs(LexicalExtensionComposer, {
170
+ extension: extensionRef.current,
171
+ contentEditable: null,
157
172
  children: [/*#__PURE__*/_jsx(SyncValuePlugin, {
158
173
  value: value
159
174
  }), /*#__PURE__*/_jsx(RichTextPlugin, {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@astryxdesign/richtext",
3
3
  "displayName": "Rich Text",
4
- "version": "0.6.0-canary.f574abd",
4
+ "version": "0.6.1-canary.09decfb",
5
5
  "description": "Astryx rich text — a Lexical-based rich text editor and viewer. Published to npm only under the @canary dist-tag for early testing; never released as a stable (latest) version.",
6
6
  "author": "Meta Open Source",
7
7
  "license": "MIT",
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "peerDependencies": {
52
52
  "@stylexjs/stylex": ">=0.10.0",
53
- "@astryxdesign/core": "0.6.0-canary.f574abd",
53
+ "@astryxdesign/core": "0.6.1-canary.09decfb",
54
54
  "react": ">=19.0.0",
55
55
  "react-dom": ">=19.0.0",
56
56
  "lexical": "^0.46.0",
@@ -101,7 +101,11 @@
101
101
  }
102
102
  },
103
103
  "devDependencies": {
104
- "@astryxdesign/cli": "0.6.0-canary.f574abd",
104
+ "@astryxdesign/cli": "0.6.1-canary.09decfb",
105
+ "@babel/cli": "^7.29.7",
106
+ "@babel/core": "^7.29.7",
107
+ "@babel/preset-react": "^7.29.7",
108
+ "@babel/preset-typescript": "^7.29.7",
105
109
  "@lexical/code": "^0.46.0",
106
110
  "@lexical/headless": "^0.46.0",
107
111
  "@lexical/html": "^0.46.0",
@@ -112,6 +116,8 @@
112
116
  "@lexical/rich-text": "^0.46.0",
113
117
  "@lexical/selection": "^0.46.0",
114
118
  "@lexical/utils": "^0.46.0",
119
+ "@stylexjs/babel-plugin": "^0.19.0",
120
+ "@testing-library/react": "^16.3.2",
115
121
  "lexical": "^0.46.0"
116
122
  },
117
123
  "scripts": {
@@ -4,8 +4,9 @@
4
4
 
5
5
  /**
6
6
  * @file RichTextEditor.tsx
7
- * @input Uses React, useId, Lexical (lexical + @lexical/react), Field,
8
- * VisuallyHidden, useInputStatusIcon, mergeProps, design tokens
7
+ * @input Uses React, useId, Lexical (lexical + @lexical/react, composed through
8
+ * LexicalExtensionComposer), Field, VisuallyHidden, useInputStatusIcon,
9
+ * mergeProps, design tokens
9
10
  * @output Exports an accessibly labelled RichTextEditor component with a flush
10
11
  * top toolbar slot and configurable editable-surface minimum height, RichTextEditorProps,
11
12
  * RichTextEditorStatus, RichTextEditorStatusType, RichTextEditorSize
@@ -58,10 +59,7 @@ import {VisuallyHidden} from '@astryxdesign/core/VisuallyHidden';
58
59
  import {mergeProps, themeProps, type SizeValue} from '@astryxdesign/core/utils';
59
60
  import {useSize} from '@astryxdesign/core/SizeContext';
60
61
 
61
- import {
62
- LexicalComposer,
63
- type InitialConfigType,
64
- } from '@lexical/react/LexicalComposer';
62
+ import {LexicalExtensionComposer} from '@lexical/react/LexicalExtensionComposer';
65
63
  import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
66
64
  import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
67
65
  import {ContentEditable} from '@lexical/react/LexicalContentEditable';
@@ -83,10 +81,12 @@ import {DEFAULT_NODES} from './editorNodes';
83
81
  import {
84
82
  BLUR_COMMAND,
85
83
  COMMAND_PRIORITY_LOW,
84
+ defineExtension,
86
85
  KEY_DOWN_COMMAND,
87
86
  KEY_ESCAPE_COMMAND,
88
87
  KEY_TAB_COMMAND,
89
88
  mergeRegister,
89
+ type AnyLexicalExtension,
90
90
  type EditorState,
91
91
  type Klass,
92
92
  type LexicalEditor,
@@ -526,17 +526,35 @@ export const RichTextEditor = forwardRef<
526
526
  // isn't running the transform in this repo, so this isn't auto-memoized.)
527
527
  const markdownTransformers = useMemo(() => [...transformers], [transformers]);
528
528
 
529
- const initialConfig: InitialConfigType = {
530
- namespace,
531
- theme: themeRef.current,
532
- editable,
533
- editorState: defaultValue ?? undefined,
534
- nodes: nodes ? [...DEFAULT_NODES, ...nodes] : [...DEFAULT_NODES],
535
- onError(error: Error) {
536
- // Surface errors to the host app rather than swallowing them.
537
- throw error;
538
- },
539
- };
529
+ // The extension replaces LexicalComposer's `initialConfig`: it carries the
530
+ // same editor configuration (namespace, theme, nodes, editability, initial
531
+ // state) in the shape LexicalBuilder consumes.
532
+ //
533
+ // LexicalExtensionComposer re-creates the editor whenever the extension's
534
+ // identity changes, whereas LexicalComposer read `initialConfig` exactly once
535
+ // on mount. Build it on first render and keep it, so the editor's lifetime —
536
+ // and the content it holds — is unaffected by later renders (a consumer
537
+ // passing an inline `nodes={[...]}` array would otherwise blow away the
538
+ // editor's content on every render).
539
+ const extensionRef = useRef<AnyLexicalExtension | null>(null);
540
+ if (extensionRef.current === null) {
541
+ extensionRef.current = defineExtension({
542
+ name: '@astryxdesign/richtext/RichTextEditor',
543
+ namespace,
544
+ theme: themeRef.current,
545
+ editable,
546
+ nodes: nodes ? [...DEFAULT_NODES, ...nodes] : [...DEFAULT_NODES],
547
+ // `undefined` (not `null`) leaves Lexical's default initializer in place,
548
+ // which seeds the empty document with one paragraph — what
549
+ // LexicalComposer did when no `editorState` was given. `null` would mean
550
+ // "start from a genuinely empty root".
551
+ $initialEditorState: defaultValue ?? undefined,
552
+ onError(error: Error) {
553
+ // Surface errors to the host app rather than swallowing them.
554
+ throw error;
555
+ },
556
+ });
557
+ }
540
558
 
541
559
  const hasTabEscapeHint = editable && tabEscapeHint !== '';
542
560
  const hasToolbar = toolbar != null && typeof toolbar !== 'boolean';
@@ -606,7 +624,9 @@ export const RichTextEditor = forwardRef<
606
624
  className,
607
625
  style,
608
626
  )}>
609
- <LexicalComposer initialConfig={initialConfig}>
627
+ <LexicalExtensionComposer
628
+ extension={extensionRef.current}
629
+ contentEditable={null}>
610
630
  {hasToolbar ? toolbar : null}
611
631
  <div
612
632
  {...stylex.props(
@@ -664,7 +684,7 @@ export const RichTextEditor = forwardRef<
664
684
  <div {...stylex.props(styles.statusIcon)}>{statusIcon}</div>
665
685
  )}
666
686
  </div>
667
- </LexicalComposer>
687
+ </LexicalExtensionComposer>
668
688
  {hasTabEscapeHint && (
669
689
  <VisuallyHidden id={tabEscapeHintID}>{tabEscapeHint}</VisuallyHidden>
670
690
  )}
@@ -4,7 +4,8 @@
4
4
 
5
5
  /**
6
6
  * @file RichTextView.tsx
7
- * @input Uses React, Lexical (lexical + @lexical/react), design tokens
7
+ * @input Uses React, Lexical (lexical + @lexical/react, composed through
8
+ * LexicalExtensionComposer), design tokens
8
9
  * @output Exports RichTextView component and RichTextViewProps
9
10
  * @position Read-only renderer for serialized Lexical editor state; experimental
10
11
  * (richtext), exported from @astryxdesign/richtext
@@ -20,10 +21,7 @@ import * as stylex from '@stylexjs/stylex';
20
21
  import {sharedEditorTheme} from './editorTheme';
21
22
  import type {BaseProps} from '@astryxdesign/core';
22
23
 
23
- import {
24
- LexicalComposer,
25
- type InitialConfigType,
26
- } from '@lexical/react/LexicalComposer';
24
+ import {LexicalExtensionComposer} from '@lexical/react/LexicalExtensionComposer';
27
25
  import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
28
26
  import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
29
27
  import {ContentEditable} from '@lexical/react/LexicalContentEditable';
@@ -32,7 +30,13 @@ import {ListNode, ListItemNode} from '@lexical/list';
32
30
  import {HeadingNode, QuoteNode} from '@lexical/rich-text';
33
31
  import {LinkNode, AutoLinkNode} from '@lexical/link';
34
32
  import {CodeNode, CodeHighlightNode} from '@lexical/code';
35
- import type {Klass, LexicalNode, EditorThemeClasses} from 'lexical';
33
+ import type {
34
+ AnyLexicalExtension,
35
+ Klass,
36
+ LexicalNode,
37
+ EditorThemeClasses,
38
+ } from 'lexical';
39
+ import {defineExtension} from 'lexical';
36
40
 
37
41
  const styles = stylex.create({
38
42
  root: {
@@ -89,30 +93,28 @@ export interface RichTextViewProps extends BaseProps {
89
93
  /**
90
94
  * Keeps the rendered content in sync with the `value` prop after mount.
91
95
  *
92
- * `LexicalComposer`'s `initialConfig.editorState` is only read once on mount, so
93
- * a plain `<RichTextView value={changingValue} />` would freeze at its first
96
+ * The editor's initial state is seeded once, when the composer mounts, so a
97
+ * plain `<RichTextView value={changingValue} />` would freeze at its first
94
98
  * value — the content would never update when `value` changed. This plugin runs
95
99
  * inside the composer context and re-applies `value` whenever it changes, so the
96
100
  * read-only view stays reactive (e.g. previewing content edited elsewhere).
97
101
  *
98
- * The initial `value` is already applied via `initialConfig.editorState`, so we
99
- * skip the first run to avoid a redundant re-parse on mount.
102
+ * The initial `value` is already applied via the extension's
103
+ * `$initialEditorState`, so we skip the first run to avoid a redundant re-parse
104
+ * on mount.
100
105
  *
101
106
  * This mirrors the canonical Lexical pattern for applying externally-sourced
102
107
  * serialized state after mount: the Lexical Playground's ActionsPlugin does the
103
108
  * same `editor.setEditorState(editor.parseEditorState(...))` from inside a
104
109
  * plugin (see facebook/lexical
105
110
  * packages/lexical-playground/src/plugins/ActionsPlugin/index.tsx). It is
106
- * necessary because `LexicalComposer` builds the editor in a `useMemo(..., [])`
107
- * and reads `initialConfig.editorState` exactly once on init
108
- * (packages/lexical-react/src/LexicalComposer.tsx), so a changed prop cannot
109
- * re-seed the editor on its own. A read-only view has no history, so we skip the
110
- * Playground's accompanying CLEAR_HISTORY_COMMAND.
111
+ * necessary because the composer builds the editor once and reads the initial
112
+ * state exactly once at that point, so a changed prop cannot re-seed the editor
113
+ * on its own. A read-only view has no history, so we skip the Playground's
114
+ * accompanying CLEAR_HISTORY_COMMAND.
111
115
  *
112
116
  * `parseEditorState` / `setEditorState` are methods on the editor instance, so
113
- * this avoids a top-level `lexical` *value* import (which would force the
114
- * sandbox's Next build to transpile lexical's raw `src/*.ts` and fail — see the
115
- * matching note in RichTextEditor.tsx).
117
+ * this plugin needs no module-level Lexical imports of its own.
116
118
  */
117
119
  function SyncValuePlugin({value}: {value: string}): null {
118
120
  const [editor] = useLexicalComposerContext();
@@ -154,6 +156,12 @@ export function RichTextView({
154
156
  themeRef.current = sharedEditorTheme();
155
157
  }
156
158
 
159
+ // Built on first render (and again after an error, below) rather than per
160
+ // render: LexicalExtensionComposer re-creates the editor whenever the
161
+ // extension's identity changes, and `value` updates are applied in place by
162
+ // SyncValuePlugin instead.
163
+ const extensionRef = useRef<AnyLexicalExtension | null>(null);
164
+
157
165
  const [hasError, setHasError] = useState(false);
158
166
 
159
167
  // Reset the error state when the value changes so a corrected value recovers.
@@ -171,8 +179,9 @@ export function RichTextView({
171
179
  };
172
180
 
173
181
  // Validate `value` parses as JSON before handing it to Lexical. Malformed
174
- // JSON would otherwise throw synchronously during LexicalComposer init and
175
- // escape any error boundary, taking down the host on the render path.
182
+ // JSON would otherwise throw synchronously while the composer builds the
183
+ // editor and escape any error boundary, taking down the host on the render
184
+ // path.
176
185
  if (!hasError) {
177
186
  try {
178
187
  JSON.parse(value);
@@ -182,6 +191,10 @@ export function RichTextView({
182
191
  }
183
192
 
184
193
  if (hasError) {
194
+ // The composer subtree is unmounted while the fallback renders. Drop the
195
+ // extension so that recovering from the error builds a fresh one, seeded
196
+ // with the corrected `value`.
197
+ extensionRef.current = null;
185
198
  return (
186
199
  <div
187
200
  {...stylex.props(styles.root, xstyle)}
@@ -193,16 +206,19 @@ export function RichTextView({
193
206
  );
194
207
  }
195
208
 
196
- const initialConfig: InitialConfigType = {
197
- namespace,
198
- theme: themeRef.current,
199
- editable: false,
200
- editorState: value,
201
- nodes: nodes ? [...DEFAULT_NODES, ...nodes] : [...DEFAULT_NODES],
202
- // A read-only view renders persisted content; a bad node/schema should not
203
- // crash the host. Surface it via onParseError + fallback instead of re-throwing.
204
- onError: handleError,
205
- };
209
+ if (extensionRef.current === null) {
210
+ extensionRef.current = defineExtension({
211
+ name: '@astryxdesign/richtext/RichTextView',
212
+ namespace,
213
+ theme: themeRef.current,
214
+ editable: false,
215
+ nodes: nodes ? [...DEFAULT_NODES, ...nodes] : [...DEFAULT_NODES],
216
+ $initialEditorState: value,
217
+ // A read-only view renders persisted content; a bad node/schema should not
218
+ // crash the host. Surface it via onParseError + fallback instead of re-throwing.
219
+ onError: handleError,
220
+ });
221
+ }
206
222
 
207
223
  return (
208
224
  <div
@@ -210,7 +226,9 @@ export function RichTextView({
210
226
  className={className}
211
227
  style={style}
212
228
  {...rest}>
213
- <LexicalComposer initialConfig={initialConfig}>
229
+ <LexicalExtensionComposer
230
+ extension={extensionRef.current}
231
+ contentEditable={null}>
214
232
  <SyncValuePlugin value={value} />
215
233
  <RichTextPlugin
216
234
  contentEditable={<ContentEditable />}
@@ -218,7 +236,7 @@ export function RichTextView({
218
236
  ErrorBoundary={LexicalErrorBoundary}
219
237
  />
220
238
  {plugins}
221
- </LexicalComposer>
239
+ </LexicalExtensionComposer>
222
240
  </div>
223
241
  );
224
242
  }