plans/036-node-env-optional-chaining-dce.md
process.env?.NODE_ENVExecutor instructions: Follow this plan step by step. Run every verification command and confirm the expected result before moving to the next step. If anything in the "STOP conditions" section occurs, stop and report — do not improvise. When done, update the status row for this plan in
plans/README.md— unless a reviewer dispatched you and told you they maintain the index.Drift check (run first):
git diff --stat 42bfbe3ed..HEAD -- packages/motion-utils/src/errors.ts packages/framer-motion/scripts/check-bundle.js packages/framer-motion/rollup.config.mjsIf any in-scope file changed since this plan was written, compare the "Current state" excerpts against the live code before proceeding; on a mismatch, treat it as a STOP condition.
42bfbe3ed, 2026-06-11Commit 7a78368b3 (2026-01-05, "Fix ReferenceError: process is not defined
in native browser ESM") changed the dev-warning guard in motion-utils to use
optional chaining: process.env?.NODE_ENV. That token does not match the
exact-string pattern process.env.NODE_ENV used by @rollup/plugin-replace
in this repo's production builds, nor by older consumer bundlers (e.g.
webpack < 5.38 DefinePlugin). Consequence, verified in the built artifacts at
42bfbe3ed: the production UMD bundles (framer-motion/dist/framer-motion.js,
motion/dist/motion.js — what CDNs like unpkg serve) and every size bundle
contain a live runtime NODE_ENV check plus the full warning/invariant
implementations, formatErrorMessage, the motion.dev/troubleshooting URL
builder, and every call-site message string. Measured cost by patching the
minified bundle and re-running terser: ~0.27 kB gz on the main motion
bundle, ~0.2 kB gz on each of the animate/scroll/waapi bundles. The fix
keeps the typeof guard (the actual ReferenceError fix) while restoring the
exact replaceable token.
packages/motion-utils/src/errors.ts:9-27 (the whole relevant file):let warning: DevMessage = () => {}
let invariant: DevMessage = () => {}
if (
typeof process !== "undefined" &&
process.env?.NODE_ENV !== "production"
) {
warning = (check, message, errorCode) => { ... console.warn(formatErrorMessage(message, errorCode)) ... }
invariant = (check, message, errorCode) => { ... throw new Error(formatErrorMessage(message, errorCode)) ... }
}
process.env?. (optional-chaining) occurrence in any
package's src/ — all other env checks use the plain replaceable form
(verify with the grep in Step 1).packages/framer-motion/rollup.config.mjs:17-28 — replaceSettings(env)
configures @rollup/plugin-replace with key "process.env.NODE_ENV". The
production UMD (createUmd, line 78) and the size bundles
(rollup.size.config.mjs:8) use replaceSettings("production").grep -c "process.env" packages/framer-motion/dist/framer-motion.js → 1
(and it is process.env?.NODE_ENV).packages/framer-motion/scripts/check-bundle.js — post-build assertion
script (yarn build runs it last). It already enforces several
bundle-integrity invariants by reading dist files and throwing; new checks
are appended at the end of the file following the existing pattern (each
check is a block comment explaining the regression + a readFileSync +
conditional throw).packages/motion-utils/src/__tests__/errors.test.ts exists — the runtime
behavior gate for warning/invariant (jest runs with NODE_ENV=test, so
the dev branch is active under test).| Purpose | Command (from repo root) | Expected on success |
|---|---|---|
| Build all | yarn build | exit 0 (includes check-bundle.js) |
| motion-utils tests | cd packages/motion-utils && yarn test | all pass |
| Size bundles | yarn measure | exit 0 if plan 035 landed; otherwise read the table |
| Leak grep (prod UMD) | grep -c "process.env" packages/framer-motion/dist/framer-motion.js | 0 after fix |
| Leak grep (motion UMD) | grep -c "process.env" packages/motion/dist/motion.js | 0 after fix |
In scope (the only files you should modify):
packages/motion-utils/src/errors.tspackages/motion-utils/src/__tests__/errors.test.ts (extend)packages/framer-motion/scripts/check-bundle.js (add regression assertion)packages/framer-motion/package.json + packages/motion-dom/package.json
bundlesize values — ONLY in Step 5, only if plan 035 landedOut of scope (do NOT touch, even though they look related):
packages/motion-utils/src/format-error-message.ts — it becomes dead code
in production builds automatically once DCE works; do not delete or inline it.rollup.config.mjs / rollup.size.config.mjs — the replace config is
correct; the source token was the problem. (A possible dev-UMD follow-up is
noted in Maintenance, not here.)process.env.NODE_ENV checks across packages — already in the
replaceable form.advisor/036-node-env-dceFix NODE_ENV check defeating dead-code elimination of dev warnings.grep -rn "process.env?." packages/motion-utils/src packages/motion-dom/src packages/framer-motion/src --include="*.ts" --include="*.tsx" | grep -v __tests__
Verify: exactly one hit — packages/motion-utils/src/errors.ts:14. If
there are more hits, fix them all with the same pattern and list them in your
report.
errors.tsReplace the condition at errors.ts:12-15 with:
if (
typeof process !== "undefined" &&
typeof process.env !== "undefined" &&
process.env.NODE_ENV !== "production"
) {
Why this exact shape: the third clause is byte-for-byte the token
@rollup/plugin-replace and consumer define-plugins match, so production
builds fold it to "production" !== "production" → false, and terser then
drops the whole block (the typeof clauses are side-effect-free). The first
two clauses preserve the runtime safety the Jan 2026 commit added for
unbundled native-ESM browsers. Known, accepted semantic delta: an exotic
runtime where process exists but process.env is undefined now resolves to
production (warnings off) instead of dev — the safer default.
Verify: cd packages/motion-utils && yarn test → all pass (NODE_ENV=test
keeps the dev branch active under jest).
check-bundle.jsAppend to packages/framer-motion/scripts/check-bundle.js, following the
file's existing comment-block + throw pattern:
/**
* Verify the production UMD bundle contains no `process.env` reference.
* `process.env?.NODE_ENV` (optional chaining) is invisible to
* @rollup/plugin-replace's exact `process.env.NODE_ENV` pattern, so dev-only
* warning machinery and message strings ship to production (plan 036).
*/
const prodUmd = readFileSync(path.join(dist, "framer-motion.js"), "utf8")
if (prodUmd.includes("process.env")) {
throw new Error(
"Production UMD bundle (dist/framer-motion.js) references process.env — " +
"an env check is not in the exact replaceable `process.env.NODE_ENV` form"
)
}
Verify: git stash the errors.ts change, run yarn build →
framer-motion's build MUST FAIL with the new error (proves the assertion
catches the bug). git stash pop, run yarn build again → exit 0.
(Note: motion-utils must be rebuilt for framer-motion to see the change —
yarn build from the repo root handles the ordering.)
After the full yarn build from Step 3:
Verify:
grep -c "process.env" packages/framer-motion/dist/framer-motion.js → 0grep -c "troubleshooting" packages/framer-motion/dist/framer-motion.js → 0
(the motion.dev/troubleshooting URL builder is DCE'd)grep -c "process.env" packages/motion/dist/motion.js → 0yarn measure (or turbo run measure --force):
dist/size-rollup-motion.js shrinks vs the plan-035 baseline by roughly
0.2–0.3 kB gz (expected ~38.3 kB gz if measured at 42bfbe3ed + this fix).Re-run yarn measure, and lower every bundlesize maxSize that improved
to the new actual × 1.01 rounded up to the nearest 0.05 kB (same rule as plan
035 Step 2).
Verify: node dev/inc/bundlesize.mjs → exit 0, all ✅.
packages/motion-utils/src/__tests__/errors.test.ts with a case
asserting warning(false, "msg") calls console.warn and
invariant(false, "msg") throws under the test env — locking the runtime
dev behavior the new guard must preserve. Model on the existing cases in
that file.check-bundle.js assertion (Step 3),
including its deliberate stash/fail/pop exercise — that is the test that
the bug cannot return.Machine-checkable. ALL must hold:
grep -rn "process.env?." packages/*/src --include="*.ts" (excluding __tests__) → no hitsyarn build exits 0 (check-bundle assertion passing)grep -c "process.env" packages/framer-motion/dist/framer-motion.js → 0cd packages/motion-utils && yarn test → all pass, including the new casesnode dev/inc/bundlesize.mjs exits 0 with ratcheted budgetsgit status)plans/README.md status row updatedStop and report back (do not improvise) if:
grep "process.env" packages/framer-motion/dist/framer-motion.js
still matches — something else emits an unreplaceable env check; find it
via Step 1's grep before patching further, and report if it's in generated
code rather than src/.errors.ts's guard must keep the literal
token process.env.NODE_ENV intact — the check-bundle assertion enforces
this, but the reason should be in the review conversation, not just the
CI failure.<script> (no bundler, no process),
warnings are off — true since 7a78368b3, because the runtime typeof process guard fails there. If dev-UMD warnings should work, the dev
rollup config would need to replace the entire guard expression (or define
process.env.NODE_ENV in the dev build too). Deliberately not done here.