facebook / lexical

Lexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.
https://lexical.dev
MIT License
17.5k stars 1.45k forks source link

Bug: MutationObserver error when deploying to Vercel #5980

Closed colin-jiang closed 1 week ago

colin-jiang commented 2 weeks ago

I'm getting a MutationObserver production error when using Lexical 0.14, I can't reproduce it on localhost and it fails only in production when I deploy to Vercel. This error goes away once I revert to Lexical 0.13

Error message: TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'

Example repo reproducing this issue: https://github.com/colin-jiang/lexical-mutationobserver-bug

Steps To Reproduce

  1. Download example repo and npm run dev, home page should properly render with richtexteditor input
  2. Deploy to Vercel: https://lexical-mutationobserver-bug.vercel.app/
  3. Home page crashes with client side error due to MutationObserver issue, error in console

Dependencies listed in package.json Next.js page router and Next.js version 12.0.9.

Screen Shot 2024-04-28 at 2 10 52 AM

The current behavior

Lexical editor throws error on load when deployed, no error on localhost

The expected behavior

No error

etrepum commented 2 weeks ago

As far as I can tell this looks like a bug in next.js, the minifier is doing an incorrect rename

Here's a segment of the production code that's broken:

minified commitPendingUpdates ```js function so(t, e) { const n = t._pendingEditorState, r = t._rootElement, i = t._headless || null === r; if (null === n) return; const s = t._editorState, l = s._selection, c = n._selection, a = 0 !== t._dirtyType, u = qr, d = Jr, f = Ur, h = t._updating, g = t._observer; let _ = null; if ( ((t._pendingEditorState = null), (t._editorState = n), !i && a && null !== g) ) { (Ur = t), (qr = n), (Jr = !1), (t._updating = !0); try { const e = t._dirtyType, r = t._dirtyElements, o = t._dirtyLeaves; g.disconnect(), (_ = (function (t, e, n, r, o, i) { (Xe = ""), (Qe = ""), (Ye = ""), (tn = 2 === r), (nn = null), (Ke = n), (Be = n._config), (Ve = n._nodes), (je = Ke._listeners.mutation), (qe = o), (Ue = i), (Je = t._nodeMap), ($e = e._nodeMap), (en = e._readOnly), (Ge = new Map(n._keyToDOMMap)); const s = new Map(); return ( (He = s), _n("root", null), (Ke = void 0), (Ve = void 0), (qe = void 0), (Ue = void 0), (Je = void 0), ($e = void 0), (Be = void 0), (Ge = void 0), (He = void 0), s ); })(s, n, t, e, r, o)); } catch (o) { if ((o instanceof Error && t._onError(o), $r)) throw o; return ( Po(t, null, r, n), Pt(t), (t._dirtyType = 2), ($r = !0), so(t, s), void ($r = !1) ); } finally { g.observe(o, Hr), (t._updating = h), (qr = u), (Jr = d), (Ur = f); } // removed code here } // removed code here } ```

Here are just the relevant parts:

function so(t, e) {
  const r = t._rootElement;
  if (…) {
    try {
      const o = t._dirtyLeaves;
    } catch (o) {
      return;
    } finally {
      g.observe(o, Hr);
    }
  }
}

This is the original (only relevant parts):

export function commitPendingUpdates(
  editor: LexicalEditor,
  recoveryEditorState?: EditorState,
): void {
  const rootElement = editor._rootElement;
  if (…) {
    try {
      const dirtyLeaves = editor._dirtyLeaves;
    } catch (error) {
      return;
    } finally {
      observer.observe(rootElement as Node, observerOptions);
    }
  }
}

It looks like there's an unsafe and incorrect renaming in the production output. Maybe upgrading next.js would help fix this, I don't think there's anything that can be done in lexical to work around it.

etrepum commented 2 weeks ago

This is what lexical's prod build looks like, which would be what next.js's minifier is using as input and explains why next.js got this wrong in this specific way.

function Vi(t, n) {
  const i = t._rootElement;
  if (...) {
    try {
      const i = t._dirtyLeaves;
    } catch (e) {
      return;
    } finally {
      g.observe(i, Ii);
    }
  }
}

A correct minifier implementation would realize that the try {} and finally {} do not have the same scope.

You can try it yourself in any JavaScript implementation:

(() => { const i = 'outer'; try { const i = 'inner'; } finally { console.log(i); } })()
outer
etrepum commented 1 week ago

If anyone else is seeing this: I've heard reports that the swc minifier does not have this bug, and it's the default minifier in the latest version of next.js.