Skip to content

[Breaking Changes][*] Refactor: Register DOMImportExtension rules implicitly via node extensions and make tree-shaking annotations effective#8662

Merged
etrepum merged 4 commits into
facebook:mainfrom
etrepum:claude/compassionate-feynman-eenu2u
Jun 10, 2026
Merged

[Breaking Changes][*] Refactor: Register DOMImportExtension rules implicitly via node extensions and make tree-shaking annotations effective#8662
etrepum merged 4 commits into
facebook:mainfrom
etrepum:claude/compassionate-feynman-eenu2u

Conversation

@etrepum

@etrepum etrepum commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Breaking Change

If you have started using the extensions that provide DOMImportExtension rules from v0.45.0, these are all now deprecated and unified with their main extension. For example, the import rules previously defined by CodeImportExtension are now defined by CodeExtension, so CodeImportExtension is now simply an alias. As an @experimental API, these compatibility aliases may be removed as soon as v0.47.0. If you had not started using these yet, there are no breaking changes. The motivation for this change is to reduce the amount of configuration required to start using the new DOM import pipeline (e.g. ClipboardDOMImportExtension to handle paste or an explicit call to $generateNodesFromDOMViaExtension).

Description

The DOMImportExtension pipeline previously required listing a per-package
import extension for every node package, plus the shared core baseline. This
PR makes the import rules implicit — each node-providing extension registers
its own rules — and then makes the repo's tree-shaking annotations actually
work so the implicit rules (and everything else built on the pure factories)
can be dropped from bundles that don't use them.

1. Import rules ship with the node extensions (RichTextExtension,
ListExtension, LinkExtension, TableExtension, CodeExtension each
depend on CoreImportExtension + configExtension(DOMImportExtension, {rules})):

  • Rules are inert unless the editor routes HTML through the pipeline
    (ClipboardDOMImportExtension or $generateNodesFromDOMViaExtension);
    the legacy paste path is unchanged.
  • The <hr> rule moves into CoreImportRules, gated on
    HorizontalRuleNode registration (mirroring the legacy importDOM
    contract), because @lexical/extension is upstream of @lexical/html
    and cannot register import rules itself.
  • ClipboardDOMImportExtension now depends on CoreImportExtension, so
    the paste on-switch is self-sufficient.
  • RichTextImportExtension / ListImportExtension / LinkImportExtension
    / TableImportExtension / CodeImportExtension /
    HorizontalRuleImportExtension remain as deprecated aliases.
  • Rule precedence is preserved: configs merge in dependency order, so each
    package's rules sit above the core baseline, and apps control
    cross-package priority by extension order (covered by a new test for
    GitHub code-table vs. TableExtension).

2. Make @__NO_SIDE_EFFECTS__ effective. esbuild only honors the
definition annotation for same-file calls (webpack/terser not at all), so
unused extension/command/rule definitions were never tree-shaken. All
module-scope calls to the pure factories now carry call-site
/* @__PURE__ */ annotations (including argument-position
configExtension/safeCast calls, which otherwise pin the enclosing
definition), and terser is configured with format.preserve_annotations
so the annotations survive into prod dist output.

3. Keep it that way automatically. New
@lexical/internal/require-pure-annotation ESLint rule (with autofixer)
requires the annotation on module-scope calls to the pure factories;
pre-commit eslint --fix inserts them, CI lint backstops. Documented in
AGENTS.md.

The dev example, playground, and serialization/dom-import.md docs are
updated for the implicit configuration.

Test plan

Before

Editor setup required explicit importer wiring:

dependencies: [
  RichTextExtension, ListExtension, /* … */
  CoreImportExtension,
  RichTextImportExtension,
  ListImportExtension,
  LinkImportExtension,
  TableImportExtension,
  HorizontalRuleImportExtension,
  CodeImportExtension,
  ClipboardDOMImportExtension,
],

Unused defineExtension(...)/createCommand(...) results were retained by
esbuild/webpack consumers; e.g. importing only $createListNode from
@lexical/list retained ListExtension, and ~14 KB minified of unused
clipboard/DOM-import definitions shipped in every rich-text bundle.

After

