Skip to content

fix(richtext-lexical): block fields, custom nodes, and converters now type correctly#16919

Merged
AlessioGr merged 13 commits into
mainfrom
templates/fix-lexical-types
Jun 8, 2026
Merged

fix(richtext-lexical): block fields, custom nodes, and converters now type correctly#16919
AlessioGr merged 13 commits into
mainfrom
templates/fix-lexical-types

Conversation

@AlessioGr

@AlessioGr AlessioGr commented Jun 8, 2026

Copy link
Copy Markdown
Member

The strict lexical types added by #16782 were too lossy in practice: block fields went missing, custom nodes couldn't be added to the defaults, and converters couldn't be typed. This PR fixes that.

What this fixes

  • Block fields show up again. When a rich text field allows multiple blocks, you can now read and write each block's own fields (e.g. node.fields.code) - with autocomplete, and errors when you use a field that doesn't belong to that block. Before, the fields of all the blocks got merged together and each block's own fields were lost.

  • Adding your own nodes works. Use WithDefaultNodes<...> to combine the built-in nodes with your custom blocks. The old DefaultNodeTypesOf helper couldn't actually be used the way the docs showed - it produced a circular-type error. See this ts playground

  • Converters can be typed. You can now pass a strongly-typed converters function to <RichText> and spread ...defaultConverters into it. Both used to be rejected by the type-checker.

  • No more phantom inline-block node in generated types when an editor has no inline blocks (and the same for block nodes).

Also migrated the website template to WithDefaultNodes, updated the converter docs, and added type + integration tests.

Breaking changes

These breaking changes are relative to the 4.0 beta already on main - not new breaking changes for users upgrading from 3.0

  • DefaultNodeTypesOf is removed - use WithDefaultNodes.

@AlessioGr AlessioGr enabled auto-merge (squash) June 8, 2026 07:03
@AlessioGr AlessioGr disabled auto-merge June 8, 2026 07:04
@AlessioGr AlessioGr enabled auto-merge (squash) June 8, 2026 07:04
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

📦 esbuild Bundle Analysis for payload

This analysis was generated by esbuild-bundle-analyzer. 🤖

Meta File Out File Size (raw) Note
packages/next/meta_index.json esbuild/index.js 201.84 KB ✅ No change
packages/payload/meta_index.json esbuild/index.js 1.41 MB ✅ No change
packages/payload/meta_shared.json esbuild/exports/shared.js 192.56 KB ✅ No change
packages/richtext-lexical/meta_client.json esbuild/exports/client_optimized/index.js 305.26 KB ⚠️ +4 B (+0.0%)
packages/ui/meta_client.json esbuild/exports/client_optimized/index.js 1.35 MB ✅ No change
packages/ui/meta_shared.json esbuild/exports/shared_optimized/index.js 18.64 KB ✅ No change
Largest paths These visualization shows top 20 largest paths in the bundle.

Meta file: packages/next/meta_index.json, Out file: esbuild/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ████████████████████████▋ }}}$ 98.9%, 197.86 KB
dist/adapters/router.js ${{\color{Goldenrod}{ }}}$ 0.3%, 663 B
dist/adapters/server.js ${{\color{Goldenrod}{ }}}$ 0.3%, 533 B
dist/adapters/layout.js ${{\color{Goldenrod}{ }}}$ 0.3%, 526 B
dist/adapters/views.js ${{\color{Goldenrod}{ }}}$ 0.2%, 409 B
dist/esbuildEntry.js ${{\color{Goldenrod}{ }}}$ 0.0%, 0 B

Meta file: packages/payload/meta_index.json, Out file: esbuild/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ █████████████████ }}}$ 68.3%, 960.54 KB
dist/fields/hooks ${{\color{Goldenrod}{ ▊ }}}$ 3.1%, 44.05 KB
dist/collections/operations ${{\color{Goldenrod}{ ▋ }}}$ 2.9%, 40.26 KB
dist/versions/migrations ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 18.50 KB
dist/auth/operations ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 15.63 KB
dist/utilities/configToJSONSchema.js ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 15.01 KB
dist/fields/config ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 13.40 KB
dist/globals/operations ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 13.40 KB
dist/queues/operations ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 12.63 KB
dist/fields/validations.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 10.57 KB
dist/collections/config ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 9.53 KB
dist/bin/generateImportMap ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 9.47 KB
dist/config/orderable ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.92 KB
dist/uploads/fetchAPI-multipart ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.80 KB
dist/index.js ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.77 KB
dist/hierarchy/utils ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 7.64 KB
dist/database/migrations ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 7.55 KB
dist/collections/endpoints ${{\color{Goldenrod}{ }}}$ 0.4%, 6.23 KB
dist/queues/config ${{\color{Goldenrod}{ }}}$ 0.4%, 5.59 KB
dist/auth/strategies ${{\color{Goldenrod}{ }}}$ 0.4%, 5.43 KB
(other) ${{\color{Goldenrod}{ ███████▉ }}}$ 31.7%, 445.71 KB

