[MM-70687] Expose host JSX runtimes to web app plugins (#38489)
* chore(webapp): upgrade to React 19 Bump react, react-dom and react-is to 19.2.8 across the webapp workspaces along with their @types packages, and upgrade the React ecosystem packages that needed newer releases to support React 19: - @mui/material, @mui/base and @mui/styled-engine-sc - @floating-ui/react - react-day-picker - @types/react-transition-group, @types/styled-components Replace the unmaintained react-beautiful-dnd with its actively maintained fork @hello-pangea/dnd, which is API compatible, ships its own types and supports React 19. This drops both the @types/react-beautiful-dnd dependency and the local patch that forced react-redux 9 compatibility. React 19 removes the legacy ReactDOM.render entry point, so the app now always mounts through createRoot and the webapp side of the EnableConcurrentReact feature flag is gone. Apply the types-react-codemod scoped-jsx and react-element-default-any-props presets to account for React 19 removing the global JSX namespace and defaulting ReactElement props to unknown, plus the manual type fixes those changes surfaced. Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com> * chore(webapp): pin react at the workspace root so a single copy resolves With react bumped to 19 only inside the workspaces, npm kept an older react at the hoisted root to satisfy the peer requirements of packages such as react-intl, leaving the app with two React copies. Declare react, react-dom and their types at the webapp root the same way react-intl is already declared, which is the existing pattern for singleton packages shared across workspaces, and regenerate the lockfile. Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com> * fix(webapp): read a tooltip child's ref from its props React 19 moved ref into props and made element.ref throw a deprecation error on access. Read children.props.ref instead, and apply the merged ref after spreading the child's props so the child's own ref no longer overwrites it. Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com> * chore(webapp): resolve a single react-is that matches React 19 react-is 18 does not recognize React 19's react.transitional.element symbol, so every consumer that inspects elements through it - pretty-format's snapshot serializer and styled-components among them - silently failed its checks. Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com> * test(webapp): expect no legacy context argument on component calls React 19 drops legacy context, so it now hands function components undefined as the second argument instead of an empty context object. Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com> * test(webapp): re-record snapshots for React 19 and dependency updates The recorded output changes because React 19 keeps ref in props and omits an empty value attribute, @hello-pangea/dnd uses data-rfd-* in place of data-rbd-*, styled-components and MUI generate different class hashes, and the image asset mock no longer resolves to an empty string. Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com> * fix(webapp): keep createRoot available to plugins on window.ReactDOM React 19 serves createRoot and hydrateRoot only from react-dom/client. Plugins are built against React 18 and reach them through react-dom's root entry, which react-dom 18's client.js re-exports from, so every plugin that mounts its own root got undefined instead of a function. Because a plugin's initialize call is not guarded, the throw took the whole web app down with it: with plugins enabled nothing rendered at all, which failed essentially the entire e2e suite. Exposing react-dom and react-dom/client as one object keeps both surfaces available to plugins built against either version. Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com> * test(e2e): wait for lazy views to mount instead of racing them React 19 throttles revealing a Suspense boundary's content for ~300ms once a fallback has been shown, so the first navigation to a lazily-loaded chunk commits later than it did on React 18. Two specs sampled state within that window: - last_viewed_spec asserted on the team's last viewed type immediately after clicking Threads, but that is recorded when GlobalThreads mounts. Wait for the view instead, which is the condition the assertion depends on. - channel_name_tooltips_spec reached the direct message search box via cy.focused(), which resolved to the button that opened the modal before focus moved into it. Select the input directly, as every other spec does. Both specs pass on React 18 and React 19 with these changes. Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com> * fix(threads): guard clear() so it cannot undo a later navigation useThreadRouting's clear() unconditionally replaced the URL with the threads route of the team captured when the callback was created. GlobalThreads calls it from an effect that fires once its threads finish loading, so a navigation that landed between the render and that effect flush was silently undone. React 19 makes this reachable in practice: its Suspense fallback-reveal throttle delays the lazily loaded threads view by ~300ms, widening the window enough that switching teams right after opening Threads bounced back to the previous team. The test file moves to .tsx so it can render the hook inside a route match. Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com> * fix(webapp): preserve live history and React 18 plugin compatibility * DO NOT MERGE: run React 19 StrictMode E2E discovery Port Jesse Hallam's discovery tooling from #38235 onto the React 19 upgrade, adapting the development runtime loader and warning detector. Force StrictMode and E2E exception detection on for this experiment. Keep NODE_ENV=production outside React and preserve component names. Both E2E self-tests require instrumentation in discovery mode and use a React 19 warning. Local Playwright and Cypress self-tests, 13 diagnostic unit tests, the production build, webapp style/types, and Playwright checks pass. Cypress lint passes; its typecheck reports errors in unchanged bot-account and collapsed-thread specs. Revert this entire commit after collecting the full CI/E2E results. * Revert "DO NOT MERGE: run React 19 StrictMode E2E discovery" This reverts commit 564757d380380ec0d53b41b07d229e6ea96d1538. * fix(webapp): cover upstream React 19 types and stabilize ranked policy tests * test(webapp): increase convert GM modal test timeout * MM-70612 Log plugin React DOM root calls * MM-70612 Use real script elements in plugin lifecycle tests * guard against plugin failures (sync and async); don't hang webapp * MM-70678 Sample plugin startup failure reports at 5% * export jsx-runtime; tests; docs * MM-70687 Simplify JSX runtime docs to require Mattermost 12.0 * fix(webapp): deactivate superseded plugin scripts --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com>
C
Christopher Poile committed
e97df5b6e18a227cf9aa3cf47317f44672d9eaef
Parent: cfa66f0
Committed by GitHub <noreply@github.com>
on 9/14/2026, 4:23:30 PM