Skip to content

Commit

Permalink
fix #1361: allow "this" with "--define"
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jun 10, 2021
1 parent 1eb9f0e commit 7a05678
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 27 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Changelog

## Unreleased

* Allow `this` with `--define` ([#1361](https://github.com/evanw/esbuild/issues/1361))

You can now override the default value of top-level `this` with the `--define` feature. Top-level `this` defaults to being `undefined` in ECMAScript modules and `exports` in CommonJS modules. For example:

```js
// Original code
((obj) => {
...
})(this);

// Output with "--define:this=window"
((obj) => {
...
})(window);
```

Note that overriding what top-level `this` is will likely break code that uses it correctly. So this new feature is only useful in certain cases.

## 0.12.8

* Plugins can now specify `sideEffects: false` ([#1009](https://github.com/evanw/esbuild/issues/1009))
Expand Down
66 changes: 66 additions & 0 deletions internal/bundler/bundler_default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4003,6 +4003,72 @@ func TestDefineImportMeta(t *testing.T) {
})
}

func TestDefineThis(t *testing.T) {
defines := config.ProcessDefines(map[string]config.DefineData{
"this": {
DefineFunc: func(args config.DefineArgs) js_ast.E {
return &js_ast.ENumber{Value: 1}
},
},
"this.foo": {
DefineFunc: func(args config.DefineArgs) js_ast.E {
return &js_ast.ENumber{Value: 2}
},
},
"this.foo.bar": {
DefineFunc: func(args config.DefineArgs) js_ast.E {
return &js_ast.ENumber{Value: 3}
},
},
})
default_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
ok(
// These should be fully substituted
this,
this.foo,
this.foo.bar,
// Should just substitute "this.foo"
this.foo.baz,
// This should not be substituted
this.bar,
);
// This code should be the same as above
(() => {
ok(
this,
this.foo,
this.foo.bar,
this.foo.baz,
this.bar,
);
})();
// Nothing should be substituted in this code
(function() {
doNotSubstitute(
this,
this.foo,
this.foo.bar,
this.foo.baz,
this.bar,
);
})();
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.js",
Defines: &defines,
},
})
}