Meta file: packages/payload/meta_shared.json, Out file: esbuild/exports/shared.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ███████████████████▉ }}}$ 79.5%, 150.12 KB
dist/fields/validations.js ${{\color{Goldenrod}{ █▍ }}}$ 5.6%, 10.57 KB
dist/config/orderable ${{\color{Goldenrod}{ ▍ }}}$ 1.7%, 3.13 KB
dist/fields/baseFields ${{\color{Goldenrod}{ ▍ }}}$ 1.5%, 2.79 KB
dist/utilities/deepCopyObject.js ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 2.54 KB
dist/auth/cookies.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 1.55 KB
dist/utilities/flattenTopLevelFields.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 1.42 KB
dist/fields/config ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 1.29 KB
dist/utilities/getVersionsConfig.js ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 1.04 KB
dist/utilities/flattenAllFields.js ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 943 B
dist/utilities/unflatten.js ${{\color{Goldenrod}{ }}}$ 0.4%, 779 B
dist/utilities/sanitizeUserDataForEmail.js ${{\color{Goldenrod}{ }}}$ 0.4%, 713 B
dist/utilities/getFieldPermissions.js ${{\color{Goldenrod}{ }}}$ 0.3%, 651 B
dist/collections/config ${{\color{Goldenrod}{ }}}$ 0.3%, 570 B
dist/bin/generateImportMap ${{\color{Goldenrod}{ }}}$ 0.3%, 561 B
dist/auth/sessions.js ${{\color{Goldenrod}{ }}}$ 0.3%, 525 B
dist/fields/getFieldPaths.js ${{\color{Goldenrod}{ }}}$ 0.3%, 485 B
dist/utilities/appendDateTimezoneSelectFields.js ${{\color{Goldenrod}{ }}}$ 0.2%, 451 B
dist/utilities/getSafeRedirect.js ${{\color{Goldenrod}{ }}}$ 0.2%, 423 B
dist/utilities/deepMerge.js ${{\color{Goldenrod}{ }}}$ 0.2%, 413 B
(other) ${{\color{Goldenrod}{ █████▏ }}}$ 20.5%, 38.66 KB

Meta file: packages/richtext-lexical/meta_client.json, Out file: esbuild/exports/client_optimized/index.js

Path Size
dist/features/blocks ${{\color{Goldenrod}{ ███ }}}$ 12.4%, 37.43 KB
dist/lexical/ui ${{\color{Goldenrod}{ ██▊ }}}$ 11.3%, 34.16 KB
dist/lexical/plugins ${{\color{Goldenrod}{ ██▋ }}}$ 10.9%, 32.88 KB
dist/features/experimental_table ${{\color{Goldenrod}{ ██▎ }}}$ 9.0%, 27.16 KB
dist/packages/@lexical ${{\color{Goldenrod}{ █▌ }}}$ 6.3%, 18.99 KB
dist/features/link ${{\color{Goldenrod}{ █▌ }}}$ 6.2%, 18.82 KB
dist/features/toolbars ${{\color{Goldenrod}{ █▍ }}}$ 5.5%, 16.58 KB
dist/features/upload ${{\color{Goldenrod}{ █▏ }}}$ 4.7%, 14.09 KB
dist/features/textState ${{\color{Goldenrod}{ ▉ }}}$ 3.7%, 11.08 KB
dist/features/relationship ${{\color{Goldenrod}{ ▊ }}}$ 3.2%, 9.61 KB
dist/lexical/utils ${{\color{Goldenrod}{ ▋ }}}$ 2.9%, 8.79 KB
dist/features/converters ${{\color{Goldenrod}{ ▋ }}}$ 2.8%, 8.36 KB
dist/utilities/fieldsDrawer ${{\color{Goldenrod}{ ▋ }}}$ 2.7%, 8.12 KB
dist/features/debug ${{\color{Goldenrod}{ ▌ }}}$ 2.4%, 7.40 KB
dist/lexical/config ${{\color{Goldenrod}{ ▍ }}}$ 1.7%, 5.08 KB
dist/features/lists ${{\color{Goldenrod}{ ▍ }}}$ 1.7%, 5.00 KB
dist/features/format ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 3.46 KB
dist/lexical/LexicalEditor.js ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 3.23 KB
dist/features/horizontalRule ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 3.18 KB
dist/field/Field.js ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 2.84 KB
(other) ${{\color{Goldenrod}{ █████████████████████▉ }}}$ 87.6%, 264.60 KB

