[lexical-extension][lexical-react] Refactor: publish @lexical/extension as subpath exports - #9155
Closed
potatowagon wants to merge 1 commit into
Closed
potatowagon wants to merge 1 commit into
potatowagon wants to merge 1 commit into
Conversation
…on as subpath exports
## Description
`@lexical/extension` publishes a single entry point that contains both the
builder core and all 20 bundled extension implementations. Anything that
builds an editor — `LexicalExtensionComposer`, or `buildEditorFromExtensions`
directly — pulls the whole module even when it uses nothing but
`defineExtension` and `LexicalBuilder`.
That is invisible to a bundler that can tree-shake, which is most of them.
It is not invisible to a consumer that compiles the published bundles
without shaking them: Meta's www vendors this repo's Rollup output as one
pre-bundled CommonJS module per package, so requiring the entry point costs
39.9 kB raw / 12.9 kB gzipped there regardless of what is used. The same
applies to any consumer loading `dist/` through a CJS path.
This changes the package to the shape `@lexical/react` already uses: one
export per `src/*.ts`, with `index.ts` kept as a barrel so
`import {...} from '@lexical/extension'` continues to resolve everything.
- `main`/`module` are dropped from `package.json`, which is what makes
`updateVersion.mjs` generate one export entry per source file rather than
a single `.` entry. The exports map, flow stubs, tsconfigs and www stubs
in this diff are all generated by `pnpm run update-packages`.
- Sibling imports inside the package move from relative (`./signals`) to
package-qualified (`@lexical/extension/signals`), matching
`@lexical/react`. This is what keeps shared code shared: a relative import
is inlined into each entry that uses it, so with 18 modules importing
`./signals`, splitting without this would put 18 copies of the signals
re-export — and, in the www build where `@preact/signals-core` is not
external, 18 copies of the signals runtime — into the output. Signals
state is module-scope, so duplicates do not merely cost bytes, they break
batching and effect tracking. Verified below that exactly one module
contains it.
- `@lexical/react`'s own imports move to the subpaths, so the split
actually reaches the React entry points rather than being available in
principle.
No public API changes: every symbol previously exported from
`@lexical/extension` is still exported from it.
## Test plan
### Before
Everything reachable from `LexicalExtensionComposer`, www build at 0.50.0:
```
LexicalExtension 39906 raw 12860 gz (20 extensions + signals runtime)
```
### After
`pnpm run build-www`, then walking `require()` edges from
`LexicalExtensionComposer`:
```
LexicalExtensionConfig 733 raw 458 gz
LexicalExtensionDeepThemeMergeInPlace 700 raw 457 gz
LexicalExtensionExtensionRep 5233 raw 1683 gz
LexicalExtensionGetExtensionDependencyFromEditor 966 raw 580 gz
LexicalExtensionInitialStateExtension 1176 raw 666 gz
LexicalExtensionLexicalBuilder 5783 raw 2224 gz
TOTAL 14591 raw 6068 gz
```
6 of 33 modules, down from the whole package: **-25,315 raw / -6,792 gz,
53% of the gzipped cost.** The signals runtime is no longer reachable from
an editor build at all, and the barrel is not reachable either.
Signals is in exactly one module (www build, where it is inlined rather
than external):
```
$ grep -l "preact-signals" packages/lexical-extension/dist/*.prod.js
packages/lexical-extension/dist/LexicalExtensionSignals.prod.js
```
Backwards compatibility — all 58 value exports still resolve from the
barrel after the split:
```
barrel runtime exports after split: 58
value exports expected: 58
MISSING from barrel: none
```
Suites:
```
$ pnpm run test-unit
Test Files 324 passed (324)
Tests 7790 passed | 1 skipped (7791)
$ npx vitest run scripts/__tests__/integration/tree-shaking.test.mjs
Test Files 1 passed (1)
Tests 270 passed (270)
$ pnpm run ci-check
(tsc, tsc-scripts, tsc-extension, tsc-website, flow, prettier, lint)
exit 0
```
E2E and browser suites were not run in this environment.
potatowagon
requested review from
acywatson,
etrepum,
fantactuka,
ivailop7 and
zurfyx
as code owners
September 11, 2026 09:28
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
etrepum
reviewed
Sep 11, 2026
etrepum
left a comment
Collaborator
There was a problem hiding this comment.
I don't think this is really sufficient for your needs because most other packages like rich-text have dependencies on @lexical/extension as well.
The right move here is likely to have these subpath imports managed by eslint or a build plugin to eliminate all monorepo barrel imports
|
|
||
| /** | ||
| * LexicalExtensionAutoFocusExtension | ||
| */ |
Collaborator
There was a problem hiding this comment.
It looks like all of the flow types are missing from these stubs, they need to be moved from the barrel flow file. For example this line belongs in this file (plus any imports to make it work):
declare export var AutoFocusExtension: LexicalExtension<AutoFocusConfig, "@lexical/extension/AutoFocus", NamedSignalsOutput<AutoFocusConfig>, void>;
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
@lexical/extensionpublishes a single entry point that contains both thebuilder core and all 20 bundled extension implementations. Anything that
builds an editor —
LexicalExtensionComposer, orbuildEditorFromExtensionsdirectly — pulls the whole module even when it uses nothing but
defineExtensionandLexicalBuilder.That is invisible to a bundler that can tree-shake, which is most of them.
It is not invisible to a consumer that compiles the published bundles
without shaking them: Meta's www vendors this repo's Rollup output as one
pre-bundled CommonJS module per package, so requiring the entry point costs
39.9 kB raw / 12.9 kB gzipped there regardless of what is used. The same
applies to any consumer loading
dist/through a CJS path.This changes the package to the shape
@lexical/reactalready uses: oneexport per
src/*.ts, withindex.tskept as a barrel soimport {...} from '@lexical/extension'continues to resolve everything.main/moduleare dropped frompackage.json, which is what makesupdateVersion.mjsgenerate one export entry per source file rather thana single
.entry. The exports map, flow stubs, tsconfigs and www stubsin this diff are all generated by
pnpm run update-packages../signals) topackage-qualified (
@lexical/extension/signals), matching@lexical/react. This is what keeps shared code shared: a relative importis inlined into each entry that uses it, so with 18 modules importing
./signals, splitting without this would put 18 copies of the signalsre-export — and, in the www build where
@preact/signals-coreis notexternal, 18 copies of the signals runtime — into the output. Signals
state is module-scope, so duplicates do not merely cost bytes, they break
batching and effect tracking. Verified below that exactly one module
contains it.
@lexical/react's own imports move to the subpaths, so the splitactually reaches the React entry points rather than being available in
principle.
No public API changes: every symbol previously exported from
@lexical/extensionis still exported from it.Test plan
Before
Everything reachable from
LexicalExtensionComposer, www build at 0.50.0:After
pnpm run build-www, then walkingrequire()edges fromLexicalExtensionComposer:6 of 33 modules, down from the whole package: -25,315 raw / -6,792 gz,
53% of the gzipped cost. The signals runtime is no longer reachable from
an editor build at all, and the barrel is not reachable either.
Signals is in exactly one module (www build, where it is inlined rather
than external):
Backwards compatibility — all 58 value exports still resolve from the
barrel after the split:
Suites:
E2E and browser suites were not run in this environment.
Context: #9153.