Skip to content

fix(lexical-html): $generateHtmlFromNodes should self-establish active-editor scope (back-compat for #8519)#8589

Merged
etrepum merged 3 commits into
mainfrom
bc-generate-html-self-scope
May 29, 2026
Merged

fix(lexical-html): $generateHtmlFromNodes should self-establish active-editor scope (back-compat for #8519)#8589
etrepum merged 3 commits into
mainfrom
bc-generate-html-self-scope

Conversation

@potatowagon

Copy link
Copy Markdown
Contributor

Summary

PR #8519 (commit a6908ba) made $setTextContent in packages/lexical/src/nodes/LexicalTextNode.ts call $getEditor() at the top of the function. Because $generateHtmlFromNodes walks TextNode.createDOM/updateDOM → $setTextContent, it now throws inside any caller that uses the previously-idiomatic editor.getEditorState().read(cb) scope — getActiveEditor() returns null and the call dies with an opaque "getActiveEditor" invariant error.

Since $generateHtmlFromNodes already receives editor as its first argument, it can self-establish the active-editor scope.

Changes

  • $generateHtmlFromNodes: wraps its body in editor.read(...), which is a no-op if an active editor scope already exists (nested reads are safe in Lexical).
  • $generateDOMFromNodes: same treatment for consistency.
  • $generateDOMFromRoot: same treatment for consistency.

This makes all three export functions work correctly regardless of whether the caller uses:

Test Plan

Added LexicalHtmlBackwardCompat.test.ts with three tests:

  1. Calls $generateHtmlFromNodes(editor) from inside editor.getEditorState().read(cb) (legacy pattern) — asserts it returns HTML without throwing.
  2. Calls from inside editor.read(cb) (modern pattern) — asserts same behavior.
  3. Asserts both patterns produce identical output (no behavioral divergence).

All 84 existing tests in packages/lexical-html continue to pass unchanged.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label May 29, 2026
@vercel

vercel Bot commented May 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lexical Ready Ready Preview, Comment May 29, 2026 2:28pm
lexical-playground Ready Ready Preview, Comment May 29, 2026 2:28pm

Request Review

…tor scope

Back-compat fix for #8519. When called from a legacy editorState.read(cb)
scope (without {editor} option), $generateHtmlFromNodes now self-establishes
an active-editor scope via editor.update({discrete: true}).

This prevents the opaque 'getActiveEditor invariant' throw that consumers
hit on upgrade when using the previously-idiomatic legacy pattern.

When already in an active editor scope (editor.read/editor.update or
editorState.read(cb, {editor})), runs inline with zero overhead.

@zurfyx zurfyx left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stamping to unblock internal sync but would love to have @etrepum's thoughts here

@mayrang

mayrang commented May 29, 2026

Copy link
Copy Markdown
Contributor

Thanks @potatowagon — will follow up after this lands to extend the same back-compat treatment to $generateDOMFromNodes / $generateDOMFromRoot.

@etrepum

etrepum commented May 29, 2026

Copy link
Copy Markdown
Collaborator

$generateDOMFromRoot and $generateDOMFromNodes are newer functions and don’t need this legacy workaround

Comment thread packages/lexical-html/src/index.ts Outdated
@etrepum

etrepum commented May 29, 2026

Copy link
Copy Markdown
Collaborator

I think the best solution is a private LexicalUpdates API like this that we export through the lexical package for internal backwards compat use:

/** @internal */
export function $assumeActiveEditor(editor: LexicalEditor): void {
  // Throw if called outside of an update
  if (getActiveEditorState() !== null && activeEditor === null) {
    activeEditor = editor;
  }
  invariant(
    activeEditor === editor,
    'The given editor argument does not match $getEditor() in this context. Use editor.getEditorState().read(..., {editor}) if this cross-editor call is intentional.',
  );
}

Then you would just call $assumeActiveEditor(editor) in the $generateHtmlFromNodes call as the workaround (pushed an implementation)

@etrepum etrepum added this pull request to the merge queue May 29, 2026
Merged via the queue into main with commit 111a0d8 May 29, 2026
45 checks passed
@etrepum etrepum mentioned this pull request Jun 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

4 participants