Skip to content

[lexical-playground][lexical-html][lexical-extension] Refactor: Migrate playground HTML import/export to the DOMImportExtension pipeline#8590

Merged
etrepum merged 35 commits into
facebook:mainfrom
etrepum:claude/quirky-noether-6agbj
May 30, 2026
Merged

[lexical-playground][lexical-html][lexical-extension] Refactor: Migrate playground HTML import/export to the DOMImportExtension pipeline#8590
etrepum merged 35 commits into
facebook:mainfrom
etrepum:claude/quirky-noether-6agbj

Conversation

@etrepum

@etrepum etrepum commented May 29, 2026

Copy link
Copy Markdown
Collaborator

[lexical-playground] Refactor: migrate playground HTML import to DOMImportExtension

Summary

Completes #8578. Migrates the playground's HTML import from the imperative buildHTMLConfig / per-node importDOM approach to the extension-based DOMImportExtension pipeline in @lexical/html, with each feature owning its own import rules. Along the way this restores full parity with the legacy
$generateNodesFromDOM output, splits the importer set so it mirrors the node set per editor mode, tightens the settings→signals wiring, and makes the e2e harness fail fast when the editor can't build.

@lexical/html import pipeline (legacy parity)

  • Restore legacy block-container import semantics in the new pipeline: a schema-driven transparent block rule, block-aware child hoisting, and collapsing of line-break runs, so $generateNodesFromDOMViaExtension produces the same tree as the legacy $generateNodesFromDOM.
  • Drop ArtificialNode__DO_NOT_USE from the new importer path; nested block/inline grouping is handled by the schema instead.
  • New helpers: $propagateTextAlignToBlockChildren (@lexical/html) and isLastChildInBlockNode / isOnlyChildInBlockNode (lexical core).

Per-package importer extensions

  • RichTextImportExtension, ListImportExtension, TableImportExtension, CodeImportExtension (@lexical/code-core), HorizontalRuleImportExtension, and LinkImportExtension each define their own DOM import rules next to the node they import.
  • Leaf importers no longer depend on CoreImportExtension — the app/aggregator configures it once (it will be on by default in the future).
  • @lexical/list: flatten any block-level child of an <li> (not just paragraphs) and preserve nested lists; renamed isBoundary$isBoundary since it calls $-functions.

Playground feature extensions (named exports)

  • Convert the remaining playground plugins to named-export extensions (no default / *Plugin exports) and co-locate each feature's import rules with its extension: Images, Poll, Layout, DateTime, PageBreak, Twitter, YouTube,
    Collapsible, Mention, Excalidraw.
  • Remove buildHTMLConfig.tsx; add PlaygroundDOMRenderExtension, PlaygroundImportExtension, and round-trip import coverage for the migrated node importers.

Plain-text vs rich-text importer split

  • PlaygroundImportExtension is the plain-text-safe baseline (core / link / clipboard / playground inline-style overlay), added in every mode.
  • PlaygroundRichTextImportExtension carries the rich-text-only importers (rich-text / list / table / code / horizontal-rule) and is added only in rich-text mode. This keeps the importer set aligned with the node set and avoids pulling RichTextExtension (which conflicts with PlainTextExtension) into plain-text editors.

Settings → signals synchronization

  • DynamicSettings is limited to settings that genuinely require the editor to be re-created (changing the extension set); live-reconfigurable settings (table, list, link, code-highlight, …) no longer rebuild the editor.
  • All such settings are written onto extension config signals in one batched effect via the new useSynchronizeSettings() hook, with registerSettingsSynchronization() applying INITIAL_SETTINGS synchronously at editor build (no first-render flash). Optional rich-text-only extensions are resolved with the peer API; always-present ones assert their presence.
  • Promote WatchEditableExtension to @lexical/extension: editability is now a reactive signal, so "clickable links only when read-only" is a signals effect instead of useLexicalEditable. The SvelteKit example imports it directly.

e2e harness fail-fast + CI config

  • The harness previously waited for the editor's tree-view selector with no explicit timeout, so an uncaught error during editor build made every test in that mode hang to the full per-test timeout (×retries) with no signal. It now attaches a pageerror listener before navigating and races it against the editor-ready wait, so a broken load rejects in milliseconds with the actual error and a hint to check the extension configuration.
  • Reduce CI retries (4 → 2), pin the webServer URL, and enable fullyParallel.

