From 889bab11e1c8ea838a743e4dd666d2b7bb8bb366 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Wed, 15 Jun 2022 16:17:31 -0400 Subject: [PATCH] fix #2292: allow entity names as define values --- CHANGELOG.md | 4 + internal/bundler/bundler.go | 4 +- internal/bundler/bundler_default_test.go | 96 ++++++------- internal/bundler/bundler_ts_test.go | 5 +- internal/config/config.go | 9 +- internal/config/globals.go | 32 +++-- internal/js_parser/js_parser.go | 176 ++++++++++++----------- pkg/api/api_impl.go | 97 ++++--------- scripts/js-api-tests.js | 21 ++- 9 files changed, 217 insertions(+), 227 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e1023e9347..2c17b40405d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,10 @@ This fix was contributed by [@nkeynes](https://github.com/nkeynes). +* Allow entity names as define values ([#2292](https://github.com/evanw/esbuild/issues/2292)) + + The "define" feature allows you to replace certain expressions with certain other expressions at compile time. For example, you might want to replace the global identifier `IS_PRODUCTION` with the boolean value `true` when building for production. Previously the only expressions you could substitute in were either identifier expressions or anything that is valid JSON syntax. This limitation exists because supporting more complex expressions is more complex (for example, substituting in a `require()` call could potentially pull in additional files, which would need to be handled). With this release, you can now also now define something as a member expression chain of the form `foo.abc.xyz`. + * Implement package self-references ([#2312](https://github.com/evanw/esbuild/issues/2312)) This release implements a rarely-used feature in node where a package can import itself by name instead of using relative imports. You can read more about this feature here: https://nodejs.org/api/packages.html#self-referencing-a-package-using-its-name. For example, assuming the `package.json` in a given package looks like this: diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index af51db7f172..a7325523aa7 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -1189,10 +1189,10 @@ func (s *scanner) maybeParseFile( // Allow certain properties to be overridden if len(resolveResult.JSXFactory) > 0 { - optionsClone.JSX.Factory = config.JSXExpr{Parts: resolveResult.JSXFactory} + optionsClone.JSX.Factory = config.DefineExpr{Parts: resolveResult.JSXFactory} } if len(resolveResult.JSXFragment) > 0 { - optionsClone.JSX.Fragment = config.JSXExpr{Parts: resolveResult.JSXFragment} + optionsClone.JSX.Fragment = config.DefineExpr{Parts: resolveResult.JSXFragment} } if resolveResult.UseDefineForClassFieldsTS != config.Unspecified { optionsClone.UseDefineForClassFields = resolveResult.UseDefineForClassFieldsTS diff --git a/internal/bundler/bundler_default_test.go b/internal/bundler/bundler_default_test.go index 6a4cc185ead..59b9dce7137 100644 --- a/internal/bundler/bundler_default_test.go +++ b/internal/bundler/bundler_default_test.go @@ -449,8 +449,8 @@ func TestJSXImportsCommonJS(t *testing.T) { options: config.Options{ Mode: config.ModeBundle, JSX: config.JSXOptions{ - Factory: config.JSXExpr{Parts: []string{"elem"}}, - Fragment: config.JSXExpr{Parts: []string{"frag"}}, + Factory: config.DefineExpr{Parts: []string{"elem"}}, + Fragment: config.DefineExpr{Parts: []string{"frag"}}, }, AbsOutputFile: "/out.js", }, @@ -473,8 +473,8 @@ func TestJSXImportsES6(t *testing.T) { options: config.Options{ Mode: config.ModeBundle, JSX: config.JSXOptions{ - Factory: config.JSXExpr{Parts: []string{"elem"}}, - Fragment: config.JSXExpr{Parts: []string{"frag"}}, + Factory: config.DefineExpr{Parts: []string{"elem"}}, + Fragment: config.DefineExpr{Parts: []string{"frag"}}, }, AbsOutputFile: "/out.js", }, @@ -529,7 +529,7 @@ func TestJSXConstantFragments(t *testing.T) { Mode: config.ModeBundle, AbsOutputFile: "/out.js", JSX: config.JSXOptions{ - Fragment: config.JSXExpr{ + Fragment: config.DefineExpr{ Constant: &js_ast.EString{Value: helpers.StringToUTF16("]")}, }, }, @@ -2090,7 +2090,7 @@ func TestImportReExportES6Issue149(t *testing.T) { options: config.Options{ Mode: config.ModeBundle, JSX: config.JSXOptions{ - Factory: config.JSXExpr{Parts: []string{"h"}}, + Factory: config.DefineExpr{Parts: []string{"h"}}, }, AbsOutputFile: "/out.js", ExternalSettings: config.ExternalSettings{ @@ -3979,18 +3979,18 @@ func TestInjectDuplicate(t *testing.T) { func TestInject(t *testing.T) { defines := config.ProcessDefines(map[string]config.DefineData{ "chain.prop": { - DefineFunc: func(args config.DefineArgs) js_ast.E { - return &js_ast.EIdentifier{Ref: args.FindSymbol(args.Loc, "replace")} + DefineExpr: &config.DefineExpr{ + Parts: []string{"replace"}, }, }, "obj.defined": { - DefineFunc: func(args config.DefineArgs) js_ast.E { - return &js_ast.EString{Value: helpers.StringToUTF16("defined")} + DefineExpr: &config.DefineExpr{ + Constant: &js_ast.EString{Value: helpers.StringToUTF16("defined")}, }, }, "injectedAndDefined": { - DefineFunc: func(args config.DefineArgs) js_ast.E { - return &js_ast.EString{Value: helpers.StringToUTF16("should be used")} + DefineExpr: &config.DefineExpr{ + Constant: &js_ast.EString{Value: helpers.StringToUTF16("should be used")}, }, }, }) @@ -4059,18 +4059,18 @@ func TestInject(t *testing.T) { func TestInjectNoBundle(t *testing.T) { defines := config.ProcessDefines(map[string]config.DefineData{ "chain.prop": { - DefineFunc: func(args config.DefineArgs) js_ast.E { - return &js_ast.EIdentifier{Ref: args.FindSymbol(args.Loc, "replace")} + DefineExpr: &config.DefineExpr{ + Parts: []string{"replace"}, }, }, "obj.defined": { - DefineFunc: func(args config.DefineArgs) js_ast.E { - return &js_ast.EString{Value: helpers.StringToUTF16("defined")} + DefineExpr: &config.DefineExpr{ + Constant: &js_ast.EString{Value: helpers.StringToUTF16("defined")}, }, }, "injectedAndDefined": { - DefineFunc: func(args config.DefineArgs) js_ast.E { - return &js_ast.EString{Value: helpers.StringToUTF16("should be used")} + DefineExpr: &config.DefineExpr{ + Constant: &js_ast.EString{Value: helpers.StringToUTF16("should be used")}, }, }, }) @@ -4134,8 +4134,8 @@ func TestInjectNoBundle(t *testing.T) { func TestInjectJSX(t *testing.T) { defines := config.ProcessDefines(map[string]config.DefineData{ "React.createElement": { - DefineFunc: func(args config.DefineArgs) js_ast.E { - return &js_ast.EIdentifier{Ref: args.FindSymbol(args.Loc, "el")} + DefineExpr: &config.DefineExpr{ + Parts: []string{"el"}, }, }, }) @@ -4310,18 +4310,18 @@ func TestAvoidTDZNoBundle(t *testing.T) { func TestDefineImportMeta(t *testing.T) { defines := config.ProcessDefines(map[string]config.DefineData{ "import.meta": { - DefineFunc: func(args config.DefineArgs) js_ast.E { - return &js_ast.ENumber{Value: 1} + DefineExpr: &config.DefineExpr{ + Constant: &js_ast.ENumber{Value: 1}, }, }, "import.meta.foo": { - DefineFunc: func(args config.DefineArgs) js_ast.E { - return &js_ast.ENumber{Value: 2} + DefineExpr: &config.DefineExpr{ + Constant: &js_ast.ENumber{Value: 2}, }, }, "import.meta.foo.bar": { - DefineFunc: func(args config.DefineArgs) js_ast.E { - return &js_ast.ENumber{Value: 3} + DefineExpr: &config.DefineExpr{ + Constant: &js_ast.ENumber{Value: 3}, }, }, }) @@ -4354,8 +4354,8 @@ func TestDefineImportMeta(t *testing.T) { func TestDefineImportMetaES5(t *testing.T) { defines := config.ProcessDefines(map[string]config.DefineData{ "import.meta.x": { - DefineFunc: func(args config.DefineArgs) js_ast.E { - return &js_ast.ENumber{Value: 1} + DefineExpr: &config.DefineExpr{ + Constant: &js_ast.ENumber{Value: 1}, }, }, }) @@ -4391,18 +4391,18 @@ kept.js: WARNING: "import.meta" is not available in the configured target enviro 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} + DefineExpr: &config.DefineExpr{ + Constant: &js_ast.ENumber{Value: 1}, }, }, "this.foo": { - DefineFunc: func(args config.DefineArgs) js_ast.E { - return &js_ast.ENumber{Value: 2} + DefineExpr: &config.DefineExpr{ + Constant: &js_ast.ENumber{Value: 2}, }, }, "this.foo.bar": { - DefineFunc: func(args config.DefineArgs) js_ast.E { - return &js_ast.ENumber{Value: 3} + DefineExpr: &config.DefineExpr{ + Constant: &js_ast.ENumber{Value: 3}, }, }, }) @@ -4791,8 +4791,8 @@ func TestJSXThisValueCommonJS(t *testing.T) { options: config.Options{ Mode: config.ModeBundle, JSX: config.JSXOptions{ - Factory: config.JSXExpr{Parts: []string{"this"}}, - Fragment: config.JSXExpr{Parts: []string{"this"}}, + Factory: config.DefineExpr{Parts: []string{"this"}}, + Fragment: config.DefineExpr{Parts: []string{"this"}}, }, AbsOutputDir: "/out", }, @@ -4833,8 +4833,8 @@ func TestJSXThisValueESM(t *testing.T) { options: config.Options{ Mode: config.ModeBundle, JSX: config.JSXOptions{ - Factory: config.JSXExpr{Parts: []string{"this"}}, - Fragment: config.JSXExpr{Parts: []string{"this"}}, + Factory: config.DefineExpr{Parts: []string{"this"}}, + Fragment: config.DefineExpr{Parts: []string{"this"}}, }, AbsOutputDir: "/out", }, @@ -4878,8 +4878,8 @@ func TestJSXThisPropertyCommonJS(t *testing.T) { options: config.Options{ Mode: config.ModeBundle, JSX: config.JSXOptions{ - Factory: config.JSXExpr{Parts: []string{"this", "factory"}}, - Fragment: config.JSXExpr{Parts: []string{"this", "fragment"}}, + Factory: config.DefineExpr{Parts: []string{"this", "factory"}}, + Fragment: config.DefineExpr{Parts: []string{"this", "fragment"}}, }, AbsOutputDir: "/out", }, @@ -4920,8 +4920,8 @@ func TestJSXThisPropertyESM(t *testing.T) { options: config.Options{ Mode: config.ModeBundle, JSX: config.JSXOptions{ - Factory: config.JSXExpr{Parts: []string{"this", "factory"}}, - Fragment: config.JSXExpr{Parts: []string{"this", "fragment"}}, + Factory: config.DefineExpr{Parts: []string{"this", "factory"}}, + Fragment: config.DefineExpr{Parts: []string{"this", "fragment"}}, }, AbsOutputDir: "/out", }, @@ -4968,8 +4968,8 @@ func TestJSXImportMetaValue(t *testing.T) { Mode: config.ModeBundle, UnsupportedJSFeatures: compat.ImportMeta, JSX: config.JSXOptions{ - Factory: config.JSXExpr{Parts: []string{"import", "meta"}}, - Fragment: config.JSXExpr{Parts: []string{"import", "meta"}}, + Factory: config.DefineExpr{Parts: []string{"import", "meta"}}, + Fragment: config.DefineExpr{Parts: []string{"import", "meta"}}, }, AbsOutputDir: "/out", }, @@ -5018,8 +5018,8 @@ func TestJSXImportMetaProperty(t *testing.T) { Mode: config.ModeBundle, UnsupportedJSFeatures: compat.ImportMeta, JSX: config.JSXOptions{ - Factory: config.JSXExpr{Parts: []string{"import", "meta", "factory"}}, - Fragment: config.JSXExpr{Parts: []string{"import", "meta", "fragment"}}, + Factory: config.DefineExpr{Parts: []string{"import", "meta", "factory"}}, + Fragment: config.DefineExpr{Parts: []string{"import", "meta", "fragment"}}, }, AbsOutputDir: "/out", }, @@ -5978,8 +5978,8 @@ func TestManglePropsJSXTransform(t *testing.T) { AbsOutputFile: "/out.js", MangleProps: regexp.MustCompile("_$"), JSX: config.JSXOptions{ - Factory: config.JSXExpr{Parts: []string{"Foo", "createElement_"}}, - Fragment: config.JSXExpr{Parts: []string{"Foo", "Fragment_"}}, + Factory: config.DefineExpr{Parts: []string{"Foo", "createElement_"}}, + Fragment: config.DefineExpr{Parts: []string{"Foo", "Fragment_"}}, }, }, }) diff --git a/internal/bundler/bundler_ts_test.go b/internal/bundler/bundler_ts_test.go index 9440958e15f..97b5f63cd5b 100644 --- a/internal/bundler/bundler_ts_test.go +++ b/internal/bundler/bundler_ts_test.go @@ -5,7 +5,6 @@ import ( "github.com/evanw/esbuild/internal/compat" "github.com/evanw/esbuild/internal/config" - "github.com/evanw/esbuild/internal/js_ast" ) var ts_suite = suite{ @@ -1719,8 +1718,8 @@ func TestTSEnumDefine(t *testing.T) { Defines: &config.ProcessedDefines{ IdentifierDefines: map[string]config.DefineData{ "d": { - DefineFunc: func(args config.DefineArgs) js_ast.E { - return &js_ast.EIdentifier{Ref: args.FindSymbol(args.Loc, "b")} + DefineExpr: &config.DefineExpr{ + Parts: []string{"b"}, }, }, }, diff --git a/internal/config/config.go b/internal/config/config.go index c5ae0738fb4..29126d6fd33 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -13,17 +13,12 @@ import ( ) type JSXOptions struct { - Factory JSXExpr - Fragment JSXExpr + Factory DefineExpr + Fragment DefineExpr Parse bool Preserve bool } -type JSXExpr struct { - Constant js_ast.E - Parts []string -} - type TSOptions struct { Parse bool NoAmbiguousLessThan bool diff --git a/internal/config/globals.go b/internal/config/globals.go index a04379c02a0..de798d29ca7 100644 --- a/internal/config/globals.go +++ b/internal/config/globals.go @@ -5,9 +5,9 @@ import ( "strings" "sync" + "github.com/evanw/esbuild/internal/ast" "github.com/evanw/esbuild/internal/helpers" "github.com/evanw/esbuild/internal/js_ast" - "github.com/evanw/esbuild/internal/logger" ) var processedGlobalsMutex sync.Mutex @@ -842,16 +842,26 @@ var knownGlobals = [][]string{ {"window"}, } -type DefineArgs struct { - FindSymbol func(logger.Loc, string) js_ast.Ref - SymbolForDefine func(int) js_ast.Ref - Loc logger.Loc +// We currently only support compile-time replacement with certain expressions: +// +// - Primitive literals +// - Identifiers +// - "Entity names" which are identifiers followed by property accesses +// +// We don't support arbitrary expressions because arbitrary expressions may +// require the full AST. For example, there could be "import()" or "require()" +// expressions that need an import record. We also need to re-generate some +// nodes such as identifiers within the injected context so that they can +// bind to symbols in that context. Other expressions such as "this" may +// also be contextual. +type DefineExpr struct { + Constant js_ast.E + Parts []string + InjectedDefineIndex ast.Index32 } -type DefineFunc func(DefineArgs) js_ast.E - type DefineData struct { - DefineFunc DefineFunc + DefineExpr *DefineExpr // True if accessing this value is known to not have any side effects. For // example, a bare reference to "Object.create" can be removed because it @@ -928,13 +938,13 @@ func ProcessDefines(userDefines map[string]DefineData) ProcessedDefines { // Swap in certain literal values because those can be constant folded result.IdentifierDefines["undefined"] = DefineData{ - DefineFunc: func(DefineArgs) js_ast.E { return js_ast.EUndefinedShared }, + DefineExpr: &DefineExpr{Constant: js_ast.EUndefinedShared}, } result.IdentifierDefines["NaN"] = DefineData{ - DefineFunc: func(DefineArgs) js_ast.E { return &js_ast.ENumber{Value: math.NaN()} }, + DefineExpr: &DefineExpr{Constant: &js_ast.ENumber{Value: math.NaN()}}, } result.IdentifierDefines["Infinity"] = DefineData{ - DefineFunc: func(DefineArgs) js_ast.E { return &js_ast.ENumber{Value: math.Inf(1)} }, + DefineExpr: &DefineExpr{Constant: &js_ast.ENumber{Value: math.Inf(1)}}, } // Then copy the user-specified defines in afterwards, which will overwrite diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 2a364784159..efeaa4ff4e3 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -46,8 +46,6 @@ type parser struct { symbols []js_ast.Symbol isUnbound func(js_ast.Ref) bool tsUseCounts []uint32 - findSymbolHelper func(loc logger.Loc, name string) js_ast.Ref - symbolForDefineHelper func(int) js_ast.Ref injectedDefineSymbols []js_ast.Ref injectedSymbolSources map[js_ast.Ref]injectedSymbolSource mangledProps map[string]js_ast.Ref @@ -499,7 +497,7 @@ func isSameRegexp(a *regexp.Regexp, b *regexp.Regexp) bool { } } -func jsxExprsEqual(a config.JSXExpr, b config.JSXExpr) bool { +func jsxExprsEqual(a config.DefineExpr, b config.DefineExpr) bool { if !helpers.StringArraysEqual(a.Parts, b.Parts) { return false } @@ -10658,7 +10656,22 @@ func (p *parser) isDotOrIndexDefineMatch(expr js_ast.Expr, parts []string) bool return false } -func (p *parser) jsxStringsToMemberExpression(loc logger.Loc, parts []string) js_ast.Expr { +func (p *parser) instantiateDefineExpr(loc logger.Loc, expr config.DefineExpr, opts identifierOpts) js_ast.Expr { + if expr.Constant != nil { + return js_ast.Expr{Loc: loc, Data: expr.Constant} + } + + if expr.InjectedDefineIndex.IsValid() { + ref := p.injectedDefineSymbols[expr.InjectedDefineIndex.GetIndex()] + p.recordUsage(ref) + return js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: ref}} + } + + parts := expr.Parts + if len(parts) == 0 { + return js_ast.Expr{} + } + // Check both user-specified defines and known globals if defines, ok := p.options.defines.DotDefines[parts[len(parts)-1]]; ok { next: @@ -10672,8 +10685,8 @@ func (p *parser) jsxStringsToMemberExpression(loc logger.Loc, parts []string) js } // Substitute user-specified defines - if define.Data.DefineFunc != nil { - return p.valueForDefine(loc, define.Data.DefineFunc, identifierOpts{}) + if define.Data.DefineExpr != nil { + return p.instantiateDefineExpr(loc, *define.Data.DefineExpr, opts) } } } @@ -10681,8 +10694,14 @@ func (p *parser) jsxStringsToMemberExpression(loc logger.Loc, parts []string) js // Generate an identifier for the first part var value js_ast.Expr firstPart := parts[0] - nextPart := 1 + parts = parts[1:] switch firstPart { + case "NaN": + value = js_ast.Expr{Loc: loc, Data: &js_ast.ENumber{Value: math.NaN()}} + + case "Infinity": + value = js_ast.Expr{Loc: loc, Data: &js_ast.ENumber{Value: math.Inf(1)}} + case "null": value = js_ast.Expr{Loc: loc, Data: js_ast.ENullShared} @@ -10697,12 +10716,14 @@ func (p *parser) jsxStringsToMemberExpression(loc logger.Loc, parts []string) js } default: - if firstPart == "import" && len(parts) > 1 && parts[1] == "meta" { + if firstPart == "import" && len(parts) > 0 && parts[0] == "meta" { if importMeta, ok := p.valueForImportMeta(loc); ok { value = importMeta - nextPart = 2 - break + } else { + value = js_ast.Expr{Loc: loc, Data: &js_ast.EImportMeta{}} } + parts = parts[1:] + break } result := p.findSymbol(loc, firstPart) @@ -10712,13 +10733,11 @@ func (p *parser) jsxStringsToMemberExpression(loc logger.Loc, parts []string) js // Enable tree shaking CanBeRemovedIfUnused: true, - }, identifierOpts{ - wasOriginallyIdentifier: true, - }) + }, opts) } // Build up a chain of property access expressions for subsequent parts - for _, part := range parts[nextPart:] { + for _, part := range parts { if expr, ok := p.maybeRewritePropertyAccess(loc, js_ast.AssignTargetNone, false, value, part, loc, false, false); ok { value = expr } else if p.isMangledProp(part) { @@ -11322,8 +11341,8 @@ func (p *parser) valueForThis( 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{ + if data.DefineExpr != nil { + return p.instantiateDefineExpr(loc, *data.DefineExpr, identifierOpts{ assignTarget: assignTarget, isCallTarget: isCallTarget, isDeleteTarget: isDeleteTarget, @@ -11774,8 +11793,8 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO for _, define := range defines { if p.isDotOrIndexDefineMatch(expr, define.Parts) { // Substitute user-specified defines - if define.Data.DefineFunc != nil { - return p.valueForDefine(expr.Loc, define.Data.DefineFunc, identifierOpts{ + if define.Data.DefineExpr != nil { + return p.instantiateDefineExpr(expr.Loc, *define.Data.DefineExpr, identifierOpts{ assignTarget: in.assignTarget, isCallTarget: isCallTarget, isDeleteTarget: isDeleteTarget, @@ -11846,8 +11865,8 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO 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 { - new := p.valueForDefine(expr.Loc, data.DefineFunc, identifierOpts{ + if data.DefineExpr != nil { + new := p.instantiateDefineExpr(expr.Loc, *data.DefineExpr, identifierOpts{ assignTarget: in.assignTarget, isCallTarget: isCallTarget, isDeleteTarget: isDeleteTarget, @@ -11928,13 +11947,9 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO } else { // A missing tag is a fragment if e.TagOrNil.Data == nil { - var value js_ast.Expr - if len(p.options.jsx.Fragment.Parts) > 0 { - value = p.jsxStringsToMemberExpression(expr.Loc, p.options.jsx.Fragment.Parts) - } else if constant := p.options.jsx.Fragment.Constant; constant != nil { - value = js_ast.Expr{Loc: expr.Loc, Data: constant} - } - e.TagOrNil = value + e.TagOrNil = p.instantiateDefineExpr(expr.Loc, p.options.jsx.Fragment, identifierOpts{ + wasOriginallyIdentifier: true, + }) } // Arguments to createElement() @@ -11951,7 +11966,9 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO } // Call createElement() - target := p.jsxStringsToMemberExpression(expr.Loc, p.options.jsx.Factory.Parts) + target := p.instantiateDefineExpr(expr.Loc, p.options.jsx.Factory, identifierOpts{ + wasOriginallyIdentifier: true, + }) p.warnAboutImportNamespaceCall(target, exprKindCall) return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ECall{ Target: target, @@ -12595,8 +12612,8 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO for _, define := range defines { if p.isDotOrIndexDefineMatch(expr, define.Parts) { // Substitute user-specified defines - if define.Data.DefineFunc != nil { - return p.valueForDefine(expr.Loc, define.Data.DefineFunc, identifierOpts{ + if define.Data.DefineExpr != nil { + return p.instantiateDefineExpr(expr.Loc, *define.Data.DefineExpr, identifierOpts{ assignTarget: in.assignTarget, isCallTarget: isCallTarget, isDeleteTarget: isDeleteTarget, @@ -12682,8 +12699,8 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO for _, define := range defines { if p.isDotOrIndexDefineMatch(expr, define.Parts) { // Substitute user-specified defines - if define.Data.DefineFunc != nil { - return p.valueForDefine(expr.Loc, define.Data.DefineFunc, identifierOpts{ + if define.Data.DefineExpr != nil { + return p.instantiateDefineExpr(expr.Loc, *define.Data.DefineExpr, identifierOpts{ assignTarget: in.assignTarget, isCallTarget: isCallTarget, isDeleteTarget: isDeleteTarget, @@ -14111,19 +14128,6 @@ func (p *parser) maybeMarkKnownGlobalConstructorAsPure(e *js_ast.ENew) { } } -func (p *parser) valueForDefine(loc logger.Loc, defineFunc config.DefineFunc, opts identifierOpts) js_ast.Expr { - expr := js_ast.Expr{Loc: loc, Data: defineFunc(config.DefineArgs{ - Loc: loc, - FindSymbol: p.findSymbolHelper, - SymbolForDefine: p.symbolForDefineHelper, - })} - if id, ok := expr.Data.(*js_ast.EIdentifier); ok { - opts.wasOriginallyIdentifier = true - return p.handleIdentifier(loc, id, opts) - } - return expr -} - type identifierOpts struct { assignTarget js_ast.AssignTarget isCallTarget bool @@ -15193,16 +15197,6 @@ func newParser(log logger.Log, source logger.Source, lexer js_lexer.Lexer, optio return p.symbols[ref.InnerIndex].Kind == js_ast.SymbolUnbound } - p.findSymbolHelper = func(loc logger.Loc, name string) js_ast.Ref { - return p.findSymbol(loc, name).ref - } - - p.symbolForDefineHelper = func(index int) js_ast.Ref { - ref := p.injectedDefineSymbols[index] - p.recordUsage(ref) - return ref - } - p.pushScopeForParsePass(js_ast.ScopeEntry, logger.Loc{Start: locModuleScope}) return p @@ -15224,10 +15218,10 @@ func Parse(log logger.Log, source logger.Source, options Options) (result js_ast // Default options for JSX elements if len(options.jsx.Factory.Parts) == 0 { - options.jsx.Factory = config.JSXExpr{Parts: defaultJSXFactory} + options.jsx.Factory = config.DefineExpr{Parts: defaultJSXFactory} } if len(options.jsx.Fragment.Parts) == 0 && options.jsx.Fragment.Constant == nil { - options.jsx.Fragment = config.JSXExpr{Parts: defaultJSXFragment} + options.jsx.Fragment = config.DefineExpr{Parts: defaultJSXFragment} } if !options.ts.Parse { @@ -15458,39 +15452,47 @@ const ( JSXFragment ) -func ParseJSXExpr(text string, kind JSXExprKind) (config.JSXExpr, bool) { +func ParseDefineExprOrJSON(text string) (config.DefineExpr, js_ast.E) { if text == "" { - return config.JSXExpr{}, true + return config.DefineExpr{}, nil } // Try a property chain parts := strings.Split(text, ".") - for _, part := range parts { + for i, part := range parts { if !js_lexer.IsIdentifier(part) { parts = nil break } + + // Don't allow most keywords as the identifier + if i == 0 { + if token, ok := js_lexer.Keywords[part]; ok && token != js_lexer.TNull && token != js_lexer.TThis && + (token != js_lexer.TImport || len(parts) < 2 || parts[1] != "meta") { + parts = nil + break + } + } } if parts != nil { - return config.JSXExpr{Parts: parts}, true + return config.DefineExpr{Parts: parts}, nil } - if kind == JSXFragment { - // Try a JSON value - source := logger.Source{Contents: text} - expr, ok := ParseJSON(logger.NewDeferLog(logger.DeferLogNoVerboseOrDebug, nil), source, JSONOptions{}) - if !ok { - return config.JSXExpr{}, false - } + // Try parsing a JSON value + log := logger.NewDeferLog(logger.DeferLogNoVerboseOrDebug, nil) + expr, ok := ParseJSON(log, logger.Source{Contents: text}, JSONOptions{}) + if !ok { + return config.DefineExpr{}, nil + } - // Only primitives are supported for now - switch expr.Data.(type) { - case *js_ast.ENull, *js_ast.EBoolean, *js_ast.EString, *js_ast.ENumber: - return config.JSXExpr{Constant: expr.Data}, true - } + // Only primitive literals are inlined directly + switch expr.Data.(type) { + case *js_ast.ENull, *js_ast.EBoolean, *js_ast.EString, *js_ast.ENumber: + return config.DefineExpr{Constant: expr.Data}, nil } - return config.JSXExpr{}, false + // If it's not a primitive, return the whole compound JSON value to be injected out-of-line + return config.DefineExpr{}, expr.Data } // Say why this the current file is being considered an ES module @@ -15585,17 +15587,21 @@ func (p *parser) prepareForVisitPass() { // Handle "@jsx" and "@jsxFrag" pragmas now that lexing is done if p.options.jsx.Parse { - if expr, ok := ParseJSXExpr(p.lexer.JSXFactoryPragmaComment.Text, JSXFactory); !ok { - p.log.AddID(logger.MsgID_JS_UnsupportedJSXComment, logger.Warning, &p.tracker, p.lexer.JSXFactoryPragmaComment.Range, - fmt.Sprintf("Invalid JSX factory: %s", p.lexer.JSXFactoryPragmaComment.Text)) - } else if len(expr.Parts) > 0 { - p.options.jsx.Factory = expr - } - if expr, ok := ParseJSXExpr(p.lexer.JSXFragmentPragmaComment.Text, JSXFragment); !ok { - p.log.AddID(logger.MsgID_JS_UnsupportedJSXComment, logger.Warning, &p.tracker, p.lexer.JSXFragmentPragmaComment.Range, - fmt.Sprintf("Invalid JSX fragment: %s", p.lexer.JSXFragmentPragmaComment.Text)) - } else if len(expr.Parts) > 0 || expr.Constant != nil { - p.options.jsx.Fragment = expr + if jsxFactory := p.lexer.JSXFactoryPragmaComment; jsxFactory.Text != "" { + if expr, _ := ParseDefineExprOrJSON(jsxFactory.Text); len(expr.Parts) > 0 { + p.options.jsx.Factory = expr + } else { + p.log.AddID(logger.MsgID_JS_UnsupportedJSXComment, logger.Warning, &p.tracker, jsxFactory.Range, + fmt.Sprintf("Invalid JSX factory: %s", jsxFactory.Text)) + } + } + if jsxFragment := p.lexer.JSXFragmentPragmaComment; jsxFragment.Text != "" { + if expr, _ := ParseDefineExprOrJSON(jsxFragment.Text); len(expr.Parts) > 0 || expr.Constant != nil { + p.options.jsx.Fragment = expr + } else { + p.log.AddID(logger.MsgID_JS_UnsupportedJSXComment, logger.Warning, &p.tracker, jsxFragment.Range, + fmt.Sprintf("Invalid JSX fragment: %s", jsxFragment.Text)) + } } } } diff --git a/pkg/api/api_impl.go b/pkg/api/api_impl.go index 4cb546bef2e..77e8cb44e87 100644 --- a/pkg/api/api_impl.go +++ b/pkg/api/api_impl.go @@ -468,12 +468,14 @@ func validateLoaders(log logger.Log, loaders map[string]Loader) map[string]confi return result } -func validateJSXExpr(log logger.Log, text string, name string, kind js_parser.JSXExprKind) config.JSXExpr { - if expr, ok := js_parser.ParseJSXExpr(text, kind); ok { - return expr +func validateJSXExpr(log logger.Log, text string, name string) config.DefineExpr { + if text != "" { + if expr, _ := js_parser.ParseDefineExprOrJSON(text); len(expr.Parts) > 0 || (name == "fragment" && expr.Constant != nil) { + return expr + } + log.AddError(nil, logger.Range{}, fmt.Sprintf("Invalid JSX %s: %q", name, text)) } - log.AddError(nil, logger.Range{}, fmt.Sprintf("Invalid JSX %s: %q", name, text)) - return config.JSXExpr{} + return config.DefineExpr{} } func validateDefines( @@ -501,65 +503,31 @@ func validateDefines( } } - // Allow substituting for an identifier - if js_lexer.IsIdentifier(value) { - if _, ok := js_lexer.Keywords[value]; !ok { - switch value { - case "undefined": - rawDefines[key] = config.DefineData{ - DefineFunc: func(config.DefineArgs) js_ast.E { return js_ast.EUndefinedShared }, - } - case "NaN": - rawDefines[key] = config.DefineData{ - DefineFunc: func(config.DefineArgs) js_ast.E { return &js_ast.ENumber{Value: math.NaN()} }, - } - case "Infinity": - rawDefines[key] = config.DefineData{ - DefineFunc: func(config.DefineArgs) js_ast.E { return &js_ast.ENumber{Value: math.Inf(1)} }, - } - default: - name := value // The closure must close over a variable inside the loop - rawDefines[key] = config.DefineData{ - DefineFunc: func(args config.DefineArgs) js_ast.E { - return &js_ast.EIdentifier{Ref: args.FindSymbol(args.Loc, name)} - }, - } - } - continue - } - } + // Parse the value + defineExpr, injectExpr := js_parser.ParseDefineExprOrJSON(value) - // Parse the value as JSON - source := logger.Source{Contents: value} - expr, ok := js_parser.ParseJSON(logger.NewDeferLog(logger.DeferLogAll, nil), source, js_parser.JSONOptions{}) - if !ok { - log.AddError(nil, logger.Range{}, fmt.Sprintf("Invalid define value (must be valid JSON syntax or a single identifier): %s", value)) + // Define simple expressions + if defineExpr.Constant != nil || len(defineExpr.Parts) > 0 { + rawDefines[key] = config.DefineData{DefineExpr: &defineExpr} continue } - var fn config.DefineFunc - switch e := expr.Data.(type) { - // These values are inserted inline, and can participate in constant folding - case *js_ast.ENull: - fn = func(config.DefineArgs) js_ast.E { return js_ast.ENullShared } - case *js_ast.EBoolean: - fn = func(config.DefineArgs) js_ast.E { return &js_ast.EBoolean{Value: e.Value} } - case *js_ast.EString: - fn = func(config.DefineArgs) js_ast.E { return &js_ast.EString{Value: e.Value} } - case *js_ast.ENumber: - fn = func(config.DefineArgs) js_ast.E { return &js_ast.ENumber{Value: e.Value} } - - // These values are extracted into a shared symbol reference - case *js_ast.EArray, *js_ast.EObject: + // Inject complex expressions + if injectExpr != nil { definesToInject = append(definesToInject, key) if valueToInject == nil { valueToInject = make(map[string]config.InjectedDefine) } - valueToInject[key] = config.InjectedDefine{Source: source, Data: e, Name: key} + valueToInject[key] = config.InjectedDefine{ + Source: logger.Source{Contents: value}, + Data: injectExpr, + Name: key, + } continue } - rawDefines[key] = config.DefineData{DefineFunc: fn} + // Anything else is unsupported + log.AddError(nil, logger.Range{}, fmt.Sprintf("Invalid define value (must be an entity name or valid JSON syntax): %s", value)) } // Sort injected defines for determinism, since the imports will be injected @@ -569,11 +537,8 @@ func validateDefines( injectedDefines = make([]config.InjectedDefine, len(definesToInject)) sort.Strings(definesToInject) for i, key := range definesToInject { - index := i // Capture this for the closure below injectedDefines[i] = valueToInject[key] - rawDefines[key] = config.DefineData{DefineFunc: func(args config.DefineArgs) js_ast.E { - return &js_ast.EIdentifier{Ref: args.SymbolForDefine(index)} - }} + rawDefines[key] = config.DefineData{DefineExpr: &config.DefineExpr{InjectedDefineIndex: ast.MakeIndex32(uint32(i))}} } } @@ -593,11 +558,7 @@ func validateDefines( } else { value = helpers.StringToUTF16("development") } - rawDefines["process.env.NODE_ENV"] = config.DefineData{ - DefineFunc: func(args config.DefineArgs) js_ast.E { - return &js_ast.EString{Value: value} - }, - } + rawDefines["process.env.NODE_ENV"] = config.DefineData{DefineExpr: &config.DefineExpr{Constant: &js_ast.EString{Value: value}}} } } } @@ -922,8 +883,8 @@ func rebuildImpl( OriginalTargetEnv: targetEnv, JSX: config.JSXOptions{ Preserve: buildOpts.JSXMode == JSXModePreserve, - Factory: validateJSXExpr(log, buildOpts.JSXFactory, "factory", js_parser.JSXFactory), - Fragment: validateJSXExpr(log, buildOpts.JSXFragment, "fragment", js_parser.JSXFragment), + Factory: validateJSXExpr(log, buildOpts.JSXFactory, "factory"), + Fragment: validateJSXExpr(log, buildOpts.JSXFragment, "fragment"), }, Defines: defines, InjectedDefines: injectedDefines, @@ -1381,8 +1342,8 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult useDefineForClassFieldsTS := config.Unspecified jsx := config.JSXOptions{ Preserve: transformOpts.JSXMode == JSXModePreserve, - Factory: validateJSXExpr(log, transformOpts.JSXFactory, "factory", js_parser.JSXFactory), - Fragment: validateJSXExpr(log, transformOpts.JSXFragment, "fragment", js_parser.JSXFragment), + Factory: validateJSXExpr(log, transformOpts.JSXFactory, "factory"), + Fragment: validateJSXExpr(log, transformOpts.JSXFragment, "fragment"), } // Settings from "tsconfig.json" override those @@ -1397,10 +1358,10 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult } if result := resolver.ParseTSConfigJSON(log, source, &caches.JSONCache, nil); result != nil { if len(result.JSXFactory) > 0 { - jsx.Factory = config.JSXExpr{Parts: result.JSXFactory} + jsx.Factory = config.DefineExpr{Parts: result.JSXFactory} } if len(result.JSXFragmentFactory) > 0 { - jsx.Fragment = config.JSXExpr{Parts: result.JSXFragmentFactory} + jsx.Fragment = config.DefineExpr{Parts: result.JSXFragmentFactory} } if result.UseDefineForClassFields != config.Unspecified { useDefineForClassFieldsTS = result.UseDefineForClassFields diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js index bbd904af4e8..027b2f785ba 100644 --- a/scripts/js-api-tests.js +++ b/scripts/js-api-tests.js @@ -3887,9 +3887,9 @@ let transformTests = { }, async defineBuiltInConstants({ esbuild }) { - const define = { a: 'NaN', b: 'Infinity', c: 'undefined', d: 'something' } - const { code } = await esbuild.transform(`console.log([typeof a, typeof b, typeof c, typeof d])`, { define }) - assert.strictEqual(code, `console.log(["number", "number", "undefined", typeof something]);\n`) + const define = { a: 'NaN', b: 'Infinity', c: 'undefined', d: 'something', e: 'null' } + const { code } = await esbuild.transform(`console.log([typeof a, typeof b, typeof c, typeof d, typeof e])`, { define }) + assert.strictEqual(code, `console.log(["number", "number", "undefined", typeof something, "object"]);\n`) }, async defineArray({ esbuild }) { @@ -3898,6 +3898,21 @@ let transformTests = { assert.strictEqual(code, `var define_process_env_NODE_ENV_default = [1, 2, 3];\nconsole.log(define_process_env_NODE_ENV_default);\n`) }, + async defineThis({ esbuild }) { + const { code } = await esbuild.transform(`console.log(a, b); export {}`, { define: { a: 'this', b: 'this.foo' }, format: 'esm' }) + assert.strictEqual(code, `console.log(void 0, (void 0).foo);\n`) + }, + + async defineImportMetaESM({ esbuild }) { + const { code } = await esbuild.transform(`console.log(a, b); export {}`, { define: { a: 'import.meta', b: 'import.meta.foo' }, format: 'esm' }) + assert.strictEqual(code, `console.log(import.meta, import.meta.foo);\n`) + }, + + async defineImportMetaIIFE({ esbuild }) { + const { code } = await esbuild.transform(`console.log(a, b); export {}`, { define: { a: 'import.meta', b: 'import.meta.foo' }, format: 'iife' }) + assert.strictEqual(code, `(() => {\n const import_meta = {};\n console.log(import_meta, import_meta.foo);\n})();\n`) + }, + async json({ esbuild }) { const { code } = await esbuild.transform(`{ "x": "y" }`, { loader: 'json' }) assert.strictEqual(code, `module.exports = { x: "y" };\n`)