diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e66813b116..ceace38106c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/internal/bundler/bundler_default_test.go b/internal/bundler/bundler_default_test.go index 3f0c84b342d..d611eb58ed0 100644 --- a/internal/bundler/bundler_default_test.go +++ b/internal/bundler/bundler_default_test.go @@ -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{ diff --git a/internal/bundler/snapshots/snapshots_default.txt b/internal/bundler/snapshots/snapshots_default.txt index 963f8c1ecfa..2ac6aa8c64f 100644 --- a/internal/bundler/snapshots/snapshots_default.txt +++ b/internal/bundler/snapshots/snapshots_default.txt @@ -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 ---------- diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 42ee843fd9c..2235d85db20 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -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" @@ -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 + } } } @@ -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{} } diff --git a/internal/js_parser/js_parser_lower.go b/internal/js_parser/js_parser_lower.go index 524314e4130..442c1c72ec4 100644 --- a/internal/js_parser/js_parser_lower.go +++ b/internal/js_parser/js_parser_lower.go @@ -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} }