plans/issues/pr-3748.md
Executor instructions: Follow this plan step by step. Run every verification command and confirm the expected result before moving on. If anything in "STOP conditions" occurs, stop and report — do not improvise. When done, update this plan's row in
plans/issues/README.md.Drift check (run first):
gh pr view 3748 --json state,headRefOid,mergeStateStatusExpected:state: OPEN,headRefOid: 87d61a8e7d304290e7345963a6a27b993c241ae0. If closed/merged or head differs, STOP.
unstable_animateLayout)pr-3749.md). Closes PR #3747 (see plan pr-3747.md).42bfbe3ed, 2026-06-11animate-layout-batched-commits)This is the chosen iteration of the animateLayout rewrite (the engine driving
the real projection engine over plain DOM). Its headline improvement over both
the current implementation and the earlier attempt (#3747): builders created in
the same synchronous tick flush together — all snapshots are taken before any
updateDom runs, then additions/removals reconcile across builders and a
single shared root.didUpdate() fires. #3747 (per-builder pipelines, implicit
batching via the root's microtask-deduped didUpdate()) lets builder A's
updateDom run before builder B snapshots, corrupting B's origin measurements
on intersecting trees. Two of #3748's new HTML fixtures fail against the
previous implementation — the test-first gate is satisfied.
Additional signal (2026-06-11): the maintainer has already chosen this PR's
shared projection tree as the substrate for the vanilla-drag arc — see
plans/README.md plans 019–021, where plan 020 lists "PR #3748 merged" as a
hard dependency. Landing this PR unblocks that P1 track.
packages/motion-dom/src/layout/LayoutAnimationBuilder.ts — total rewrite),
4 new HTML fixtures (dev/html/public/animate-layout/:
parallel-calls-disjoint.html, parallel-calls-intersecting.html,
parallel-calls-shared-element.html, data-layout-true.html), plus the
fixture JSON. create-projection-node.ts and the public API untouched.let pendingBuilders: LayoutAnimationBuilder[] | undefined
// constructor:
if (!pendingBuilders) {
pendingBuilders = []
queueMicrotask(flushPendingBuilders)
}
pendingBuilders.push(this)
flushPendingBuilders() mounts all nodes in document order, snapshots all
builders, runs all updateDoms, reconciles additions before removals across
builders (AnimatePresence-equivalent semantics per layoutId), fires one
root.didUpdate(), finalizes via microtask.render.WeakMap<Element, IProjectionNode> reusing
visualElementStore entries.animate-layout-timing
React 18 Cypress failure mentioned in the body is pre-existing on clean main
(author bisected).fire-and-forget.html (+ fixture JSON
entry); #3748 handles fire-and-forget structurally (microtask flush runs
regardless of .then()) but has no regression fixture for it.| Purpose | Command | Expected |
|---|---|---|
| Checkout | gh pr checkout 3748 | branch animate-layout-batched-commits |
| Fetch 3747's fixture | git fetch origin animate-layout-shared-projection-tree && git checkout origin/animate-layout-shared-projection-tree -- dev/html/public/animate-layout/fire-and-forget.html | file appears |
| Build motion-dom only (dev loop) | cd packages/motion-dom && yarn build — see memory note: animate-layout fixtures run via dev/html Vite + cypress.html.json | exit 0 |
| HTML E2E | start dev/html Vite server, then cd packages/framer-motion && cypress run --config-file=cypress.html.json --spec "cypress/integration-html/animate-layout*" (check exact spec path in repo; the fixture list is driven by the animate-layout JSON) | all fixtures pass incl. fire-and-forget |
| Full CI | git push && gh pr checks 3748 --watch | green |
(Verify exact HTML-suite invocation against Makefile / package.json
test-html script before running — do not guess flags.)
In scope: porting fire-and-forget.html + its JSON entry from branch
animate-layout-shared-projection-tree; merging #3748; closing #3747.
Out of scope: any change to create-projection-node.ts; the public
animateLayout API; reworking batching semantics; plan 009's characterization
tests themselves (separate plan in plans/009-*.md).
plans/README.md:31 says plan 009 (LayoutAnimationBuilder characterization
tests) "should land before any refactor of LayoutAnimationBuilder.ts … it
exists to make such changes deliberate." This PR is exactly such a refactor.
The maintainer must either (a) execute plan 009 first, or (b) explicitly
accept the 28 HTML fixtures (24 existing + 4 new) as the characterization
suite. If the README row for this plan is not marked APPROVED (option b) and
plan 009 is not DONE, set the row to BLOCKED ("awaiting plan-009 sequencing
decision") and stop.
On the #3748 branch, copy dev/html/public/animate-layout/fire-and-forget.html
from branch animate-layout-shared-projection-tree (command above) and add
its entry to the animate-layout fixture JSON (same file the PR already edits —
match the existing entry format exactly). Memory traps for this dev loop:
suppressed build output, public/ bare imports, showError re-parses body —
see memory note "animate-layout dev loop" if fixtures behave oddly.
Verify: HTML E2E suite passes locally including the new fixture; commit
and push; gh pr checks 3748 green.
gh pr merge 3748 --squash. Then
gh pr close 3747 --comment "Superseded by #3748 (batched cross-builder commits; fire-and-forget fixture ported). See plans/issues/pr-3747.md."
Do NOT use gh pr edit.
Verify: 3748 MERGED; 3747 CLOSED.
motion-plus (/Users/matt/Sites/plus, packages/motion-remotion and
unstable_animateLayout consumers) depends on this engine. Add a line to the
README row noting the rewrite landed, so the next motion-plus sync re-runs its
integration tests against a motion-dom build containing this change.
fire-and-forget.html + JSON entry present on the branch; HTML suite green locallyplans/issues/README.md rows for pr-3748 AND pr-3747 updatedanimate-layout-timing React-18 case — report the fixture name
and output; do not patch fixtures to pass.pendingBuilders queue means all same-tick builders share
fate; anything that makes builder construction async (e.g. awaiting before
constructing) opts out of batching — document this if the public API grows.