diff --git a/CHANGELOG.md b/CHANGELOG.md index 959c8b1a242..6afc3a72974 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,38 @@ ## Unreleased +* Add the `--drop-labels=` option ([#2398](https://github.com/evanw/esbuild/issues/2398)) + + If you want to conditionally disable some development-only code and have it not be present in the final production bundle, right now the most straightforward way of doing this is to use the `--define:` flag along with a specially-named global variable. For example, consider the following code: + + ```js + function main() { + DEV && doAnExpensiveCheck() + } + ``` + + You can build this for development and production like this: + + * Development: `esbuild --define:DEV=true` + * Production: `esbuild --define:DEV=false` + + One drawback of this approach is that the resulting code crashes if you don't provide a value for `DEV` with `--define:`. In practice this isn't that big of a problem, and there are also various ways to work around this. + + However, another approach that avoids this drawback is to use JavaScript label statements instead. That's what the `--drop-labels=` flag implements. For example, consider the following code: + + ```js + function main() { + DEV: doAnExpensiveCheck() + } + ``` + + With this release, you can now build this for development and production like this: + + * Development: `esbuild` + * Production: `esbuild --drop-labels=DEV` + + This means that code containing optional development-only checks can now be written such that it's safe to run without any additional configuration. The `--drop-labels=` flag takes comma-separated list of multiple label names to drop. + * Avoid causing `unhandledRejection` during shutdown ([#3219](https://github.com/evanw/esbuild/issues/3219)) All pending esbuild JavaScript API calls are supposed to fail if esbuild's underlying child process is unexpectedly terminated. This can happen if `SIGINT` is sent to the parent `node` process with Ctrl+C, for example. Previously doing this could also cause an unhandled promise rejection when esbuild attempted to communicate this failure to its own child process that no longer exists. This release now swallows this communication failure, which should prevent this internal unhandled promise rejection. This change means that you can now use esbuild's JavaScript API with a custom `SIGINT` handler that extends the lifetime of the `node` process without esbuild's internals causing an early exit due to an unhandled promise rejection. diff --git a/cmd/esbuild/main.go b/cmd/esbuild/main.go index 3879b83f7e1..d00c928c015 100644 --- a/cmd/esbuild/main.go +++ b/cmd/esbuild/main.go @@ -68,6 +68,7 @@ var helpText = func(colors logger.Colors) string { (default "[name]-[hash]") --color=... Force use of color terminal escapes (true | false) --drop:... Remove certain constructs (console | debugger) + --drop-labels=... Remove labeled statements with these label names --entry-names=... Path template to use for entry point output paths (default "[dir]/[name]", can also use "[hash]") --footer:T=... Text to be appended to each output file of type T diff --git a/internal/bundler_tests/bundler_dce_test.go b/internal/bundler_tests/bundler_dce_test.go index d0487ce5593..b20ab9c558b 100644 --- a/internal/bundler_tests/bundler_dce_test.go +++ b/internal/bundler_tests/bundler_dce_test.go @@ -4447,3 +4447,66 @@ func TestDCEOfExprAfterKeepNamesIssue3195(t *testing.T) { }, }) } + +func TestDropLabels(t *testing.T) { + dce_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + keep_1: require('foo1') + DROP_1: require('bar1') + exports.bar = function() { + if (x) DROP_2: require('foo2') + if (y) keep_2: require('bar2') + } + `, + }, + entryPaths: []string{"/entry.js"}, + options: config.Options{ + Mode: config.ModeBundle, + DropLabels: []string{ + "DROP_1", + "DROP_2", + }, + ExternalSettings: config.ExternalSettings{ + PreResolve: config.ExternalMatchers{ + Exact: map[string]bool{ + "foo1": true, + "bar2": true, + }, + }, + }, + AbsOutputFile: "/out.js", + OutputFormat: config.FormatCommonJS, + }, + }) +} + +func TestRemoveCodeAfterLabelWithReturn(t *testing.T) { + dce_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + function earlyReturn() { + // This comes up when doing conditional compilation with "DropLabels" + keep: { + onlyWithKeep() + return + } + onlyWithoutKeep() + } + function loop() { + if (foo()) { + keep: { + bar() + return; + } + } + } + `, + }, + entryPaths: []string{"/entry.js"}, + options: config.Options{ + AbsOutputFile: "/out.js", + MinifySyntax: true, + }, + }) +} diff --git a/internal/bundler_tests/bundler_default_test.go b/internal/bundler_tests/bundler_default_test.go index 0d676acb578..20174e5729b 100644 --- a/internal/bundler_tests/bundler_default_test.go +++ b/internal/bundler_tests/bundler_default_test.go @@ -2797,38 +2797,34 @@ func TestMinifyNestedLabelsNoBundle(t *testing.T) { default_suite.expectBundled(t, bundled{ files: map[string]string{ "/entry.js": ` - L001:{L002:{L003:{L004:{L005:{L006:{L007:{L008:{L009:{L010:{L011:{L012:{L013:{L014:{L015:{L016:{nl('\n') - L017:{L018:{L019:{L020:{L021:{L022:{L023:{L024:{L025:{L026:{L027:{L028:{L029:{L030:{L031:{L032:{nl('\n') - L033:{L034:{L035:{L036:{L037:{L038:{L039:{L040:{L041:{L042:{L043:{L044:{L045:{L046:{L047:{L048:{nl('\n') - L049:{L050:{L051:{L052:{L053:{L054:{L055:{L056:{L057:{L058:{L059:{L060:{L061:{L062:{L063:{L064:{nl('\n') - L065:{L066:{L067:{L068:{L069:{L070:{L071:{L072:{L073:{L074:{L075:{L076:{L077:{L078:{L079:{L080:{nl('\n') - L081:{L082:{L083:{L084:{L085:{L086:{L087:{L088:{L089:{L090:{L091:{L092:{L093:{L094:{L095:{L096:{nl('\n') - L097:{L098:{L099:{L100:{L101:{L102:{L103:{L104:{L105:{L106:{L107:{L108:{L109:{L110:{L111:{L112:{nl('\n') - L113:{L114:{L115:{L116:{L117:{L118:{L119:{L120:{L121:{L122:{L123:{L124:{L125:{L126:{L127:{L128:{nl('\n') - L129:{L130:{L131:{L132:{L133:{L134:{L135:{L136:{L137:{L138:{L139:{L140:{L141:{L142:{L143:{L144:{nl('\n') - L145:{L146:{L147:{L148:{L149:{L150:{L151:{L152:{L153:{L154:{L155:{L156:{L157:{L158:{L159:{L160:{nl('\n') - L161:{L162:{L163:{L164:{L165:{L166:{L167:{L168:{L169:{L170:{L171:{L172:{L173:{L174:{L175:{L176:{nl('\n') - L177:{L178:{L179:{L180:{L181:{L182:{L183:{L184:{L185:{L186:{L187:{L188:{L189:{L190:{L191:{L192:{nl('\n') - L193:{L194:{L195:{L196:{L197:{L198:{L199:{L200:{L201:{L202:{L203:{L204:{L205:{L206:{L207:{L208:{nl('\n') - L209:{L210:{L211:{L212:{L213:{L214:{L215:{L216:{L217:{L218:{L219:{L220:{L221:{L222:{L223:{L224:{nl('\n') - L225:{L226:{L227:{L228:{L229:{L230:{L231:{L232:{L233:{L234:{L235:{L236:{L237:{L238:{L239:{L240:{nl('\n') - L241:{L242:{L243:{L244:{L245:{L246:{L247:{L248:{L249:{L250:{L251:{L252:{L253:{L254:{L255:{L256:{nl('\n') - L257:{L258:{L259:{L260:{L261:{L262:{L263:{L264:{L265:{L266:{L267:{L268:{L269:{L270:{L271:{L272:{nl('\n') - L273:{L274:{L275:{L276:{L277:{L278:{L279:{L280:{L281:{L282:{L283:{L284:{L285:{L286:{L287:{L288:{nl('\n') - L289:{L290:{L291:{L292:{L293:{L294:{L295:{L296:{L297:{L298:{L299:{L300:{L301:{L302:{L303:{L304:{nl('\n') - L305:{L306:{L307:{L308:{L309:{L310:{L311:{L312:{L313:{L314:{L315:{L316:{L317:{L318:{L319:{L320:{nl('\n') - L321:{L322:{L323:{L324:{L325:{L326:{L327:{L328:{L329:{L330:{L331:{L332:{L333:{}}}}}}}}}}}}}}}}}}nl('\n') - }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}nl('\n') - }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}nl('\n') - }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}nl('\n') - }}}}}}}}}}}}}}}}}}}}}}}}}}} + L001:L002:L003:L004:L005:L006:L007:L008:L009:L010:L011:L012:L013:L014:L015:L016:{nl(` + "`\n`" + `) + L017:L018:L019:L020:L021:L022:L023:L024:L025:L026:L027:L028:L029:L030:L031:L032:{nl(` + "`\n`" + `) + L033:L034:L035:L036:L037:L038:L039:L040:L041:L042:L043:L044:L045:L046:L047:L048:{nl(` + "`\n`" + `) + L049:L050:L051:L052:L053:L054:L055:L056:L057:L058:L059:L060:L061:L062:L063:L064:{nl(` + "`\n`" + `) + L065:L066:L067:L068:L069:L070:L071:L072:L073:L074:L075:L076:L077:L078:L079:L080:{nl(` + "`\n`" + `) + L081:L082:L083:L084:L085:L086:L087:L088:L089:L090:L091:L092:L093:L094:L095:L096:{nl(` + "`\n`" + `) + L097:L098:L099:L100:L101:L102:L103:L104:L105:L106:L107:L108:L109:L110:L111:L112:{nl(` + "`\n`" + `) + L113:L114:L115:L116:L117:L118:L119:L120:L121:L122:L123:L124:L125:L126:L127:L128:{nl(` + "`\n`" + `) + L129:L130:L131:L132:L133:L134:L135:L136:L137:L138:L139:L140:L141:L142:L143:L144:{nl(` + "`\n`" + `) + L145:L146:L147:L148:L149:L150:L151:L152:L153:L154:L155:L156:L157:L158:L159:L160:{nl(` + "`\n`" + `) + L161:L162:L163:L164:L165:L166:L167:L168:L169:L170:L171:L172:L173:L174:L175:L176:{nl(` + "`\n`" + `) + L177:L178:L179:L180:L181:L182:L183:L184:L185:L186:L187:L188:L189:L190:L191:L192:{nl(` + "`\n`" + `) + L193:L194:L195:L196:L197:L198:L199:L200:L201:L202:L203:L204:L205:L206:L207:L208:{nl(` + "`\n`" + `) + L209:L210:L211:L212:L213:L214:L215:L216:L217:L218:L219:L220:L221:L222:L223:L224:{nl(` + "`\n`" + `) + L225:L226:L227:L228:L229:L230:L231:L232:L233:L234:L235:L236:L237:L238:L239:L240:{nl(` + "`\n`" + `) + L241:L242:L243:L244:L245:L246:L247:L248:L249:L250:L251:L252:L253:L254:L255:L256:{nl(` + "`\n`" + `) + L257:L258:L259:L260:L261:L262:L263:L264:L265:L266:L267:L268:L269:L270:L271:L272:{nl(` + "`\n`" + `) + L273:L274:L275:L276:L277:L278:L279:L280:L281:L282:L283:L284:L285:L286:L287:L288:{nl(` + "`\n`" + `) + L289:L290:L291:L292:L293:L294:L295:L296:L297:L298:L299:L300:L301:L302:L303:L304:{nl(` + "`\n`" + `) + L305:L306:L307:L308:L309:L310:L311:L312:L313:L314:L315:L316:L317:L318:L319:L320:{nl(` + "`\n`" + `) + L321:L322:L323:L324:L325:L326:L327:L328:L329:L330:L331:L332:L333:{}}}}}}}}}}}}}}}}}}nl(` + "`\n`" + `) + }}} `, }, entryPaths: []string{"/entry.js"}, options: config.Options{ MinifyWhitespace: true, MinifyIdentifiers: true, - MinifySyntax: true, AbsOutputFile: "/out.js", }, }) diff --git a/internal/bundler_tests/snapshots/snapshots_dce.txt b/internal/bundler_tests/snapshots/snapshots_dce.txt index 700dc55fdd4..bcdbd8fe29c 100644 --- a/internal/bundler_tests/snapshots/snapshots_dce.txt +++ b/internal/bundler_tests/snapshots/snapshots_dce.txt @@ -778,9 +778,7 @@ function testStmts() { var f; if (!x) var g; - var h; - x: - var i; + var h, i; } testReturn(); testThrow(); @@ -805,6 +803,20 @@ var keepMe4 = keepMe3(); var keepMe5 = pure(); var keepMe6 = some.fn(); +================================================================================ +TestDropLabels +---------- /out.js ---------- +// entry.js +keep_1: + require("foo1"); +exports.bar = function() { + if (x) + ; + if (y) + keep_2: + require("bar2"); +}; + ================================================================================ TestFileLoaderRemoveUnused ---------- /out.js ---------- @@ -2347,6 +2359,19 @@ TestPureCallsWithSpread [...args]; [...args]; +================================================================================ +TestRemoveCodeAfterLabelWithReturn +---------- /out.js ---------- +function earlyReturn() { + onlyWithKeep(); +} +function loop() { + if (foo()) { + bar(); + return; + } +} + ================================================================================ TestRemoveTrailingReturn ---------- /out.js ---------- diff --git a/internal/bundler_tests/snapshots/snapshots_default.txt b/internal/bundler_tests/snapshots/snapshots_default.txt index c3f0e7fee02..6332c6224d9 100644 --- a/internal/bundler_tests/snapshots/snapshots_default.txt +++ b/internal/bundler_tests/snapshots/snapshots_default.txt @@ -4536,31 +4536,28 @@ var i=r((t,e)=>{e.exports=123});var s=i();console.log(s,"no identifier in this f ================================================================================ TestMinifyNestedLabelsNoBundle ---------- /out.js ---------- -n:l:a:b:c:d:e:f:g:h:i:j:k:m:o:p:{nl(` -`);q:r:s:t:u:v:w:x:y:z:A:{B:C:D:E:F:{nl(` +l:n:a:b:c:d:e:f:g:h:i:j:k:m:o:p:{nl(` +`);q:r:s:t:u:v:w:x:y:z:A:B:C:D:E:F:{nl(` `);G:H:I:J:K:L:M:N:O:P:Q:R:S:T:U:V:{nl(` -`);W:X:Y:Z:_:$:nn:ln:an:bn:cn:dn:en:fn:gn:hn:{nl(` -`);jn:kn:mn:on:pn:qn:rn:sn:tn:un:vn:wn:xn:yn:zn:An:{nl(` -`);Bn:Cn:Dn:En:Fn:Gn:Hn:In:Jn:Kn:Ln:Mn:Nn:On:Pn:Qn:{nl(` -`);Rn:Sn:Tn:Un:Vn:Wn:Xn:Yn:Zn:_n:$n:nl:ll:al:bl:cl:{nl(` -`);dl:el:fl:gl:hl:il:jl:kl:ml:ol:pl:{ql:rl:sl:tl:ul:{nl(` -`);vl:wl:xl:yl:zl:Al:Bl:Cl:Dl:El:Fl:Gl:Hl:Il:Jl:Kl:{nl(` -`);Ll:Ml:Nl:Ol:Pl:Ql:Rl:Sl:Tl:Ul:Vl:Wl:Xl:Yl:Zl:_l:{nl(` -`);$l:na:la:aa:ba:ca:da:ea:fa:ga:ha:ia:ja:ka:ma:oa:{nl(` +`);W:X:Y:Z:_:$:ll:nl:al:bl:cl:dl:el:fl:gl:hl:{nl(` +`);il:jl:kl:ml:ol:pl:ql:rl:sl:tl:ul:vl:wl:xl:yl:zl:{nl(` +`);Al:Bl:Cl:Dl:El:Fl:Gl:Hl:Il:Jl:Kl:Ll:Ml:Nl:Ol:Pl:{nl(` +`);Ql:Rl:Sl:Tl:Ul:Vl:Wl:Xl:Yl:Zl:_l:$l:ln:nn:an:bn:{nl(` +`);cn:dn:en:fn:gn:hn:jn:kn:mn:on:pn:qn:rn:sn:tn:un:{nl(` +`);vn:wn:xn:yn:zn:An:Bn:Cn:Dn:En:Fn:Gn:Hn:In:Jn:Kn:{nl(` +`);Ln:Mn:Nn:On:Pn:Qn:Rn:Sn:Tn:Un:Vn:Wn:Xn:Yn:Zn:_n:{nl(` +`);$n:la:na:aa:ba:ca:da:ea:fa:ga:ha:ia:ja:ka:ma:oa:{nl(` `);pa:qa:ra:sa:ta:ua:va:wa:xa:ya:za:Aa:Ba:Ca:Da:Ea:{nl(` `);Fa:Ga:Ha:Ia:Ja:Ka:La:Ma:Na:Oa:Pa:Qa:Ra:Sa:Ta:Ua:{nl(` -`);Va:Wa:Xa:Ya:Za:_a:$a:nb:lb:ab:bb:{cb:db:eb:fb:gb:{nl(` +`);Va:Wa:Xa:Ya:Za:_a:$a:lb:nb:ab:bb:cb:db:eb:fb:gb:{nl(` `);hb:ib:jb:kb:mb:ob:pb:qb:rb:sb:tb:ub:vb:wb:xb:yb:{nl(` `);zb:Ab:Bb:Cb:Db:Eb:Fb:Gb:Hb:Ib:Jb:Kb:Lb:Mb:Nb:Ob:{nl(` -`);Pb:Qb:Rb:Sb:Tb:Ub:Vb:Wb:Xb:Yb:Zb:_b:$b:nc:lc:ac:{nl(` +`);Pb:Qb:Rb:Sb:Tb:Ub:Vb:Wb:Xb:Yb:Zb:_b:$b:lc:nc:ac:{nl(` `);bc:cc:dc:ec:fc:gc:hc:ic:jc:kc:mc:oc:pc:qc:rc:sc:{nl(` `);tc:uc:vc:wc:xc:yc:zc:Ac:Bc:Cc:Dc:Ec:Fc:Gc:Hc:Ic:{nl(` -`);Jc:Kc:Lc:Mc:Nc:Oc:Pc:Qc:Rc:Sc:Tc:{Uc:Vc:Wc:Xc:Yc:{nl(` -`);Zc:_c:$c:nd:ld:ad:bd:cd:dd:ed:fd:gd:hd:;}nl(` -`)}}}}}}}nl(` -`)}}}}}}}nl(` -`)}}}}}}}nl(` -`)}} +`);Jc:Kc:Lc:Mc:Nc:Oc:Pc:Qc:Rc:Sc:Tc:Uc:Vc:Wc:Xc:Yc:{nl(` +`);Zc:_c:$c:ld:nd:ad:bd:cd:dd:ed:fd:gd:hd:{}}}}}}}}}}}}}}}}}}nl(` +`)}}} ================================================================================ TestMinifyPrivateIdentifiersNoBundle diff --git a/internal/config/config.go b/internal/config/config.go index ed195f056bf..c90bc72f15a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -383,6 +383,7 @@ type Options struct { // unsupported feature sets above. It's used for error messages. OriginalTargetEnv string + DropLabels []string ExtensionOrder []string MainFields []string Conditions []string diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 2c4d8f32224..2356dbe240a 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -48,6 +48,7 @@ type parser struct { injectedDefineSymbols []js_ast.Ref injectedSymbolSources map[js_ast.Ref]injectedSymbolSource injectedDotNames map[string][]injectedDotName + dropLabelsMap map[string]struct{} exprComments map[logger.Loc][]string mangledProps map[string]js_ast.Ref reservedProps map[string]bool @@ -434,6 +435,7 @@ type Options struct { tsAlwaysStrict *config.TSAlwaysStrict mangleProps *regexp.Regexp reserveProps *regexp.Regexp + dropLabels []string // This pointer will always be different for each build but the contents // shouldn't ever behave different semantically. We ignore this field for the @@ -490,6 +492,7 @@ func OptionsFromConfig(options *config.Options) Options { tsAlwaysStrict: options.TSAlwaysStrict, mangleProps: options.MangleProps, reserveProps: options.ReserveProps, + dropLabels: options.DropLabels, optionsThatSupportStructuralEquality: optionsThatSupportStructuralEquality{ unsupportedJSFeatures: options.UnsupportedJSFeatures, @@ -522,18 +525,23 @@ func (a *Options) Equal(b *Options) bool { return false } - // Compare "TSAlwaysStrict" + // Compare "tsAlwaysStrict" if (a.tsAlwaysStrict == nil && b.tsAlwaysStrict != nil) || (a.tsAlwaysStrict != nil && b.tsAlwaysStrict == nil) || (a.tsAlwaysStrict != nil && b.tsAlwaysStrict != nil && *a.tsAlwaysStrict != *b.tsAlwaysStrict) { return false } - // Compare "MangleProps" and "ReserveProps" + // Compare "mangleProps" and "reserveProps" if !isSameRegexp(a.mangleProps, b.mangleProps) || !isSameRegexp(a.reserveProps, b.reserveProps) { return false } - // Compare "InjectedFiles" + // Compare "dropLabels" + if !helpers.StringArraysEqual(a.dropLabels, b.dropLabels) { + return false + } + + // Compare "injectedFiles" if len(a.injectedFiles) != len(b.injectedFiles) { return false } @@ -549,7 +557,7 @@ func (a *Options) Equal(b *Options) bool { } } - // Compare "JSX" + // Compare "jsx" if a.jsx.Parse != b.jsx.Parse || !jsxExprsEqual(a.jsx.Factory, b.jsx.Factory) || !jsxExprsEqual(a.jsx.Fragment, b.jsx.Fragment) { return false } @@ -8803,7 +8811,7 @@ func (p *parser) mangleStmts(stmts []js_ast.Stmt, kind stmtsKind) []js_ast.Stmt break } } - result = appendIfBodyPreservingScope(result, stmt) + result = appendIfOrLabelBodyPreservingScope(result, stmt) if isJumpStatement(stmt.Data) { isControlFlowDead = true } @@ -9601,7 +9609,7 @@ func mangleFor(s *js_ast.SFor) { } } -func appendIfBodyPreservingScope(stmts []js_ast.Stmt, body js_ast.Stmt) []js_ast.Stmt { +func appendIfOrLabelBodyPreservingScope(stmts []js_ast.Stmt, body js_ast.Stmt) []js_ast.Stmt { if block, ok := body.Data.(*js_ast.SBlock); ok { keepBlock := false for _, stmt := range block.Stmts { @@ -9635,7 +9643,7 @@ func (p *parser) mangleIf(stmts []js_ast.Stmt, loc logger.Loc, s *js_ast.SIf) [] stmts = append(stmts, js_ast.Stmt{Loc: s.Test.Loc, Data: &js_ast.SExpr{Value: test}}) } } - return appendIfBodyPreservingScope(stmts, s.Yes) + return appendIfOrLabelBodyPreservingScope(stmts, s.Yes) } else { // We have to keep the "no" branch } @@ -9652,7 +9660,7 @@ func (p *parser) mangleIf(stmts []js_ast.Stmt, loc logger.Loc, s *js_ast.SIf) [] if s.NoOrNil.Data == nil { return stmts } - return appendIfBodyPreservingScope(stmts, s.NoOrNil) + return appendIfOrLabelBodyPreservingScope(stmts, s.NoOrNil) } else { // We have to keep the "yes" branch } @@ -10049,14 +10057,29 @@ func (p *parser) visitAndAppendStmt(stmts []js_ast.Stmt, stmt js_ast.Stmt) []js_ case *js_ast.SFor, *js_ast.SForIn, *js_ast.SForOf, *js_ast.SWhile, *js_ast.SDoWhile: p.currentScope.LabelStmtIsLoop = true } + + // Drop this entire statement if requested + if _, ok := p.dropLabelsMap[name]; ok { + old := p.isControlFlowDead + p.isControlFlowDead = true + s.Stmt = p.visitSingleStmt(s.Stmt, stmtsNormal) + p.isControlFlowDead = old + return stmts + } + s.Stmt = p.visitSingleStmt(s.Stmt, stmtsNormal) p.popScope() - // Optimize "x: break x" which some people apparently write by hand if p.options.minifySyntax { + // Optimize "x: break x" which some people apparently write by hand if child, ok := s.Stmt.Data.(*js_ast.SBreak); ok && child.Label != nil && child.Label.Ref == s.Name.Ref { return stmts } + + // Remove the label if it's not necessary + if p.symbols[ref.InnerIndex].UseCountEstimate == 0 { + return appendIfOrLabelBodyPreservingScope(stmts, s.Stmt) + } } case *js_ast.SLocal: @@ -16212,6 +16235,13 @@ func newParser(log logger.Log, source logger.Source, lexer js_lexer.Lexer, optio suppressWarningsAboutWeirdCode: helpers.IsInsideNodeModules(source.KeyPath.Text), } + if len(options.dropLabels) > 0 { + p.dropLabelsMap = make(map[string]struct{}) + for _, name := range options.dropLabels { + p.dropLabelsMap[name] = struct{}{} + } + } + if !options.minifyWhitespace { p.exprComments = make(map[logger.Loc][]string) } diff --git a/internal/js_parser/js_parser_test.go b/internal/js_parser/js_parser_test.go index a2f5ea395cc..ab7e3e46d80 100644 --- a/internal/js_parser/js_parser_test.go +++ b/internal/js_parser/js_parser_test.go @@ -2306,10 +2306,10 @@ func TestLabels(t *testing.T) { expectPrinted(t, "x: { class X { static { new X } } }", "x: {\n class X {\n static {\n new X();\n }\n }\n}\n") expectPrintedMangle(t, "x: break x", "") expectPrintedMangle(t, "x: { break x; foo() }", "") - expectPrintedMangle(t, "y: while (foo()) x: { break x; foo() }", "y:\n for (; foo(); )\n ;\n") - expectPrintedMangle(t, "y: while (foo()) x: { break y; foo() }", "y:\n for (; foo(); )\n x:\n break y;\n") - expectPrintedMangle(t, "x: { y: { z: { foo(); break x; } } }", "x:\n y:\n z: {\n foo();\n break x;\n }\n") - expectPrintedMangle(t, "x: { class X { static { new X } } }", "x: {\n class X {\n static {\n new X();\n }\n }\n}\n") + expectPrintedMangle(t, "y: while (foo()) x: { break x; foo() }", "for (; foo(); )\n ;\n") + expectPrintedMangle(t, "y: while (foo()) x: { break y; foo() }", "y:\n for (; foo(); )\n break y;\n") + expectPrintedMangle(t, "x: { y: { z: { foo(); break x; } } }", "x: {\n foo();\n break x;\n}\n") + expectPrintedMangle(t, "x: { class X { static { new X } } }", "{\n class X {\n static {\n new X();\n }\n }\n}\n") } func TestArrow(t *testing.T) { @@ -3678,9 +3678,13 @@ func TestMangleIf(t *testing.T) { expectPrintedMangle(t, "x: while (x) y: while (y) { if (a) continue x; if (b) continue y; }", "x:\n for (; x; )\n y:\n for (; y; ) {\n if (a)\n continue x;\n if (b)\n continue y;\n }\n") expectPrintedMangle(t, "x: while (x) y: while (y) { if (a) break x; if (b) break x; }", - "x:\n for (; x; )\n y:\n for (; y; )\n if (a || b)\n break x;\n") + "x:\n for (; x; )\n for (; y; )\n if (a || b)\n break x;\n") expectPrintedMangle(t, "x: while (x) y: while (y) { if (a) continue x; if (b) continue x; }", - "x:\n for (; x; )\n y:\n for (; y; )\n if (a || b)\n continue x;\n") + "x:\n for (; x; )\n for (; y; )\n if (a || b)\n continue x;\n") + expectPrintedMangle(t, "x: while (x) y: while (y) { if (a) break y; if (b) break y; }", + "for (; x; )\n y:\n for (; y; )\n if (a || b)\n break y;\n") + expectPrintedMangle(t, "x: while (x) y: while (y) { if (a) continue y; if (b) continue y; }", + "for (; x; )\n y:\n for (; y; )\n if (a || b)\n continue y;\n") expectPrintedNormalAndMangle(t, "if (x ? y : 0) foo()", "if (x ? y : 0)\n foo();\n", "x && y && foo();\n") expectPrintedNormalAndMangle(t, "if (x ? y : 1) foo()", "if (x ? y : 1)\n foo();\n", "(!x || y) && foo();\n") @@ -3754,7 +3758,7 @@ func TestMangleWrapToAvoidAmbiguousElse(t *testing.T) { expectPrintedMangle(t, "if (a) for (x in y) { if (b) return c } else return d", "if (a) {\n for (x in y)\n if (b)\n return c;\n} else\n return d;\n") expectPrintedMangle(t, "if (a) for (x of y) { if (b) return c } else return d", "if (a) {\n for (x of y)\n if (b)\n return c;\n} else\n return d;\n") expectPrintedMangle(t, "if (a) with (x) { if (b) return c } else return d", "if (a) {\n with (x)\n if (b)\n return c;\n} else\n return d;\n") - expectPrintedMangle(t, "if (a) x: { if (b) return c } else return d", "if (a) {\n x:\n if (b)\n return c;\n} else\n return d;\n") + expectPrintedMangle(t, "if (a) x: { if (b) break x } else return c", "if (a) {\n x:\n if (b)\n break x;\n} else\n return c;\n") } func TestMangleOptionalChain(t *testing.T) { @@ -4921,8 +4925,12 @@ func TestTrimCodeInDeadControlFlow(t *testing.T) { expectPrintedMangle(t, "if (1) a(); else { let b }", "a();\n") expectPrintedMangle(t, "if (1) a(); else { throw b }", "a();\n") expectPrintedMangle(t, "if (1) a(); else { return b }", "a();\n") - expectPrintedMangle(t, "b: { if (1) a(); else { break b } }", "b:\n a();\n") - expectPrintedMangle(t, "b: while (1) if (1) a(); else { continue b }", "b:\n for (; ; )\n a();\n") + expectPrintedMangle(t, "b: { if (x) a(); else { break b } }", "b:\n if (x)\n a();\n else\n break b;\n") + expectPrintedMangle(t, "b: { if (1) a(); else { break b } }", "a();\n") + expectPrintedMangle(t, "b: { if (0) a(); else { break b } }", "") + expectPrintedMangle(t, "b: while (1) if (x) a(); else { continue b }", "b:\n for (; ; )\n if (x)\n a();\n else\n continue b;\n") + expectPrintedMangle(t, "b: while (1) if (1) a(); else { continue b }", "for (; ; )\n a();\n") + expectPrintedMangle(t, "b: while (1) if (0) a(); else { continue b }", "b:\n for (; ; )\n continue b;\n") expectPrintedMangle(t, "if (1) a(); else { class b {} }", "a();\n") expectPrintedMangle(t, "if (1) a(); else { debugger }", "a();\n") diff --git a/lib/shared/common.ts b/lib/shared/common.ts index a7c6ade82d3..a988c6372d5 100644 --- a/lib/shared/common.ts +++ b/lib/shared/common.ts @@ -147,6 +147,7 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe let minifyIdentifiers = getFlag(options, keys, 'minifyIdentifiers', mustBeBoolean) let lineLimit = getFlag(options, keys, 'lineLimit', mustBeInteger) let drop = getFlag(options, keys, 'drop', mustBeArray) + let dropLabels = getFlag(options, keys, 'dropLabels', mustBeArray) let charset = getFlag(options, keys, 'charset', mustBeString) let treeShaking = getFlag(options, keys, 'treeShaking', mustBeBoolean) let ignoreAnnotations = getFlag(options, keys, 'ignoreAnnotations', mustBeBoolean) @@ -185,6 +186,7 @@ function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKe if (treeShaking !== void 0) flags.push(`--tree-shaking=${treeShaking}`) if (ignoreAnnotations) flags.push(`--ignore-annotations`) if (drop) for (let what of drop) flags.push(`--drop:${validateStringValue(what, 'drop')}`) + if (dropLabels) flags.push(`--drop-labels=${Array.from(dropLabels).map(what => validateStringValue(what, 'dropLabels')).join(',')}`) if (mangleProps) flags.push(`--mangle-props=${mangleProps.source}`) if (reserveProps) flags.push(`--reserve-props=${reserveProps.source}`) if (mangleQuoted !== void 0) flags.push(`--mangle-quoted=${mangleQuoted}`) diff --git a/lib/shared/types.ts b/lib/shared/types.ts index 2e62b26090b..279de897091 100644 --- a/lib/shared/types.ts +++ b/lib/shared/types.ts @@ -36,6 +36,8 @@ interface CommonOptions { mangleCache?: Record /** Documentation: https://esbuild.github.io/api/#drop */ drop?: Drop[] + /** Documentation: https://esbuild.github.io/api/#drop-labels */ + dropLabels?: string[] /** Documentation: https://esbuild.github.io/api/#minify */ minify?: boolean /** Documentation: https://esbuild.github.io/api/#minify */ diff --git a/pkg/api/api.go b/pkg/api/api.go index 2ec4b9a20eb..032ba23f414 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -282,6 +282,7 @@ type BuildOptions struct { MangleQuoted MangleQuoted // Documentation: https://esbuild.github.io/api/#mangle-props MangleCache map[string]interface{} // Documentation: https://esbuild.github.io/api/#mangle-props Drop Drop // Documentation: https://esbuild.github.io/api/#drop + DropLabels []string // Documentation: https://esbuild.github.io/api/#drop-labels MinifyWhitespace bool // Documentation: https://esbuild.github.io/api/#minify MinifyIdentifiers bool // Documentation: https://esbuild.github.io/api/#minify MinifySyntax bool // Documentation: https://esbuild.github.io/api/#minify @@ -415,6 +416,7 @@ type TransformOptions struct { MangleQuoted MangleQuoted // Documentation: https://esbuild.github.io/api/#mangle-props MangleCache map[string]interface{} // Documentation: https://esbuild.github.io/api/#mangle-props Drop Drop // Documentation: https://esbuild.github.io/api/#drop + DropLabels []string // Documentation: https://esbuild.github.io/api/#drop-labels MinifyWhitespace bool // Documentation: https://esbuild.github.io/api/#minify MinifyIdentifiers bool // Documentation: https://esbuild.github.io/api/#minify MinifySyntax bool // Documentation: https://esbuild.github.io/api/#minify diff --git a/pkg/api/api_impl.go b/pkg/api/api_impl.go index e759d91a630..9880c8b5aaa 100644 --- a/pkg/api/api_impl.go +++ b/pkg/api/api_impl.go @@ -1284,6 +1284,7 @@ func validateBuildOptions( MangleProps: validateRegex(log, "mangle props", buildOpts.MangleProps), ReserveProps: validateRegex(log, "reserve props", buildOpts.ReserveProps), MangleQuoted: buildOpts.MangleQuoted == MangleQuotedTrue, + DropLabels: append([]string{}, buildOpts.DropLabels...), DropDebugger: (buildOpts.Drop & DropDebugger) != 0, AllowOverwrite: buildOpts.AllowOverwrite, ASCIIOnly: validateASCIIOnly(buildOpts.Charset), @@ -1728,6 +1729,7 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult MangleProps: validateRegex(log, "mangle props", transformOpts.MangleProps), ReserveProps: validateRegex(log, "reserve props", transformOpts.ReserveProps), MangleQuoted: transformOpts.MangleQuoted == MangleQuotedTrue, + DropLabels: append([]string{}, transformOpts.DropLabels...), DropDebugger: (transformOpts.Drop & DropDebugger) != 0, ASCIIOnly: validateASCIIOnly(transformOpts.Charset), IgnoreDCEAnnotations: transformOpts.IgnoreAnnotations, diff --git a/pkg/cli/cli_impl.go b/pkg/cli/cli_impl.go index a7ca286b489..a23000bd016 100644 --- a/pkg/cli/cli_impl.go +++ b/pkg/cli/cli_impl.go @@ -226,6 +226,13 @@ func parseOptionsImpl( ) } + case strings.HasPrefix(arg, "--drop-labels="): + if buildOpts != nil { + buildOpts.DropLabels = splitWithEmptyCheck(arg[len("--drop-labels="):], ",") + } else { + transformOpts.DropLabels = splitWithEmptyCheck(arg[len("--drop-labels="):], ",") + } + case strings.HasPrefix(arg, "--legal-comments="): value := arg[len("--legal-comments="):] var legalComments api.LegalComments