Skip to content

[lexical][lexical-react] Bug Fix: actually route the update-recursion guard through editor._onWarn end-to-end (follow-up to #8644)#8658

Merged
potatowagon merged 1 commit into
facebook:mainfrom
potatowagon:fix-onwarn-route-cascade-guard
Jun 9, 2026
Merged

[lexical][lexical-react] Bug Fix: actually route the update-recursion guard through editor._onWarn end-to-end (follow-up to #8644)#8658
potatowagon merged 1 commit into
facebook:mainfrom
potatowagon:fix-onwarn-route-cascade-guard

Conversation

@potatowagon

@potatowagon potatowagon commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Description

Follow-up to #8644, which added the onWarn editor hook but left it dead for real consumers along two paths. The goal of #8644 was to let embedders route the #8542 update-recursion-guard warning to telemetry at warn severity; until both gaps below are fixed, no registered onWarn handler ever fires.

1. The recursion guard never reached _onWarn in compiled builds

$triggerEnqueuedUpdates called a $devInvariant(editor, …) helper that invoked editor._onWarn(...) in its body. But transform-error-messages rewrites those call sites into a hoisted if (!cond) { formatProd*Message(code, …) } and discards the helper body — dropping the editor reference. In the shipped bundle the guard compiled to a bare console.warn and editor._onWarn was invoked 0 times (verified in Lexical.prod.js / Lexical.dev.js; editor._onError is invoked at 3 sites, confirming the bundle is current).

Fix: call editor._onWarn(new Error(...)) directly at the guard so the routing survives into every build (dev, prod bundle, untransformed source). The tradeoff is that the message string ships in the bundle — unavoidable if the warning must reach _onWarn in a compiled artifact, because the error-message transform cannot preserve a call into the editor. Removes the now-unused $devInvariant helper, its transform-error-messages registration (and the conditionIndex/messageIndex plumbing added for it), and the transform unit tests.

2. @lexical/react's LexicalComposer did not forward onWarn

LexicalComposer destructured only onError (and friends) from initialConfig and never passed onWarn to createEditor. So even with #8644 + fix (1), embedders using the React wrapper — the common path — could not register an onWarn handler; it reached nothing.

Fix: add onWarn?: (error, editor) => void to InitialConfigType and forward it to createEditor as onWarn: error => onWarn(error, editor), mirroring onError.

The onWarn API surface from #8644 (CreateEditorArgs.onWarn, _onWarn, defaultOnWarn, LexicalBuilder wiring) is unchanged.

Test plan

  • Existing LexicalEditor.test.tsx recursion-guard tests continue to pass (default onWarn throws in dev; a custom onWarn receives the namespace-tagged Error once and nothing surfaces as an uncaught error).
  • New LexicalComposer.test.tsx test asserts initialConfig.onWarn is forwarded to the editor's _onWarn as a (error, editor) handler — the coverage that would have caught gap 2.
  • pnpm vitest --project unit packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx packages/lexical-react/src/__tests__/unit/LexicalComposer.test.tsx --run

Before

The recursion guard compiled to a bare console.warn; editor._onWarn invoked 0× in the bundle. LexicalComposer dropped onWarn entirely. Any registered onWarn handler never fired.

After

The guard calls editor._onWarn(...) directly, and LexicalComposer forwards onWarn into createEditor, so the compiled bundle invokes editor._onWarn at the guard site and React-wrapper embedders' onWarn handlers receive the recovered condition at warn severity.

Note for reviewers: unit tests run against untransformed source, where the old $devInvariant body executed — which is why they passed while the compiled bundle was broken. Worth confirming the built Lexical.*.js now invokes editor._onWarn at the guard site.

@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:02am
lexical-playground Ready Ready Preview, Comment Jun 9, 2026 10:02am

Request Review

@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
@potatowagon potatowagon force-pushed the fix-onwarn-route-cascade-guard branch from dd0f7bd to d5f0cb9 Compare June 9, 2026 09:50
@potatowagon potatowagon changed the title [lexical] Bug Fix: actually route the update-recursion guard through editor._onWarn (follow-up to #8644) Jun 9, 2026
… guard through editor._onWarn end-to-end

Follow-up to facebook#8644, which added the `onWarn` editor hook but left it dead for
real consumers along two paths.

1. The recursion guard never reached `_onWarn` in compiled builds.
   `$triggerEnqueuedUpdates` called a `$devInvariant(editor, …)` helper that
   invoked `editor._onWarn(...)` in its body — but `transform-error-messages`
   rewrites those call sites to a bare `formatProd*Message(code, …)` and
   discards the helper body, dropping the editor reference. In the shipped
   bundle the guard compiled to a plain `console.warn` and `editor._onWarn`
   was invoked 0 times.
   Fix: call `editor._onWarn(new Error(...))` directly at the guard so the
   routing survives into every build (dev, prod bundle, and untransformed
   source), at the cost of shipping the message string. Removes the now-unused
   `$devInvariant` helper, its `transform-error-messages` registration (and the
   `conditionIndex`/`messageIndex` plumbing), and the transform unit tests.

2. `@lexical/react`'s `LexicalComposer` did not forward `onWarn`.
   It destructured only `onError` (and friends) from `initialConfig` and never
   passed `onWarn` to `createEditor`, so even with facebook#8644 + fix (1), embedders
   using the React wrapper (the common path) could not register an `onWarn`
   handler — it reached nothing.
   Fix: add `onWarn?: (error, editor) => void` to `InitialConfigType` and
   forward it to `createEditor` as `onWarn: error => onWarn(error, editor)`,
   mirroring `onError`. Adds a unit test asserting the passthrough reaches the
   editor's `_onWarn` (the coverage that would have caught this gap).

The `onWarn` API surface from facebook#8644 (`CreateEditorArgs.onWarn`, `_onWarn`,
`defaultOnWarn`, `LexicalBuilder` wiring) is unchanged.

@etrepum etrepum left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This does lose the string capabilities that were in $devInvariant, so repeated use with descriptive messages would have a measurable impact on prod code size.

@potatowagon potatowagon added this pull request to the merge queue Jun 9, 2026
Merged via the queue into facebook:main with commit 970d0ec Jun 9, 2026
46 checks passed
@potatowagon

Copy link
Copy Markdown
Contributor Author

Thanks for the review and the quick turnaround on the unblock 🙏

Good point on the code-size tradeoff, and agreed — the direct editor._onWarn(new Error(...)) does forgo the errors.json minification that $devInvariant/invariant get, so the string ships in the bundle. The constraint that forced it: transform-error-messages rewrites the call site to formatProd*Message(code, …) and drops the helper body, so the editor reference (and the _onWarn call) never survives into a compiled/minified artifact — only when the untransformed source is consumed. That made the two goals mutually exclusive at this call site: strip the message or reach _onWarn in the built output, not both. For this one guard I went with the latter, since the point was getting the warn signal to a registered handler in prod builds.

You're right that it doesn't generalize — repeated descriptive _onWarn calls would add up. If more warn-level sites come up, the clean fix would be teaching the transform to emit editor._onWarn(formatProd*Message(code, …)) for a dedicated warn macro, preserving both the code-strip and the editor routing in compiled output. Happy to follow up with that if/when there's a second consumer — for now this keeps it to a single site.

— via Navi on behalf of @potatowagon

@etrepum etrepum mentioned this pull request Jun 25, 2026
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.

2 participants