func TestKeepNamesTreeShaking(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
Expand Down
12 changes: 12 additions & 0 deletions internal/bundler/snapshots/snapshots_default.txt
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,18 @@ TestDefineImportMeta
// entry.js
console.log(1, 2, 3, 2 .baz, 1 .bar);

================================================================================
TestDefineThis
---------- /out.js ----------
// entry.js
ok(1, 2, 3, 2 .baz, 1 .bar);
(() => {
ok(1, 2, 3, 2 .baz, 1 .bar);
})();
(function() {
doNotSubstitute(this, this.foo, this.foo.bar, this.foo.baz, this.bar);
})();

================================================================================
TestDirectEvalTaintingNoBundle
---------- /out.js ----------
Expand Down
82 changes: 56 additions & 26 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -9735,6 +9735,12 @@ func (p *parser) isDotDefineMatch(expr js_ast.Expr, parts []string) bool {
p.isDotDefineMatch(e.Target, parts[:last])
}

case *js_ast.EThis:
// Allow matching on top-level "this"
if !p.fnOnlyDataVisit.isThisNested {
return len(parts) == 1 && parts[0] == "this"
}

case *js_ast.EImportMeta:
// Allow matching on "import.meta"
return len(parts) == 2 && parts[0] == "import" && parts[1] == "meta"
Expand Down Expand Up @@ -10316,40 +10322,61 @@ func (p *parser) visitExpr(expr js_ast.Expr) js_ast.Expr {
return expr
}

func (p *parser) valueForThis(loc logger.Loc, shouldWarn bool) (js_ast.Expr, bool) {
func (p *parser) valueForThis(
loc logger.Loc,
shouldWarn bool,
assignTarget js_ast.AssignTarget,
isCallTarget bool,
isDeleteTarget bool,
) (js_ast.Expr, bool) {
// Substitute "this" if we're inside a static class property initializer
if p.fnOnlyDataVisit.thisClassStaticRef != nil {
p.recordUsage(*p.fnOnlyDataVisit.thisClassStaticRef)
return js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: *p.fnOnlyDataVisit.thisClassStaticRef}}, true
}

if p.options.mode != config.ModePassThrough && !p.fnOnlyDataVisit.isThisNested {
if p.hasESModuleSyntax {
// Warn about "this" becoming undefined, but only once per file
if shouldWarn && !p.warnedThisIsUndefined {
p.warnedThisIsUndefined = true
r := js_lexer.RangeOfIdentifier(p.source, loc)
text := "Top-level \"this\" will be replaced with undefined since this file is an ECMAScript module"
notes := p.whyESModule()
// Is this a top-level use of "this"?
if !p.fnOnlyDataVisit.isThisNested {
// Substitute user-specified defines
if data, ok := p.options.defines.IdentifierDefines["this"]; ok {
if data.DefineFunc != nil {
return p.valueForDefine(loc, data.DefineFunc, identifierOpts{
assignTarget: assignTarget,
isCallTarget: isCallTarget,
isDeleteTarget: isDeleteTarget,
}), true
}
}

// Show the warning as a debug message if we're in "node_modules"
if !p.suppressWarningsAboutWeirdCode {
p.log.AddRangeWarningWithNotes(&p.tracker, r, text, notes)
} else {
p.log.AddRangeDebugWithNotes(&p.tracker, r, text, notes)
// Otherwise, replace top-level "this" with either "undefined" or "exports"
if p.options.mode != config.ModePassThrough {
if p.hasESModuleSyntax {
// Warn about "this" becoming undefined, but only once per file
if shouldWarn && !p.warnedThisIsUndefined {
p.warnedThisIsUndefined = true
r := js_lexer.RangeOfIdentifier(p.source, loc)
text := "Top-level \"this\" will be replaced with undefined since this file is an ECMAScript module"
notes := p.whyESModule()

// Show the warning as a debug message if we're in "node_modules"
if !p.suppressWarningsAboutWeirdCode {
p.log.AddRangeWarningWithNotes(&p.tracker, r, text, notes)
} else {
p.log.AddRangeDebugWithNotes(&p.tracker, r, text, notes)
}
}
}

// In an ES6 module, "this" is supposed to be undefined. Instead of
// doing this at runtime using "fn.call(undefined)", we do it at
// compile time using expression substitution here.
return js_ast.Expr{Loc: loc, Data: js_ast.EUndefinedShared}, true
} else {
// In a CommonJS module, "this" is supposed to be the same as "exports".
// Instead of doing this at runtime using "fn.call(module.exports)", we
// do it at compile time using expression substitution here.
p.recordUsage(p.exportsRef)
return js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: p.exportsRef}}, true
// In an ES6 module, "this" is supposed to be undefined. Instead of
// doing this at runtime using "fn.call(undefined)", we do it at
// compile time using expression substitution here.
return js_ast.Expr{Loc: loc, Data: js_ast.EUndefinedShared}, true
} else {
// In a CommonJS module, "this" is supposed to be the same as "exports".
// Instead of doing this at runtime using "fn.call(module.exports)", we
// do it at compile time using expression substitution here.
p.recordUsage(p.exportsRef)
return js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: p.exportsRef}}, true
}
}
}

Expand Down Expand Up @@ -10516,7 +10543,10 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
}

case *js_ast.EThis:
if value, ok := p.valueForThis(expr.Loc, true /* shouldWarn */); ok {
isDeleteTarget := e == p.deleteTarget
isCallTarget := e == p.callTarget

if value, ok := p.valueForThis(expr.Loc, true /* shouldWarn */, in.assignTarget, isDeleteTarget, isCallTarget); ok {
return value, exprOut{}
}

Expand Down
8 changes: 7 additions & 1 deletion internal/js_parser/js_parser_lower.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,13 @@ func (p *parser) lowerFunction(
}

// Determine the value for "this"
thisValue, hasThisValue := p.valueForThis(bodyLoc, false /* shouldWarn */)
thisValue, hasThisValue := p.valueForThis(
bodyLoc,
false, /* shouldWarn */
js_ast.AssignTargetNone,
false, /* isCallTarget */
false, /* isDeleteTarget */
)
if !hasThisValue {
thisValue = js_ast.Expr{Loc: bodyLoc, Data: js_ast.EThisShared}
}
Expand Down

0 comments on commit 7a05678

Please sign in to comment.