Skip to content

Commit

Permalink
fix #28: add the "--drop:console" flag
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Dec 29, 2021
1 parent ce7262f commit 674eb50
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 18 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,12 @@

Passing this flag causes all [`debugger;` statements](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/debugger) to be removed from the output. This is similar to the `drop_debugger: true` flag available in the popular UglifyJS and Terser JavaScript minifiers.

* Add the `--drop:console` flag ([#28](https://github.com/evanw/esbuild/issues/28))

Passing this flag causes all [`console.xyz()` API calls](https://developer.mozilla.org/en-US/docs/Web/API/console#methods) to be removed from the output. This is similar to the `drop_console: true` flag available in the popular UglifyJS and Terser JavaScript minifiers.

WARNING: Using this flag can introduce bugs into your code! This flag removes the entire call expression including all call arguments. If any of those arguments had important side effects, using this flag will change the behavior of your code. Be very careful when using this flag. If you want to remove console API calls without removing arguments with side effects (which does not introduce bugs), you should mark the relevant API calls as pure instead like this: `--pure:console.log --minify`.

## 0.14.9

* Implement cross-module tree shaking of TypeScript enum values ([#128](https://github.com/evanw/esbuild/issues/128))
Expand Down
2 changes: 1 addition & 1 deletion cmd/esbuild/main.go
Expand Up @@ -67,7 +67,7 @@ var helpText = func(colors logger.Colors) string {
--chunk-names=... Path template to use for code splitting chunks
(default "[name]-[hash]")
--color=... Force use of color terminal escapes (true | false)
--drop:... Remove certain constructs (debugger)
--drop:... Remove certain constructs (console | debugger)
--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
Expand Down
6 changes: 6 additions & 0 deletions internal/config/globals.go
Expand Up @@ -845,6 +845,12 @@ type DefineData struct {
// example, a bare call to "Object()" can be removed because it does not
// have any observable side effects.
CallCanBeUnwrappedIfUnused bool

// If true, the user has indicated that every direct calls to a property on
// this object and all of that call's arguments are to be removed from the
// output, even when the arguments have side effects. This is used to
// implement the "--drop:console" flag.
MethodCallsMustBeReplacedWithUndefined bool
}

func mergeDefineData(old DefineData, new DefineData) DefineData {
Expand Down
71 changes: 59 additions & 12 deletions internal/js_parser/js_parser.go
Expand Up @@ -9217,14 +9217,30 @@ func (p *parser) visitAndAppendStmt(stmts []js_ast.Stmt, stmt js_ast.Stmt) []js_
}

case *js_ast.SExpr:
shouldTrimUndefined := false
if !p.options.mangleSyntax {
if _, ok := s.Value.Data.(*js_ast.ECall); ok {
shouldTrimUndefined = true
}
}

p.stmtExprValue = s.Value.Data
s.Value = p.visitExpr(s.Value)

// If this was a call and is now undefined, then it probably was a console
// API call that was dropped with "--drop:console". Manually discard the
// undefined value even when we're not minifying for aesthetic reasons.
if shouldTrimUndefined {
if _, ok := s.Value.Data.(*js_ast.EUndefined); ok {
return stmts
}
}

// Trim expressions without side effects
if p.options.mangleSyntax {
s.Value = p.simplifyUnusedExpr(s.Value)
if s.Value.Data == nil {
stmt = js_ast.Stmt{Loc: stmt.Loc, Data: &js_ast.SEmpty{}}
return stmts
}
}

Expand Down Expand Up @@ -10970,6 +10986,10 @@ type exprOut struct {
// with an IsOptionalChain value of true)
childContainsOptionalChain bool

// If true and this is used as a call target, the whole call expression
// must be replaced with undefined.
methodCallMustBeReplacedWithUndefined bool

// If our parent is an ECall node with an OptionalChain value of
// OptionalChainContinue, then we may need to return the value for "this"
// from this node or one of this node's children so that the parent that is
Expand Down Expand Up @@ -11330,6 +11350,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
}

// Substitute user-specified defines for unbound or injected symbols
methodCallMustBeReplacedWithUndefined := false
if p.symbols[e.Ref.InnerIndex].Kind.IsUnboundOrInjected() && !result.isInsideWithScope && e != p.deleteTarget {
if data, ok := p.options.defines.IdentifierDefines[name]; ok {
if data.DefineFunc != nil {
Expand All @@ -11354,15 +11375,20 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
if data.CallCanBeUnwrappedIfUnused && !p.options.ignoreDCEAnnotations {
e.CallCanBeUnwrappedIfUnused = true
}
if data.MethodCallsMustBeReplacedWithUndefined {
methodCallMustBeReplacedWithUndefined = true
}
}
}

return p.handleIdentifier(expr.Loc, e, identifierOpts{
assignTarget: in.assignTarget,
isCallTarget: isCallTarget,
isDeleteTarget: isDeleteTarget,
wasOriginallyIdentifier: true,
}), exprOut{}
assignTarget: in.assignTarget,
isCallTarget: isCallTarget,
isDeleteTarget: isDeleteTarget,
wasOriginallyIdentifier: true,
}), exprOut{
methodCallMustBeReplacedWithUndefined: methodCallMustBeReplacedWithUndefined,
}

case *js_ast.EPrivateIdentifier:
// We should never get here
Expand Down Expand Up @@ -12118,9 +12144,10 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO

// Potentially rewrite this property access
out = exprOut{
childContainsOptionalChain: containsOptionalChain,
thisArgFunc: out.thisArgFunc,
thisArgWrapFunc: out.thisArgWrapFunc,
childContainsOptionalChain: containsOptionalChain,
methodCallMustBeReplacedWithUndefined: out.methodCallMustBeReplacedWithUndefined,
thisArgFunc: out.thisArgFunc,
thisArgWrapFunc: out.thisArgWrapFunc,
}
if !in.hasChainParent {
out.thisArgFunc = nil
Expand Down Expand Up @@ -12359,9 +12386,10 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO

// Potentially rewrite this property access
out = exprOut{
childContainsOptionalChain: containsOptionalChain,
thisArgFunc: out.thisArgFunc,
thisArgWrapFunc: out.thisArgWrapFunc,
childContainsOptionalChain: containsOptionalChain,
methodCallMustBeReplacedWithUndefined: out.methodCallMustBeReplacedWithUndefined,
thisArgFunc: out.thisArgFunc,
thisArgWrapFunc: out.thisArgWrapFunc,
}
if !in.hasChainParent {
out.thisArgFunc = nil
Expand Down Expand Up @@ -12879,6 +12907,19 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
p.warnAboutImportNamespaceCall(e.Target, exprKindCall)

hasSpread := false
oldIsControlFlowDead := p.isControlFlowDead

// If we're removing this call, don't count any arguments as symbol uses
if out.methodCallMustBeReplacedWithUndefined {
switch e.Target.Data.(type) {
case *js_ast.EDot, *js_ast.EIndex:
p.isControlFlowDead = true
default:
out.methodCallMustBeReplacedWithUndefined = false
}
}

// Visit the arguments
for i, arg := range e.Args {
arg = p.visitExpr(arg)
if _, ok := arg.Data.(*js_ast.ESpread); ok {
Expand All @@ -12887,6 +12928,12 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
e.Args[i] = arg
}

// Stop now if this call must be removed
if out.methodCallMustBeReplacedWithUndefined {
p.isControlFlowDead = oldIsControlFlowDead
return js_ast.Expr{Loc: expr.Loc, Data: js_ast.EUndefinedShared}, exprOut{}
}

// Recognize "require.resolve()" calls
if couldBeRequireResolve {
if dot, ok := e.Target.Data.(*js_ast.EDot); ok && dot.Name == "resolve" {
Expand Down
2 changes: 1 addition & 1 deletion lib/shared/types.ts
Expand Up @@ -3,7 +3,7 @@ export type Format = 'iife' | 'cjs' | 'esm';
export type Loader = 'js' | 'jsx' | 'ts' | 'tsx' | 'css' | 'json' | 'text' | 'base64' | 'file' | 'dataurl' | 'binary' | 'default';
export type LogLevel = 'verbose' | 'debug' | 'info' | 'warning' | 'error' | 'silent';
export type Charset = 'ascii' | 'utf8';
export type Drop = 'debugger';
export type Drop = 'console' | 'debugger';

interface CommonOptions {
/** Documentation: https://esbuild.github.io/api/#sourcemap */
Expand Down
3 changes: 2 additions & 1 deletion pkg/api/api.go
Expand Up @@ -242,7 +242,8 @@ const (
type Drop uint8

const (
DropDebugger Drop = 1 << iota
DropConsole Drop = 1 << iota
DropDebugger
)

////////////////////////////////////////////////////////////////////////////////
Expand Down
12 changes: 10 additions & 2 deletions pkg/api/api_impl.go
Expand Up @@ -449,6 +449,7 @@ func validateDefines(
pureFns []string,
platform Platform,
minify bool,
drop Drop,
) (*config.ProcessedDefines, []config.InjectedDefine) {
rawDefines := make(map[string]config.DefineData)
var valueToInject map[string]config.InjectedDefine
Expand Down Expand Up @@ -554,6 +555,13 @@ func validateDefines(
}
}

// If we're dropping all console API calls, replace each one with undefined
if (drop & DropConsole) != 0 {
define := rawDefines["console"]
define.MethodCallsMustBeReplacedWithUndefined = true
rawDefines["console"] = define
}

for _, key := range pureFns {
// The key must be a dot-separated identifier list
for _, part := range strings.Split(key, ".") {
Expand Down Expand Up @@ -827,7 +835,7 @@ func rebuildImpl(
bannerJS, bannerCSS := validateBannerOrFooter(log, "banner", buildOpts.Banner)
footerJS, footerCSS := validateBannerOrFooter(log, "footer", buildOpts.Footer)
minify := buildOpts.MinifyWhitespace && buildOpts.MinifyIdentifiers && buildOpts.MinifySyntax
defines, injectedDefines := validateDefines(log, buildOpts.Define, buildOpts.Pure, buildOpts.Platform, minify)
defines, injectedDefines := validateDefines(log, buildOpts.Define, buildOpts.Pure, buildOpts.Platform, minify, buildOpts.Drop)
options := config.Options{
TargetFromAPI: targetFromAPI,
UnsupportedJSFeatures: jsFeatures,
Expand Down Expand Up @@ -1321,7 +1329,7 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult

// Convert and validate the transformOpts
targetFromAPI, jsFeatures, cssFeatures, targetEnv := validateFeatures(log, transformOpts.Target, transformOpts.Engines)
defines, injectedDefines := validateDefines(log, transformOpts.Define, transformOpts.Pure, PlatformNeutral, false /* minify */)
defines, injectedDefines := validateDefines(log, transformOpts.Define, transformOpts.Pure, PlatformNeutral, false /* minify */, transformOpts.Drop)
options := config.Options{
TargetFromAPI: targetFromAPI,
UnsupportedJSFeatures: jsFeatures,
Expand Down
8 changes: 7 additions & 1 deletion pkg/cli/cli_impl.go
Expand Up @@ -99,6 +99,12 @@ func parseOptionsImpl(
case strings.HasPrefix(arg, "--drop:"):
value := arg[len("--drop:"):]
switch value {
case "console":
if buildOpts != nil {
buildOpts.Drop |= api.DropConsole
} else {
transformOpts.Drop |= api.DropConsole
}
case "debugger":
if buildOpts != nil {
buildOpts.Drop |= api.DropDebugger
Expand All @@ -108,7 +114,7 @@ func parseOptionsImpl(
default:
return cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Invalid value %q in %q", value, arg),
"Valid values are \"debugger\".",
"Valid values are \"console\" or \"debugger\".",
), nil
}

Expand Down
19 changes: 19 additions & 0 deletions scripts/js-api-tests.js
Expand Up @@ -3692,6 +3692,25 @@ let transformTests = {
assert.strictEqual(code, `console.log("ab"+c);\n`)
},

async keepConsole({ esbuild }) {
const { code } = await esbuild.transform(`console.log('foo')`, { drop: [] })
assert.strictEqual(code, `console.log("foo");\n`)
},

async dropConsole({ esbuild }) {
const { code } = await esbuild.transform(`
console('foo')
console.log('foo')
console.log(foo())
x = console.log(bar())
console.abc.xyz('foo')
console['log']('foo')
console[abc][xyz]('foo')
console[foo()][bar()]('foo')
`, { drop: ['console'] })
assert.strictEqual(code, `console("foo");\nx = void 0;\n`)
},

async keepDebugger({ esbuild }) {
const { code } = await esbuild.transform(`if (x) debugger`, { drop: [] })
assert.strictEqual(code, `if (x)\n debugger;\n`)
Expand Down

0 comments on commit 674eb50

Please sign in to comment.