Meta file: packages/ui/meta_client.json, Out file: esbuild/exports/client_optimized/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ██████████▊ }}}$ 43.3%, 580.42 KB
dist/elements/Hierarchy ${{\color{Goldenrod}{ ▊ }}}$ 3.2%, 43.08 KB
dist/elements/BulkUpload ${{\color{Goldenrod}{ ▌ }}}$ 2.1%, 28.13 KB
dist/views/Version ${{\color{Goldenrod}{ ▌ }}}$ 2.0%, 27.46 KB
dist/views/HierarchyList ${{\color{Goldenrod}{ ▍ }}}$ 1.5%, 20.43 KB
dist/elements/Table ${{\color{Goldenrod}{ ▎ }}}$ 1.4%, 19.28 KB
dist/views/Dashboard ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 17.73 KB
dist/views/Edit ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 17.62 KB
dist/elements/WhereBuilder ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 17.34 KB
dist/forms/Form ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 15.89 KB
dist/fields/Relationship ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 15.48 KB
dist/fields/Blocks ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 15.11 KB
dist/fields/Upload ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 14.40 KB
dist/elements/QueryPresets ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 10.22 KB
dist/elements/PublishButton ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 8.67 KB
dist/elements/HTMLDiff ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 8.38 KB
dist/elements/LivePreview ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 8.26 KB
dist/views/List ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 8.15 KB
dist/elements/ReactSelect ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.83 KB
dist/fields/Array ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.76 KB
(other) ${{\color{Goldenrod}{ ██████████████▏ }}}$ 56.7%, 761.50 KB

Meta file: packages/ui/meta_shared.json, Out file: esbuild/exports/shared_optimized/index.js

Path Size
dist/graphics/Logo ${{\color{Goldenrod}{ ███████▋ }}}$ 30.9%, 5.57 KB
../../node_modules ${{\color{Goldenrod}{ ███▋ }}}$ 14.7%, 2.65 KB
dist/graphics/Icon ${{\color{Goldenrod}{ ██▏ }}}$ 8.5%, 1.52 KB
dist/utilities/formatDocTitle ${{\color{Goldenrod}{ █▊ }}}$ 7.4%, 1.32 KB
dist/providers/TableColumns ${{\color{Goldenrod}{ █▏ }}}$ 4.8%, 866 B
dist/utilities/getGlobalData.js ${{\color{Goldenrod}{ █ }}}$ 4.2%, 762 B
dist/utilities/api.js ${{\color{Goldenrod}{ █ }}}$ 4.2%, 756 B
dist/utilities/groupNavItems.js ${{\color{Goldenrod}{ █ }}}$ 4.1%, 745 B
dist/elements/Translation ${{\color{Goldenrod}{ ▋ }}}$ 2.7%, 493 B
dist/utilities/handleTakeOver.js ${{\color{Goldenrod}{ ▌ }}}$ 2.4%, 440 B
dist/utilities/traverseForLocalizedFields.js ${{\color{Goldenrod}{ ▌ }}}$ 2.2%, 399 B
dist/elements/withMergedProps ${{\color{Goldenrod}{ ▍ }}}$ 1.9%, 339 B
dist/utilities/getNavGroups.js ${{\color{Goldenrod}{ ▍ }}}$ 1.9%, 338 B
dist/utilities/getVisibleEntities.js ${{\color{Goldenrod}{ ▍ }}}$ 1.8%, 329 B
dist/elements/WithServerSideProps ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 232 B
dist/utilities/handleGoBack.js ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 180 B
dist/fields/mergeFieldStyles.js ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 157 B
dist/utilities/handleBackToDashboard.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 152 B
dist/forms/Form ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 148 B
dist/utilities/abortAndIgnore.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 146 B
(other) ${{\color{Goldenrod}{ █████████████████▎ }}}$ 69.1%, 12.45 KB
Details

