Fix LazyMotion not working with motion/react-m subpath (#3091)#3710
Fix LazyMotion not working with motion/react-m subpath (#3091)#3710mattgperry wants to merge 1 commit intomainfrom
Conversation
…ndles The CJS bundles for the main `framer-motion` entry and the `/m` subpath were rolled up in separate invocations, so each emitted its own copy of `createContext()` for `LazyContext`, `MotionContext`, etc. When users combined `<LazyMotion>` from `motion/react` with `<m.div>` from `motion/react-m`, the LazyMotion provider and the m component subscribed to two different React Context instances, so the renderer and feature definitions never reached the m component. Bundling `lib/m.js` alongside `lib/index.js` in a single rollup CJS invocation lets rollup share the contexts via a common chunk, so both entry points reference the same context instances. ESM was already fine via `preserveModules`, so only the CJS structure changes. Adds a regression check in `check-bundle.js` and a Cypress smoke test for `<LazyMotion>` + `<m.div>` across the `motion/react` and `motion/react-m` subpaths. Fixes #3091
Greptile SummaryThis PR fixes a long-standing silent failure (#3091) where
Confidence Score: 5/5Safe to merge — the change is a narrow Rollup config adjustment that co-bundles lib/m.js with lib/index.js, and includes a build-time guard and integration tests to validate the fix. The build change is minimal and well-contained: one line added to the cjs input array and one standalone cjsM config removed. The shared-chunk mechanism is standard Rollup behavior and doesn't alter the public API or runtime behavior of either entry point. The regression guard in check-bundle.js ensures the fix can't silently regress. The only notable issue is that the Cypress test's animationFailed assertion is vacuously true at the 500ms check point, but the animationComplete assertion provides genuine regression coverage for the bug. No files require special attention; the one nit is in the Cypress test timing logic. Important Files Changed
Reviews (1): Last reviewed commit: "Share React contexts between framer-moti..." | Re-trigger Greptile |
| expect($element.dataset.animationFailed).to.not.equal("true") | ||
| expect($element.dataset.animationComplete).to.equal("true") | ||
| expect(getComputedStyle($element).opacity).to.equal("1") |
There was a problem hiding this comment.
animationFailed assertion is vacuously true at the assertion point
The data-animation-failed attribute is set by the React component's setTimeout at 1000 ms, but Cypress checks at .wait(500) — before the timeout can ever fire. The assertion expect($element.dataset.animationFailed).to.not.equal("true") will therefore always pass, even if the animation silently fails to start. The real regression protection comes entirely from the animationComplete check on the next line. If you want the failure-path assertion to be meaningful, either increase .wait() beyond 1000 ms, or drop the animationFailed check since it adds no coverage here.
Summary
Fixes #3091 —
<LazyMotion>frommotion/reactcouldn't load features for<m.div>imported frommotion/react-m. When users wrapped m components from the/msubpath in a<LazyMotion>provider, the m components silently rendered without animations, gestures, or layout features.Cause
The CJS bundles for the main
framer-motionentry and theframer-motion/msubpath were emitted in separate rollup invocations, with a comment that intentionally avoided sharing chunks. Each CJS bundle therefore made its owncreateContext()calls forLazyContext,MotionContext,MotionConfigContext, etc.createContext()returns a unique object each time it is called, so the two bundles ended up with two distinct React Context instances. The<LazyMotion>frommotion/reactpublished the renderer and feature definitions on its bundle'sLazyContext; the<m.div>frommotion/react-mread from a differentLazyContextwhose value was the default{ strict: false }(no renderer). The m component never built aVisualElement, so nothing animated.ESM consumers were unaffected because the ESM build uses
preserveModules: true, so the shareddist/es/context/LazyContext.mjsresolves to a single module.Fix
Bundle
lib/m.jstogether withlib/index.jsin the same CJS rollup invocation. Rollup hoists shared modules (the contexts,createMotionComponent,useVisualElement, etc.) into a common chunk that both entry points require, soframer-motion/dist/cjs/index.jsandframer-motion/dist/cjs/m.jsnow reference the same context instances. ESM and the existing per-entry CJS bundles fordom,dom-mini,mini, anddebugare unchanged.Regression coverage
packages/framer-motion/scripts/check-bundle.jsnow fails the build ifdist/cjs/m.jsre-introduces its owncreateContext({ strict: false })call.dev/react/src/tests/lazy-motion-react-m-subpath.tsx+packages/framer-motion/cypress/integration/lazy-motion-react-m-subpath.tsexercise<LazyMotion features={domAnimation}>wrapping<m.div>frommotion/react-m.Test plan
yarn buildsucceeds (check-bundle gate passes).yarn test— 793 unit tests pass.lazy-motion-react-m-subpathpasses on React 18.lazy-motion-react-m-subpathpasses on React 19.🤖 Generated with Claude Code