[lexical-html][lexical-playground] Feature: Conditional DOM render overrides (disabledForEditor/disabledForSession)#8575
Merged
Conversation
…errides via disabledForEditor/disabledForSession Adds an options argument to domOverride that gates whether an override is installed, based only on render context: - disabledForEditor: gates residency in the editor's render config (reconciliation + export base). Mirrored imperatively via $setRenderContextValue, which recompiles the resident config and re-renders affected nodes, recreating DOM for structural overrides. - disabledForSession: gates participation in a single export/generate session, evaluated against the active session context. No effect on live reconciliation. VisibleLineBreakExtension now removes its wrap from the pipeline entirely when disabled (no per-node checks, no manual re-render hack), and TerseExportExtension is installed only for terse export sessions. https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Clarifies that the captured config is the editor config from init (whose `dom` is the user's pre-override base), not a recompile-specific structure. https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
compileDOMRenderConfigOverrides only reads `nodes` (type tree) and `dom` (base), so capture and pass just `Pick<InitialEditorConfig, 'nodes' | 'dom'>` instead of the whole InitialEditorConfig. https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
…arkDirtyByType We only need to assert the popped value is defined, not cast its type. https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
…d use it for conditional override re-renders Adds an @internal $fullReconcile() to lexical that schedules a FULL_RECONCILE of the active editor, re-rendering every node through the current config without cloning the node map (so no spurious mutation/collaboration changes). - lexical-utils $restoreEditorState drops its local `const FULL_RECONCILE = 2` workaround in favor of $fullReconcile(). - DOMRenderRuntime toggles now re-render via $fullReconcile() instead of marking/cloning nodes. Structural overrides force a recreate through a transient $updateDOM wrapper (a single (node) => boolean predicate) that is installed only for that reconcile and removed immediately after. https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
…er override test Use null | LexicalNode instead of unknown for the before/after node captures and clarify that identity equality proves the node was never made writable. https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
… indexed access Move DOMRenderMatchConfig to types.ts and export it, since it appears in the public signature of domOverride. Type init's initialEditorConfig as DOMRenderInitResult['initialEditorConfig'] to avoid duplicating the Pick. https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
…e toggle Collapse the rerender/recreate split into a single recreatePredicate. $createDOM/$getDOMSlot/$decorateDOM all recreate the affected nodes — including $decorateDOM, whose additive DOM can only be reverted by a fresh $createDOM. $updateDOM (diff-driven, applies on the next update) and export-only hooks need no re-render. Recreating is the simple, always-correct choice; toggles are rare so it can be optimized later. https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
Use HISTORY_MERGE_TAG instead of the literal, pass $fullReconcile directly (eta-reduce), and restore the transient $updateDOM on the next line — the discrete update commits synchronously, so onUpdate is unnecessary. https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
…ust be discrete The transient $updateDOM recreate wrapper mutates the shared active config, so the reconcile must run and finish synchronously before the original is restored (and the call must not be nested in an editor.update). Document that invariant so the update isn't naively made non-discrete. https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
… a theme load $codeNodeTransform returned early while the default theme loaded asynchronously, even when the tokenizer has defaultLanguage: null and the text is plainified (no theme needed). That left such code blocks as an unsplit TextNode until the theme happened to finish loading. Only gate the split on the theme load when a language will actually consume it. https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
…es for VSCode
VSCode walks up from a test file to the nearest tsconfig.json (the root),
which excluded **/__tests__/** and lacked the test-only path aliases. That
left test files as loose files in the editor — every workspace import
("@lexical/*", "lexical", and the *src/__tests__/utils* subpaths) reported
"Cannot find module" even though the CLI tsc-test was fine.
Have update-tsconfig.mjs write the same generated test paths into
tsconfig.json so VSCode resolves them, and drop **/__tests__/** from its
exclude. Build output is unaffected (tsconfig.build.json has its own
**/__tests__/** exclude and emits no test files into the dist .d.ts —
verified). tsc-test is now equivalent to tsc, so drop the script.
https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
…dirty in the same update
internalMarkNodeAsDirty (and the sibling clone path) unconditionally wrote
editor._dirtyType = HAS_DIRTY_NODES, which silently downgraded a previously
set FULL_RECONCILE — if a caller does
`editor.update(() => { $fullReconcile(); n.markDirty(); })`, the commit ends
up reconciling only the marked nodes instead of the full graph. Upgrade
the dirty type only when nothing has been marked yet (`=== NO_DIRTY_NODES`).
Also replaces a bare `editor._dirtyType = 0` reset with `NO_DIRTY_NODES`.
https://claude.ai/code/session_01TCX95t8LLYu41Yz9NgNLJx
zurfyx
approved these changes
May 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
domOverrideoverrides registered withDOMRenderExtensionare currently compiled once into the editor's render config and stay active for the editor's lifetime. Extensions that want to toggle behavior have to keep theoverride permanently installed and no-op per node — e.g.
VisibleLineBreakExtensionchecked$isDisabled(editor)inside every$createDOM/$updateDOMand forced a re-render via a no-opregisterNodeTransform, andTerseExportExtensionran a wildcard$exportDOMon every node of every export and bailed after a context check.This PR adds an optional third
optionsargument todomOverridethat gates whether an override is installed, based only on render context:disabledForEditor(ctx)— gates residency in the editor's render config (reconciliation + the base for export). It is driven imperatively via the new$setRenderContextValue, which recompiles the resident config and re-renders the affected nodes, recreating DOM for structural overrides ($createDOM/$getDOMSlot).disabledForSession(ctx)— gates participation in a single export/generate session, evaluated once against the active session context. It has no effect on live reconciliation (which is not a session).Both predicates default to "not disabled", so existing
domOverridecalls areunaffected.
New
@lexical/htmlAPI:domOverride(nodes, config, options?)withDOMOverrideOptions(disabledForEditor,disabledForSession) and theRenderContextReaderpassed to them.$setRenderContextValue/$updateRenderContextValue— imperative, persistent writes to the editor render context (vs. the scoped$withRenderContext).$getSessionDOMRenderConfig— resolves the per-session render config.VisibleLineBreakExtensionnow usesdisabledForEditor(dropping the per-node$isDisabledchecks and the no-op-transform re-render hack) andTerseExportExtensionusesdisabledForSession(dropping the per-call terse check, so non-terse exports never enter the terse middleware).Implementation note: this is contained entirely within
@lexical/html— no core reconciler change was needed. Structural reverts on disable are driven through the existing$updateDOM → truerecreate path via a transient force-recreate wrapper on the recompiled config, committed with adiscreteupdate so the toggle re-renders synchronously.lexicalAdds a new internal
$fullReconcile()API to cause a reconciliation without having to mark nodes as dirty and cause NodeMap/history churnOther:
Includes some drive-by fixes to:
Closes #8567
Test plan
New unit tests