Next to the size is how much the size has increased or decreased compared with the base branch of this PR.

  • ‼️: Size increased by 20% or more. Special attention should be given to this.
  • ⚠️: Size increased in acceptable range (lower than 20%).
  • ✅: No change or even downsized.
  • 🗑️: The out file is deleted: not found in base branch.
  • 🆕: The out file is newly found: will be added to base branch.

@AlessioGr AlessioGr requested review from GermanJablo and denolfe June 8, 2026 07:10

@GermanJablo GermanJablo 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.

  • I added a commit with the regenerated types.

  • The main thing I wanted to raise is the naming strategy for the generated types. Each rich-text field generates a type that lists every node it can contain (paragraphs, headings, links, blocks, and so on). That type is named LexicalNodes_<hash>, where <hash> is a fingerprint computed from the full contents of that list, so if any node in the editor changes, the fingerprint changes and the type gets a new name. The issue is that the type for a link's custom fields reuses that same editor fingerprint: today it takes the LexicalNodes_<hash> name and just swaps the prefix to produce LexicalLinkFields_<hash>. Because the name is tied to the fingerprint of the whole editor and not to the link fields themselves, adding an unrelated field to a block changes the editor fingerprint, which renames the link-fields type, which churns every reference to it. That is how a tiny change becomes a noisy diff. Upload fields already avoid this: they compute the hash from the upload fields' own content, so the name only changes when the upload fields change. Could we make link fields consistent with upload fields and hash them by their own content? That keeps the dedup and determinism but stops unrelated changes from cascading into LexicalLinkFields_* renames.

  • In this test, the assertion expect(generatedTypes).not.toContain('SerializedInlineBlockNode<{blockType: string}>') reads the generated output as plain text and checks that string is absent. The problem is the generator never emits that exact string: the real output is formatted differently (with spaces and a different shape, like SerializedInlineBlockNode<TFields extends { blockType: string }> for the declaration and SerializedInlineBlockNode<SomeBlock> for usages). A not.toContain for a string that can never appear always passes, so this test would stay green even if the bug came back. Could we assert against the shape the generator actually emits, so the test really guards the fix?

  • SerializedBlockNode / SerializedInlineBlockNode are written down twice: once as the real TypeScript types, and once as a hardcoded string that gets copied verbatim into every user's generated payload-types.ts. The two must be identical (there is even a comment saying they "MUST stay byte-for-byte in sync"), but nothing enforces it: if someone edits one and forgets the other, the type users receive in their generated file silently drifts from the real one. Could we add a small test that asserts the emitted string matches the runtime type, so this can't break unnoticed?

@AlessioGr AlessioGr requested a review from GermanJablo June 8, 2026 16:07
@AlessioGr AlessioGr merged commit 75d846c into main Jun 8, 2026
166 of 169 checks passed
@AlessioGr AlessioGr deleted the templates/fix-lexical-types branch June 8, 2026 16:27
denolfe added a commit that referenced this pull request Jun 8, 2026
# Overview

Fixes the `blank` and `website` template e2e suites, the only two
template jobs failing in CI.

## Key Changes

#### Update stale dashboard breadcrumb selector

The admin e2e login helper and dashboard test waited for
`span[title="Dashboard"]`, which no longer exists after the StepNav
refactor in #16680. The dashboard breadcrumb now renders with class
`.step-nav__first`. Applied to both `blank` and `website`.

#### Fix website rich-text prop types

Three website forwarder components (`Form`, `Form/Message`,
`RelatedPosts`) typed their rich-text props as
`DefaultTypedEditorState`. After #16919, generated fields are typed
`LexicalRichText<...>`, which is not assignable to that, so the website
`next build` type-check failed. They now use `SerializedEditorState`,
matching the `RichText` `data` prop they forward to.

## Design Decisions

`SerializedEditorState` is the type the underlying `RichText` component
already accepts for its `data` prop. Typing the forwarder props the same
way keeps them consistent with the sink and accepts any generated editor
state, which is what other blocks (`Content`, `CallToAction`) already
pass through.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants