[Breaking Changes][*] Refactor: Register DOMImportExtension rules implicitly via node extensions and make tree-shaking annotations effective#8662
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
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/.tsxsource file (only the synced Haste bundles re-declare them, which get re-synced anyway). - wwws real HTML→Lexical path is the legacy
$generateNodesFromDOMAPI (258 source references), which this PR does not touch. - The
<hr>behavioral change (rule now gated onHorizontalRuleNoderegistration) 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 registerHorizontalRuleNodeexplicitly in their node lists, and none route through the new extension import rules anyway. - Deprecated
*ImportExtensionaliases are preserved with@deprecatedJSDoc + 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.
…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.
…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.
…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.
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
CodeImportExtensionare now defined byCodeExtension, soCodeImportExtensionis now simply an alias. As an@experimentalAPI, 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.ClipboardDOMImportExtensionto handle paste or an explicit call to$generateNodesFromDOMViaExtension).Description
The
DOMImportExtensionpipeline previously required listing a per-packageimport 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,CodeExtensioneachdepend on
CoreImportExtension+configExtension(DOMImportExtension, {rules})):(
ClipboardDOMImportExtensionor$generateNodesFromDOMViaExtension);the legacy paste path is unchanged.
<hr>rule moves intoCoreImportRules, gated onHorizontalRuleNoderegistration (mirroring the legacyimportDOMcontract), because
@lexical/extensionis upstream of@lexical/htmland cannot register import rules itself.
ClipboardDOMImportExtensionnow depends onCoreImportExtension, sothe paste on-switch is self-sufficient.
RichTextImportExtension/ListImportExtension/LinkImportExtension/
TableImportExtension/CodeImportExtension/HorizontalRuleImportExtensionremain as deprecated aliases.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 thedefinition 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-positionconfigExtension/safeCastcalls, which otherwise pin the enclosingdefinition), and terser is configured with
format.preserve_annotationsso the annotations survive into prod dist output.
3. Keep it that way automatically. New
@lexical/internal/require-pure-annotationESLint rule (with autofixer)requires the annotation on module-scope calls to the pure factories;
pre-commit
eslint --fixinserts them, CI lint backstops. Documented inAGENTS.md.The dev example, playground, and
serialization/dom-import.mddocs areupdated for the implicit configuration.
Test plan
Before
Editor setup required explicit importer wiring:
Unused
defineExtension(...)/createCommand(...)results were retained byesbuild/webpack consumers; e.g. importing only
$createListNodefrom@lexical/listretainedListExtension, and ~14 KB minified of unusedclipboard/DOM-import definitions shipped in every rich-text bundle.
After
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.
NODE_ENV=production, minified/gzip):$createListNode: unchanged (unused extensions nowfully tree-shaken).
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.