Related to #24531, but a distinct trigger — see the comparison below.
Repro
export function f(a) {
if (a) var x = true;
else var x = false;
return x ? "ok" : "fail";
}
minify("t.js", code, { compress: true, mangle: false }) →
export function f(a){if(a)var x=!0;else var x=!1;return`fail`}
f(true) returns "fail" instead of "ok" — the true from the first
branch is discarded. Notably the swapped order compiles correctly:
if (a) var x = false;
else var x = true; // → x ? "ok" : "fail" is preserved ✓
which suggests the analysis models the binding as
{undefined, <last declaration's initializer>}, dropping the earlier
declaration's initializer: {undefined, false} is uniformly falsy → folded;
{undefined, true} is mixed → not folded.
How this differs from #24531
Both look like the same underlying reaching-definitions defect (one reaching
value ignored at the use site), so a fix for #24531 may or may not cover
this — worth verifying against both repros.
Real-world impact
Scala.js routinely emits this pattern for branch-dependent locals. In our
production app it hit the CBOR validator of the
borer library, whose onBreak method
compiles to exactly this shape — compress deleted the valid-BREAK path and
made it unconditionally throw, so every CBOR message containing an
indefinite-length array failed to decode in vite production builds while
dev builds (unminified) worked.
Versions
oxc-minify 0.140.0 (standalone, latest at time of writing)
rolldown 1.1.4 and 1.1.5
vite 8.1.3 (default production minify); vite 7.x (esbuild minify) unaffected
Workaround
minify: { compress: false, mangle: true, removeWhitespace: true }
Related to #24531, but a distinct trigger — see the comparison below.
Repro
minify("t.js", code, { compress: true, mangle: false })→f(true)returns"fail"instead of"ok"— thetruefrom the firstbranch is discarded. Notably the swapped order compiles correctly:
which suggests the analysis models the binding as
{undefined, <last declaration's initializer>}, dropping the earlierdeclaration's initializer:
{undefined, false}is uniformly falsy → folded;{undefined, true}is mixed → not folded.How this differs from #24531
varto be captured by areturned arrow function; here the condition is folded in plain
straight-line code within the same function. The single-declaration shape
from minifier: var declared with a initializer conditionally is inlined unconditionally #24531 compiles correctly without the closure — this repro fails
even without one.
varis redeclared in both branches of an if/else; minifier: var declared with a initializer conditionally is inlined unconditionally #24531 has asingle declaration in one branch.
initializer), not the implicit
undefinedof a never-taken branch as inminifier: var declared with a initializer conditionally is inlined unconditionally #24531. Consequently the fold goes the other way: minifier: var declared with a initializer conditionally is inlined unconditionally #24531 folds to the
assigned value, this one folds to falsy even though one branch assigns
true.Both look like the same underlying reaching-definitions defect (one reaching
value ignored at the use site), so a fix for #24531 may or may not cover
this — worth verifying against both repros.
Real-world impact
Scala.js routinely emits this pattern for branch-dependent locals. In our
production app it hit the CBOR validator of the
borer library, whose
onBreakmethodcompiles to exactly this shape —
compressdeleted the valid-BREAK path andmade it unconditionally throw, so every CBOR message containing an
indefinite-length array failed to decode in vite production builds while
dev builds (unminified) worked.
Versions
oxc-minify0.140.0 (standalone, latest at time of writing)rolldown1.1.4 and 1.1.5vite8.1.3 (default production minify); vite 7.x (esbuild minify) unaffectedWorkaround
minify: { compress: false, mangle: true, removeWhitespace: true }