Test plan

  • pnpm run tsc, pnpm run lint, and the unit suites (import-pipeline tests, full @lexical/html suite, new importer round-trip tests) all pass.
  • Targeted Playwright runs: plain-text shards 1/8 and 2/8 (0 failures), plain-text CharacterLimit (16 passed), rich-text CopyAndPaste (34 passed) and HorizontalRule (7 passed).
  • Fail-fast verified: an injected uncaught error during load makes initialize() reject in ~0.5s with the diagnostic message instead of timing out. Full e2e matrix runs in CI.
claude and others added 20 commits May 28, 2026 06:21
…mportExtension

Replace the legacy static importDOM machinery with the new
DOMImportExtension pipeline across every playground node and remove the
html config from the editor:

- Drop every `static importDOM` / `$config().importDOM` on playground
  nodes and export an equivalent `defineImportRule(...)` from each node
  file (Image, Tweet, YouTube, Figma-via-rendered-iframe, Equation,
  Mention, Poll, DateTime, PageBreak, Excalidraw, LayoutContainer,
  LayoutItem, CollapsibleContainer/Content/Title).
- Bundle the rules into a new `PlaygroundImportExtension` together with
  a playground-specific inline-style overlay that mirrors the legacy
  `buildHTMLConfig`'s span-style wrapping for font-size, color, and
  background-color.
- Move the legacy `<p>` -> `<div role="paragraph">` export fixup for
  ParagraphNode out of `html.export` and into a `PlaygroundDOMRenderExtension`
  using `DOMRenderExtension` overrides.
- Wire the per-package import extensions (Core, RichText, List, Link,
  Table, Code, HorizontalRule) and `ClipboardDOMImportExtension` into
  the AppExtension dependency graph, and delete `buildHTMLConfig.tsx`
  along with the `html: buildHTMLConfig()` field on the editor.
- Update unit tests that exercised the legacy `$generateNodesFromDOM`
  path to drive the new `$generateNodesFromDOMViaExtension` pipeline.

https://claude.ai/code/session_01QRiVwbqog5CGzuGmexcvjG
…port tests

Restore CollapsibleExtension as a dependency of the test extension and
move the structural assertions into the same editor.update() block as
the import. Inside the update, registered node transforms haven't run
yet, so we observe the structure the importer produced without the
container-unwrap transform interfering. The no-summary case now asserts
the synthesized empty Title is still present pre-transform.

https://claude.ai/code/session_01QRiVwbqog5CGzuGmexcvjG
- Add two tests that read the final state after transforms have run:
  a well-formed <details> survives intact, and a <details> with no
  <summary> ends up as a bare paragraph after the empty Title,
  one-child Container, and orphaned Content all unwrap in turn.
- Rephrase the PlaygroundInlineStyleRule ordering comment to
  explain the actual hazard (post-processing styles onto specialized
  node descendants) rather than the misleading "intercept" wording.
- Rename the layout-container negative case to describe the new
  pipeline ("does not import a div without ...") instead of the
  legacy DOMConversion "returns null" terminology.

https://claude.ai/code/session_01QRiVwbqog5CGzuGmexcvjG
…ceOf