dependencies: [
  RichTextExtension, ListExtension, /* … */
  ClipboardDOMImportExtension,
],
  • pnpm run test-unit — 159 files / 3,774 tests pass (new coverage:
    implicit-rule editors per package, <hr> gating, deprecated aliases,
    cross-package rule precedence, ESLint rule fixture tests).
  • pnpm run ci-check (tsc, Flow, Prettier, ESLint incl. the new rule)
    passes.
  • Bundle measurements (esbuild against workspace sources,
    NODE_ENV=production, minified/gzip):
    • Importing only $createListNode: unchanged (unused extensions now
      fully tree-shaken).
    • Editor already using the pipeline: −0.3 KB minified.
    • Rich-text editor not using the pipeline: net +0.2 KB gzip vs. main
      (implicit rules cost ~5 KB gzip, offset by ~4.7 KB recovered from the
      tree-shaking fix); typical multi-feature editor: net +1.3 KB gzip.

claude added 3 commits June 9, 2026 18:21
Each node-providing extension (RichTextExtension, ListExtension,
LinkExtension, TableExtension, CodeExtension) now depends on
CoreImportExtension and contributes its own DOMImportExtension rules, so
editors no longer need to list the per-package *ImportExtension bundles
explicitly — the import pipeline is configured by the same extensions
that register the nodes. The rules are inert unless the editor routes
HTML through the pipeline (e.g. via ClipboardDOMImportExtension or
$generateNodesFromDOMViaExtension).

- The <hr> rule moves into CoreImportRules, gated on HorizontalRuleNode
  registration (mirroring the legacy importDOM contract), because
  @lexical/extension is upstream of @lexical/html and cannot register
  import rules itself.
- ClipboardDOMImportExtension now depends on CoreImportExtension so the
  paste on-switch is self-sufficient.
- RichTextImportExtension / ListImportExtension / LinkImportExtension /
  TableImportExtension / CodeImportExtension /
  HorizontalRuleImportExtension remain as deprecated aliases of the
  corresponding runtime extensions.
- dev-examples/dom-import and the playground drop the explicit importer
  lists; the playground's PlaygroundRichTextImportExtension is removed
  (TableExtension is now listed directly in PlaygroundRichTextExtension).

https://claude.ai/code/session_018KKe2pqTUmq1pTi4SVdBk2
…ents

The @__NO_SIDE_EFFECTS__ annotations on defineExtension, createCommand,
and the other pure factories were only honored by esbuild for calls in
the same file as the annotated definition (and not at all by
webpack/terser consumers), so unused top-level factory results — entire
extension definitions, commands, and import rules — were being retained
in application bundles.

- Annotate top-level factory call sites (defineExtension, createCommand,
  defineImportRule, defineOverlayRules, createState, createImportState,
  createRenderState, domOverride) with /* @__PURE__ */, which esbuild,
  Rollup, webpack, and terser all honor cross-module.
- Also annotate argument-position calls to configExtension, safeCast,
  declarePeerDependency, and domOverride: a pure call is only droppable
  when its arguments are side-effect-free, so these nested calls were
  blocking elimination of the enclosing extension definitions (e.g. an
  app importing only $createListNode would otherwise retain
  ListExtension and the DOM-import machinery it now references).
- Preserve the annotations in production dist output via terser's
  format.preserve_annotations so downstream bundlers see them too.

Measured with esbuild against workspace sources (NODE_ENV=production,
minify): an app importing only node factories now pays zero cost for
unused extensions, and a rich-text editor bundle shrinks by ~14 KB
minified (~4.7 KB gzip) from dropping the previously-retained unused
clipboard/DOM-import definitions.

https://claude.ai/code/session_018KKe2pqTUmq1pTi4SVdBk2
Enforces the /* @__PURE__ */ annotation on module-scope calls to the
side-effect-free factories (defineExtension, configExtension, safeCast,
createCommand, createState, defineImportRule, defineOverlayRules,
createImportState, createContextState, createRenderState, domOverride)
so the tree-shaking guarantees stay maintained automatically: the
pre-commit eslint --fix inserts missing annotations and CI lint catches
anything else. Calls inside function bodies are exempt (they don't
affect module-level tree-shaking), as are tests (never bundled).

The AST-based rule also caught the call sites the initial regex codemod
missed (declarations whose type annotation spans lines, e.g.
`LexicalCommand<...>\n> = createCommand(...)`), which are annotated
here, and AGENTS.md documents the convention and how to extend the
factory list.

https://claude.ai/code/session_018KKe2pqTUmq1pTi4SVdBk2
@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 Jun 9, 2026
@vercel

