268 lines
7.4 KiB
Go
268 lines
7.4 KiB
Go
package config
|
|
|
|
import (
|
|
"math"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/evanw/esbuild/internal/js_ast"
|
|
"github.com/evanw/esbuild/internal/logger"
|
|
)
|
|
|
|
var processedGlobalsMutex sync.Mutex
|
|
var processedGlobals *ProcessedDefines
|
|
|
|
var knownGlobals = [][]string{
|
|
// These global identifiers should exist in all JavaScript environments
|
|
{"Array"},
|
|
{"Boolean"},
|
|
{"Function"},
|
|
{"Math"},
|
|
{"Number"},
|
|
{"Object"},
|
|
{"RegExp"},
|
|
{"String"},
|
|
|
|
// Object: Static methods
|
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object#Static_methods
|
|
{"Object", "assign"},
|
|
{"Object", "create"},
|
|
{"Object", "defineProperties"},
|
|
{"Object", "defineProperty"},
|
|
{"Object", "entries"},
|
|
{"Object", "freeze"},
|
|
{"Object", "fromEntries"},
|
|
{"Object", "getOwnPropertyDescriptor"},
|
|
{"Object", "getOwnPropertyDescriptors"},
|
|
{"Object", "getOwnPropertyNames"},
|
|
{"Object", "getOwnPropertySymbols"},
|
|
{"Object", "getPrototypeOf"},
|
|
{"Object", "is"},
|
|
{"Object", "isExtensible"},
|
|
{"Object", "isFrozen"},
|
|
{"Object", "isSealed"},
|
|
{"Object", "keys"},
|
|
{"Object", "preventExtensions"},
|
|
{"Object", "seal"},
|
|
{"Object", "setPrototypeOf"},
|
|
{"Object", "values"},
|
|
|
|
// Object: Instance methods
|
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object#Instance_methods
|
|
{"Object", "prototype", "__defineGetter__"},
|
|
{"Object", "prototype", "__defineSetter__"},
|
|
{"Object", "prototype", "__lookupGetter__"},
|
|
{"Object", "prototype", "__lookupSetter__"},
|
|
{"Object", "prototype", "hasOwnProperty"},
|
|
{"Object", "prototype", "isPrototypeOf"},
|
|
{"Object", "prototype", "propertyIsEnumerable"},
|
|
{"Object", "prototype", "toLocaleString"},
|
|
{"Object", "prototype", "toString"},
|
|
{"Object", "prototype", "unwatch"},
|
|
{"Object", "prototype", "valueOf"},
|
|
{"Object", "prototype", "watch"},
|
|
|
|
// Math: Static properties
|
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math#Static_properties
|
|
{"Math", "E"},
|
|
{"Math", "LN10"},
|
|
{"Math", "LN2"},
|
|
{"Math", "LOG10E"},
|
|
{"Math", "LOG2E"},
|
|
{"Math", "PI"},
|
|
{"Math", "SQRT1_2"},
|
|
{"Math", "SQRT2"},
|
|
|
|
// Math: Static methods
|
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math#Static_methods
|
|
{"Math", "abs"},
|
|
{"Math", "acos"},
|
|
{"Math", "acosh"},
|
|
{"Math", "asin"},
|
|
{"Math", "asinh"},
|
|
{"Math", "atan"},
|
|
{"Math", "atan2"},
|
|
{"Math", "atanh"},
|
|
{"Math", "cbrt"},
|
|
{"Math", "ceil"},
|
|
{"Math", "clz32"},
|
|
{"Math", "cos"},
|
|
{"Math", "cosh"},
|
|
{"Math", "exp"},
|
|
{"Math", "expm1"},
|
|
{"Math", "floor"},
|
|
{"Math", "fround"},
|
|
{"Math", "hypot"},
|
|
{"Math", "imul"},
|
|
{"Math", "log"},
|
|
{"Math", "log10"},
|
|
{"Math", "log1p"},
|
|
{"Math", "log2"},
|
|
{"Math", "max"},
|
|
{"Math", "min"},
|
|
{"Math", "pow"},
|
|
{"Math", "random"},
|
|
{"Math", "round"},
|
|
{"Math", "sign"},
|
|
{"Math", "sin"},
|
|
{"Math", "sinh"},
|
|
{"Math", "sqrt"},
|
|
{"Math", "tan"},
|
|
{"Math", "tanh"},
|
|
{"Math", "trunc"},
|
|
}
|
|
|
|
type DefineArgs struct {
|
|
Loc logger.Loc
|
|
FindSymbol func(logger.Loc, string) js_ast.Ref
|
|
SymbolForDefine func(int) js_ast.Ref
|
|
}
|
|
|
|
type DefineFunc func(DefineArgs) js_ast.E
|
|
|
|
type DefineData struct {
|
|
DefineFunc DefineFunc
|
|
|
|
// 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
|
|
// does not have any observable side effects.
|
|
CanBeRemovedIfUnused bool
|
|
|
|
// True if a call to this value is known to not have any side effects. For
|
|
// example, a bare call to "Object()" can be removed because it does not
|
|
// have any observable side effects.
|
|
CallCanBeUnwrappedIfUnused bool
|
|
|
|
// Set to true to warn users that this should be defined if it's not already.
|
|
WarnAboutLackOfDefine bool
|
|
}
|
|
|
|
func mergeDefineData(old DefineData, new DefineData) DefineData {
|
|
if old.CanBeRemovedIfUnused {
|
|
new.CanBeRemovedIfUnused = true
|
|
}
|
|
if old.CallCanBeUnwrappedIfUnused {
|
|
new.CallCanBeUnwrappedIfUnused = true
|
|
}
|
|
|
|
// Don't warn if the user defined this
|
|
new.WarnAboutLackOfDefine = false
|
|
return new
|
|
}
|
|
|
|
type DotDefine struct {
|
|
Parts []string
|
|
Data DefineData
|
|
}
|
|
|
|
type ProcessedDefines struct {
|
|
IdentifierDefines map[string]DefineData
|
|
DotDefines map[string][]DotDefine
|
|
}
|
|
|
|
// This transformation is expensive, so we only want to do it once. Make sure
|
|
// to only call processDefines() once per compilation. Unfortunately Golang
|
|
// doesn't have an efficient way to copy a map and the overhead of copying
|
|
// all of the properties into a new map once for every new parser noticeably
|
|
// slows down our benchmarks.
|
|
func ProcessDefines(userDefines map[string]DefineData) ProcessedDefines {
|
|
// Optimization: reuse known globals if there are no user-specified defines
|
|
hasUserDefines := len(userDefines) != 0
|
|
if !hasUserDefines {
|
|
processedGlobalsMutex.Lock()
|
|
if processedGlobals != nil {
|
|
defer processedGlobalsMutex.Unlock()
|
|
return *processedGlobals
|
|
}
|
|
processedGlobalsMutex.Unlock()
|
|
}
|
|
|
|
result := ProcessedDefines{
|
|
IdentifierDefines: make(map[string]DefineData),
|
|
DotDefines: make(map[string][]DotDefine),
|
|
}
|
|
|
|
// Mark these property accesses as free of side effects. That means they can
|
|
// be removed if their result is unused. We can't just remove all unused
|
|
// property accesses since property accesses can have side effects. For
|
|
// example, the property access "a.b.c" has the side effect of throwing an
|
|
// exception if "a.b" is undefined.
|
|
for _, parts := range knownGlobals {
|
|
tail := parts[len(parts)-1]
|
|
if len(parts) == 1 {
|
|
result.IdentifierDefines[tail] = DefineData{CanBeRemovedIfUnused: true}
|
|
} else {
|
|
result.DotDefines[tail] = append(result.DotDefines[tail], DotDefine{Parts: parts, Data: DefineData{CanBeRemovedIfUnused: true}})
|
|
}
|
|
}
|
|
|
|
// Swap in certain literal values because those can be constant folded
|
|
result.IdentifierDefines["undefined"] = DefineData{
|
|
DefineFunc: func(DefineArgs) js_ast.E { return &js_ast.EUndefined{} },
|
|
}
|
|
result.IdentifierDefines["NaN"] = DefineData{
|
|
DefineFunc: func(DefineArgs) js_ast.E { return &js_ast.ENumber{Value: math.NaN()} },
|
|
}
|
|
result.IdentifierDefines["Infinity"] = DefineData{
|
|
DefineFunc: func(DefineArgs) js_ast.E { return &js_ast.ENumber{Value: math.Inf(1)} },
|
|
}
|
|
|
|
// Warn about use of this without a define
|
|
result.DotDefines["NODE_ENV"] = []DotDefine{{
|
|
Parts: []string{"process", "env", "NODE_ENV"},
|
|
Data: DefineData{WarnAboutLackOfDefine: true},
|
|
}}
|
|
|
|
// Then copy the user-specified defines in afterwards, which will overwrite
|
|
// any known globals above.
|
|
for key, data := range userDefines {
|
|
parts := strings.Split(key, ".")
|
|
|
|
// Identifier defines are special-cased
|
|
if len(parts) == 1 {
|
|
result.IdentifierDefines[key] = mergeDefineData(result.IdentifierDefines[key], data)
|
|
continue
|
|
}
|
|
|
|
tail := parts[len(parts)-1]
|
|
dotDefines := result.DotDefines[tail]
|
|
found := false
|
|
|
|
// Try to merge with existing dot defines first
|
|
for i, define := range dotDefines {
|
|
if arePartsEqual(parts, define.Parts) {
|
|
define := &dotDefines[i]
|
|
define.Data = mergeDefineData(define.Data, data)
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
dotDefines = append(dotDefines, DotDefine{Parts: parts, Data: data})
|
|
}
|
|
result.DotDefines[tail] = dotDefines
|
|
}
|
|
|
|
// Potentially cache the result for next time
|
|
if !hasUserDefines {
|
|
processedGlobalsMutex.Lock()
|
|
defer processedGlobalsMutex.Unlock()
|
|
if processedGlobals == nil {
|
|
processedGlobals = &result
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func arePartsEqual(a []string, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|