Switch LayoutContainerNode import assertions from
\`expect(container).toBeInstanceOf(LayoutContainerNode)\` (plus a
trailing \`as LayoutContainerNode\` cast) to
\`assert(\$isLayoutContainerNode(container), '...')\`, which narrows the
type for the follow-up \`.getTemplateColumns()\` call and matches the
\$is-guard convention used by the other tests in this file.

https://claude.ai/code/session_01QRiVwbqog5CGzuGmexcvjG
…sion

Co-locate every playground node importer with the feature extension
that owns the node, and migrate the remaining React-shaped plugins to
extensions so each registered node has an extension carrying its
importer:

- Rename PollPlugin, EquationsPlugin, LayoutPlugin, MentionsPlugin,
  ExcalidrawPlugin to *Extension and turn each one into a
  defineExtension (with nodes, commands, transforms, and rule). Plugins
  whose work was only registerCommand-via-useEffect (Poll, Equations,
  Layout) are gone from Editor.tsx entirely; Mentions and Excalidraw
  keep their React UI but the node + rule live on the extension.
- Move every per-node import rule (Page-break, Tweet, YouTube, Image,
  Equation, DateTime, Layout, Mention, Excalidraw, Poll, Collapsible)
  out of the node files and into the matching feature extension's
  configExtension(DOMImportExtension, {rules: ...}). Node files no
  longer import defineImportRule.
- Simplify the rules with the new API:
  - Use regex captures (Tweet/YouTube) and selector predicates
    (DateTime's Google-Docs branch keys off a regex on data-rich-links)
    so $convertXxxElement helpers and explicit $next() escape hatches
    go away.
  - Rewrite the <details> importer on top of BlockSchema +
    ImportChildrenOpts.$onChild: BlockSchema wraps inline runs into
    paragraphs (no more hand-rolled flushPending), and $onChild siphons
    the CollapsibleTitleNode out so the rest stays in `bodyNodes` for
    the content split.
  - Inline `<details>.open` directly instead of round-tripping through
    the always-true undefined check.
- Shrink PlaygroundImportExtension to a single rule (the
  font-size/color/background-color span overlay that the legacy
  `buildHTMLConfig` carried); everything else now ships from the
  feature extensions.
- Switch the LayoutContainerNode test to use $isLayoutContainerNode +
  assert (instead of toBeInstanceOf + as cast) and depend on the new
  LayoutExtension; also point ComponentPicker, Toolbar, Editor, and
  ImageNode at the renamed paths.

https://claude.ai/code/session_01QRiVwbqog5CGzuGmexcvjG
The example install step in scripts/__tests__/integration/utils.mjs
used `npm install --no-save <tarball>...` to materialize each freshly
built monorepo tarball into the example. npm ignores pnpm.overrides,
so examples that stub heavy native deps that way (agent-example uses
`link:./stubs/empty` for onnxruntime-node and sharp via pnpm.overrides
to avoid downloading large native binaries) had those stubs bypassed
and the real packages were installed, with their postinstall scripts
making outbound HTTP calls that time out in sandboxed CI.

Switch buildExample to pnpm: layer the tarball paths onto the
example's existing pnpm.overrides (file:<abs-tarball-path>) and run
`pnpm install --ignore-workspace` + `pnpm run build`. The example's
package.json is restored from the original bytes in a finally block
so the working tree stays clean regardless of test outcome.

https://claude.ai/code/session_01QRiVwbqog5CGzuGmexcvjG
Switching the example install step to pnpm dropped transitive
monorepo deps out of the example's top-level node_modules (pnpm's
default isolated layout puts them under .pnpm/), so the
\`installed lexical X.Y.Z\` assertion in describeExample which checks
\`node_modules/<dep>/package.json\` for every entry in depsMap (e.g.
\`@lexical/internal\`) flipped to false across every example.

Pass \`--shamefully-hoist\` (== node-linker=hoisted) so pnpm
materializes a flat node_modules tree like npm does, restoring those
assertions while keeping the pnpm.overrides for the agent-example
onnxruntime / sharp stubs in effect.

https://claude.ai/code/session_01QRiVwbqog5CGzuGmexcvjG
Reverting the \`--shamefully-hoist\` workaround in favor of teaching
the assertion about pnpm's default isolated layout. The
\`installed lexical X.Y.Z\` check previously poked at
\`<exampleDir>/node_modules/<name>/package.json\` for every dep in
depsMap, which only happens to work when transitive deps are hoisted
to the top of node_modules (npm-style, or pnpm with
node-linker=hoisted).

Switch to globbing both shapes: direct/symlinked entries at
\`node_modules/<name>/\` and isolated copies under
\`node_modules/.pnpm/*/node_modules/<name>/\`. The first
package.json whose \`name\` + \`version\` matches the freshly built
monorepo version wins, so the test asserts what pnpm actually
produces instead of pinning a layout. Direct + transitive deps now
pass under both npm and pnpm without the shamefully-hoist crutch.

https://claude.ai/code/session_01QRiVwbqog5CGzuGmexcvjG
…errides

The example uses pnpm.overrides to replace onnxruntime-node and sharp
(transitive deps of @huggingface/transformers) with a tiny local stub
so npm-style postinstall scripts don't try to download the real
~hundreds-of-MB native binaries. npm honors neither pnpm.overrides
nor relative file: paths placed directly in top-level overrides (it
resolves them relative to whichever package brings the dep in, not
the project root).

Add the equivalent shape that npm does understand:

- declare onnxruntime-node and sharp as devDependencies pointing at
  the same `./stubs/empty` directory (npm resolves these from the
  project root just like any normal file: dependency),
- mirror them into top-level `overrides` via the `$onnxruntime-node`
  and `$sharp` self-references so every transitive use gets the same
  stub.

pnpm continues to use pnpm.overrides (which it already understood)
and is unaffected — verified empirically that both pnpm and npm
resolve qs/accepts-style transitive deps to the stub when the same
dual-field pattern is applied.

https://claude.ai/code/session_01QRiVwbqog5CGzuGmexcvjG
…ableExtension

The DOM import migration pulled in TableImportExtension, which depends on
TableExtension and registers the same observers (registerTablePlugin /
registerTableSelectionObserver) that the React `<TablePlugin>` was
already registering from Editor.tsx. The two stacks of observers raced
on every keystroke and selection change, swallowing typed text inside
table cells, hiding the cell action button, and dropping the
text-align format set via the table-selection-aware FORMAT_ELEMENT_COMMAND.

Drop the React `<TablePlugin>` (and its imports) and instead configure
the underlying TableExtension from buildExtensionFromSettings with the
same options the React plugin was forwarding (cell merge, background
color, horizontal scroll, nested tables). The extension now owns the
table observer wiring, so it's only registered once.

Also switch the playground's HTML action button from the legacy
$generateNodesFromDOM to $generateNodesFromDOMViaExtension so the
import side of the HTML round-trip exercises the same DOMImportExtension
rules the rest of the app uses — including the playground node rules
(DateTime, etc.) that no longer expose static importDOM after the
migration.

https://claude.ai/code/session_01QRiVwbqog5CGzuGmexcvjG
…l] Bug Fix: Restore legacy block-container import semantics

The DOMImportExtension rules dropped four behaviors that the legacy
$generateNodesFromDOM provided via wrapContinuousInlines /
$unwrapArtificialNodes / LineBreakNode.importDOM, breaking the
playground paste e2e suite once it migrated off the legacy importer:

- text-align on a block DOM parent (<td>, <ol>, <div>, …) is now
  propagated onto each block-level Lexical child that doesn't already
  carry a format. <th> is intentionally excluded (legacy `BLOCK_TAG_RE`
  omits it), so <th style="text-align: start"><p>…</p></th> still
  imports as a plain paragraph.
- A new ImportInBlockContext flag mirrors legacy
  `hasBlockAncestorLexicalNode`. ListItemRule, TableCellRule,
  HeadingRule, QuoteRule, and ParagraphRule set it; the new <div> rule
  consults it to either wrap children in an ArtificialNode (when
  nested) or run them through BlockSchema (at the root).
- $insertLineBreaksBetweenBlockArtificials reproduces the legacy
  "insert a <br> between adjacent transparent blocks" behavior for list
  items. The table cell variant ($packageCellChildren) instead lifts
  each artificial into its own sibling paragraph, matching the legacy
  <td>123<div>456</div></td> → <p>123</p><p>456</p> shape.
- The <br> import rule now defers to isOnlyChildInBlockNode /
  isLastChildInBlockNode (now exported from `lexical`) so stray Apple
  clipboard / trailing-<br> artifacts are dropped before they can
  survive as LineBreakNodes.
- $paragraphPackageRun collapses a sole-LineBreakNode rejected run to
  an empty paragraph, matching the legacy `selection.insertNodes`
  shortcut that clipboard pastes ending in <br> rely on.
This file is generated locally by the remote execution environment to
point Playwright at the installed chromium binary; it should not be
checked into the repo.
Missing space after comma in import statement.
…div>

The block-container import rule only matched <div>, so other unconverted
block-level elements (<section>, <article>, <header>, <figure>, ...) still
fell through to the inline-hoisting fallback and lost their block boundary:
sibling <section>s collapsed into one paragraph and any text-align was
dropped. The legacy $generateNodesFromDOM applied wrapContinuousInlines to
every isBlockDomNode (the BLOCK_TAG_RE set), not just <div>.

Replace the <div>-only rule with a sel.any() rule gated on isBlockDomNode
that defers (via $next()) for inline elements, so all block-level tags get
the same ArtificialNode / ImportInBlockContext / text-align treatment.
Higher-priority tag rules (<p>, <li>, <td>, headings, ...) still dispatch
first and never reach it.

Adds regression coverage for sibling <section>/<article> block separation
and text-align propagation on a non-<div> block.
…-table][lexical-playground] Refactor: drop CoreImportExtension from leaf importers

Leaf importer extensions (RichTextImportExtension, ListImportExtension,
LinkImportExtension, TableImportExtension, HorizontalRuleImportExtension,
plus every playground node/plugin extension) used to each re-declare
CoreImportExtension as a dependency. That was wasted code size — any
application using more than one of them only needs CoreImportExtension
once. Move the responsibility for adding it up to PlaygroundImportExtension
(which now also aggregates every per-package import extension and the
ClipboardDOMImportExtension paste handler), so App.tsx just depends on
PlaygroundImportExtension.

Unit tests that built bare editors from a single leaf importer now have
to add CoreImportExtension explicitly, matching the new contract.

Also drop two stray `default` exports on playground React components
(MentionsPlugin, ExcalidrawPlugin) and switch their importers to named
imports — playground extension modules should export their pieces by
name.
…or: drop ArtificialNode__DO_NOT_USE from the new importer

The original port of `wrapContinuousInlines` reused the legacy
`ArtificialNode__DO_NOT_USE` marker to distinguish "this block came
from a transparent block element" from "this block came from a real
`<p>`/`<h1>`/`<blockquote>`/…", but that distinction never really
mattered to the consumers — every container that cared (list items
and table cells) just wanted to know "this is a block boundary".

Lower `<div>`/`<section>`/`<article>`/`<header>`/… into a regular
`ParagraphNode` in `TransparentBlockRule` (running its children
through `BlockSchema` so each inline run is wrapped in its own
paragraph, then propagating the element's `text-align`). The
enclosing rules then operate on `ParagraphNode` directly:

- `ListItemRule` calls a new `$flattenParagraphChildren` helper that
  unwraps each paragraph and inserts a `LineBreakNode` between
  adjacent runs, reproducing the legacy
  `<li>1<div>2</div>3</li>` → `<li>1<br>2<br>3</li>` shape.
- `$packageCellChildren` in `TableImportExtension` now treats any
  non-inline child the same way — `<td>789<div>000</div></td>` still
  surfaces as `<p>789</p><p>000</p>` without the artificial-node
  detour.

`$insertLineBreaksBetweenBlockArtificials` and `ImportInBlockContext`
are gone with the marker — neither has any remaining caller, and the
overall change is a net deletion of ~160 lines. The legacy
`$generateNodesFromDOM` in `@lexical/html` still uses
`ArtificialNode__DO_NOT_USE` for its own pre-existing flow; that is
unchanged.
@vercel

vercel Bot commented May 29, 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 May 30, 2026 2:43am
lexical-playground Ready Ready Preview, Comment May 30, 2026 2:43am

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 May 29, 2026
claude added 2 commits May 29, 2026 15:17
…rters

Each playground node whose static importDOM was replaced by a
defineImportRule now has a DOMImportExtension round-trip test (create →
exportDOM → $generateNodesFromDOMViaExtension → assert): Image, Poll,
Equation, Mention, DateTime, PageBreak, Tweet, and YouTube.

Tests compose each feature extension with PlaygroundImportExtension so the
rule-dispatch ordering matches App.tsx (the per-node <span> rules must
out-prioritize the core inline-format <span> rule). ExcalidrawNode is left
to the e2e suite — importing its extension pulls the @excalidraw/excalidraw
UI bundle, which does not resolve under jsdom.
…e core inline-span rule

DateTimeExtension and MentionsExtension are listed in AppExtension *before*
PlaygroundImportExtension (which brings CoreImportExtension). Because
DOMImportExtension rules merged later win dispatch, the core inline-format
`<span>` rule was out-prioritizing the more specific `<span
data-lexical-datetime>` / `<span data-lexical-mention>` rules — so a
re-imported DateTime collapsed into bold/italic text and pasting over a
mention misbehaved (HTML "export/import" and Mentions e2e regressions).

Give each extension an explicit CoreImportExtension dependency so its rules
are always merged after the core rules, independent of where the app lists
the extension. (The feature extensions in PlaygroundRichTextExtension already
resolve after the import baseline, so this just makes the ordering robust for
the two that don't.)
@etrepum etrepum force-pushed the claude/quirky-noether-6agbj branch from 58b058f to 9741e3c Compare May 29, 2026 15:52
@etrepum etrepum changed the title [lexical-playground] Refactor: migrate playground HTML import to DOMImportExtension May 29, 2026
…mode

PlaygroundImportExtension (always in AppExtension) depended on
RichTextImportExtension, which pulls RichTextExtension. That put
RichTextExtension in the graph in *every* mode, so in plain-text mode it
conflicted with PlainTextExtension ("@lexical/plain-text conflicts with
@lexical/rich-text"), the editor failed to build, and every plain-text e2e
test timed out at initialize() waiting for the tree view — which, with CI
retries, ran for hours.

Split the importers so they mirror the node set per mode: the always-on
PlaygroundImportExtension now only carries the plain-text-safe baseline
(Core, Link, Clipboard, the playground inline-style overlay), and a new
PlaygroundRichTextImportExtension (rich-text/list/table/code/horizontal-rule)
is added to PlaygroundRichTextExtension. List and Table are therefore absent
in plain-text, so useSynchronizeSettings now resolves them with the optional
peerOutput (alongside CheckList/CodeHighlight).

Verified: the editor builds in plain-text, previously-failing plain-text specs
(CharacterLimit, …) pass, and rich-text paste is unaffected.
… load

The e2e harness waited for the editor's tree-view selector with no explicit
timeout, so when the app threw an uncaught error during the editor build
(e.g. a conflicting extension set), every test in that mode silently hung
until the long per-test timeout -- multiplied across retries and all tests,
that could stall a CI shard for hours with no useful signal.

Listen for "pageerror" before navigating and race it against the
editor-ready wait, so a broken load rejects in milliseconds with the actual
uncaught error instead of timing out.
@etrepum etrepum changed the title [lexical-playground][lexical-html] Migrate playground HTML import/export to the DOMImportExtension pipeline May 30, 2026
@etrepum etrepum added this pull request to the merge queue May 30, 2026
Merged via the queue into facebook:main with commit aba382a May 30, 2026
34 checks passed
@etrepum etrepum deleted the claude/quirky-noether-6agbj branch May 30, 2026 03:13

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

Reviewed by Navi (Tater Thoughts Bobblehead) on behalf of @potatowagon.

LGTM ✅ — Large refactor migrating playground HTML import/export to DOMImportExtension pipeline.

What this does: Migrates the playground's HTML import/export from the legacy inline approach to the new DOMImportExtension pipeline architecture. This is a substantial refactor (5493 lines across many files) that:

  1. Moves HTML import/export logic into proper extension-based architecture
  2. Exports WatchEditableExtension from @lexical/extension (was previously defined inline in examples)
  3. Removes the dead flaky: CI job (now if: false)
  4. Updates the agent-example and svelte-kit examples to use the exported WatchEditableExtension
  5. Adds onnxruntime/sharp stubs to agent-example (fixing npm/pnpm resolution issues)

What I checked:

  • ✅ The WatchEditableExtension export from @lexical/extension is a new public API surface — good for reuse, well-defined behavior (watches editor editable state via signal).
  • ✅ Example updates correctly import from the new location instead of defining inline.
  • ✅ CI infrastructure change (removing flaky job) is a clean no-op removal.
  • ✅ www compat: The new WatchEditableExtension export is additive. Existing imports are unaffected. The DOMImportExtension pipeline is the new recommended approach for custom HTML import — existing legacy approaches still work.
  • ✅ CI: All checks green.

Safe to land. This is foundational work for the extension-based architecture. The large diff is primarily playground/example restructuring, not library behavior changes.

@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. extended-tests Run extended e2e tests on a PR

4 participants