vercel Bot commented Jun 9, 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 Jun 9, 2026 10:33pm
lexical-playground Ready Ready Preview, Comment Jun 9, 2026 10:33pm

Request Review

@etrepum etrepum changed the title [*] Refactor: Register DOMImportExtension rules implicitly via node extensions and make tree-shaking annotations effective [Breaking Changes][*] Refactor: Register DOMImportExtension rules implicitly via node extensions and make tree-shaking annotations effective Jun 9, 2026
@etrepum etrepum marked this pull request as ready for review June 9, 2026 22:49

@potatowagon potatowagon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed by Navi (Tater Thoughts Bobblehead) on behalf of @potatowagon.

LGTM — safe to land. www backwards-compat verified, no action needed.

What this PR does: registers DOMImportExtension rules implicitly via node-providing extensions (RichText, List, Link, Table, Code, HorizontalRule) instead of explicit wiring, and annotates module-scope factory calls with /* @__PURE__ */ for effective tree-shaking. New ESLint rule require-pure-annotation enforces it. 121 files, ~4700 lines, mostly mechanical.

Backwards compatibility — traced against Meta www consumers:
I checked whether www actually depends on the surface this PR refactors. It does not, on the path that changed:

  • New extension import API has zero www source consumers: DOMImportExtension, CoreImportExtension, RichTextImportExtension, CodeImportExtension, ListImportExtension, ClipboardDOMImportExtension — none are imported by any www .js/.jsx/.ts/.tsx source file (only the synced Haste bundles re-declare them, which get re-synced anyway).
  • wwws real HTML→Lexical path is the legacy $generateNodesFromDOM API (258 source references), which this PR does not touch.
  • The <hr> behavioral change (rule now gated on HorizontalRuleNode registration) is therefore moot for www: the surfaces that handle horizontal rules (GSD status-report utils, ASA R2 protocol viewer, Bento analytics rich text, CaaS editor) all already register HorizontalRuleNode explicitly in their node lists, and none route through the new extension import rules anyway.
  • Deprecated *ImportExtension aliases are preserved with @deprecated JSDoc + explicit regression tests (e.g. "deprecated HorizontalRuleImportExtension alias still imports <hr>"). No hard export removals — the "removed" exports in the diff are re-added under the same names with the pure annotation.

Conclusion: No internal www call sites break. The change is additive/mechanical for OSS consumers, the deprecation path is covered by tests, and Meta www doesnt use the affected extension import API in source. CI is all green across the full matrix. Safe to land.

@etrepum etrepum added this pull request to the merge queue Jun 10, 2026
Merged via the queue into facebook:main with commit 27b2a33 Jun 10, 2026
50 checks passed
mayrang added a commit to mayrang/lexical that referenced this pull request Jun 10, 2026
…rts (facebook#8603)

The rebase onto origin/main resolved a conflict with facebook#8662 (Register DOMImportExtension rules implicitly via node extensions) by keeping the PlaygroundRichTextImportExtension addition, but the import block at the top of the file lost the five rich-text dependencies it references (RichTextImportExtension, ListImportExtension, TableImportExtension, CodeImportExtension, HorizontalRuleImportExtension). tsc passes but the dependencies array referenced undefined identifiers at runtime.
mayrang added a commit to mayrang/lexical that referenced this pull request Jun 10, 2026
…rts (facebook#8603)

The rebase onto origin/main resolved a conflict with facebook#8662 (Register DOMImportExtension rules implicitly via node extensions) by keeping the PlaygroundRichTextImportExtension addition, but the import block at the top of the file lost the five rich-text dependencies it references (RichTextImportExtension, ListImportExtension, TableImportExtension, CodeImportExtension, HorizontalRuleImportExtension). tsc passes but the dependencies array referenced undefined identifiers at runtime.
mayrang added a commit to mayrang/lexical that referenced this pull request Jun 10, 2026
…rts (facebook#8603)

The rebase onto origin/main resolved a conflict with facebook#8662 (Register DOMImportExtension rules implicitly via node extensions) by keeping the PlaygroundRichTextImportExtension addition, but the import block at the top of the file lost the five rich-text dependencies it references (RichTextImportExtension, ListImportExtension, TableImportExtension, CodeImportExtension, HorizontalRuleImportExtension). tsc passes but the dependencies array referenced undefined identifiers at runtime.
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. extended-tests Run extended e2e tests on a PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants