package js_printer import ( "bytes" "fmt" "math" "strconv" "strings" "unicode/utf8" "github.com/evanw/esbuild/internal/ast" "github.com/evanw/esbuild/internal/compat" "github.com/evanw/esbuild/internal/config" "github.com/evanw/esbuild/internal/js_ast" "github.com/evanw/esbuild/internal/js_lexer" "github.com/evanw/esbuild/internal/logger" "github.com/evanw/esbuild/internal/renamer" "github.com/evanw/esbuild/internal/sourcemap" ) var positiveInfinity = math.Inf(1) var negativeInfinity = math.Inf(-1) const hexChars = "0123456789ABCDEF" const firstASCII = 0x20 const lastASCII = 0x7E const firstHighSurrogate = 0xD800 const lastHighSurrogate = 0xDBFF const firstLowSurrogate = 0xDC00 const lastLowSurrogate = 0xDFFF func canPrintWithoutEscape(c rune, asciiOnly bool) bool { if c <= lastASCII { return c >= firstASCII && c != '\\' && c != '"' } else { return !asciiOnly && c != '\uFEFF' && (c < firstHighSurrogate || c > lastLowSurrogate) } } func QuoteForJSON(text string, asciiOnly bool) []byte { // Estimate the required length lenEstimate := 2 for _, c := range text { if canPrintWithoutEscape(c, asciiOnly) { lenEstimate += utf8.RuneLen(c) } else { switch c { case '\b', '\f', '\n', '\r', '\t', '\\', '"': lenEstimate += 2 default: if c <= 0xFFFF { lenEstimate += 6 } else { lenEstimate += 12 } } } } // Preallocate the array bytes := make([]byte, 0, lenEstimate) i := 0 n := len(text) bytes = append(bytes, '"') for i < n { c, width := js_lexer.DecodeWTF8Rune(text[i:]) // Fast path: a run of characters that don't need escaping if canPrintWithoutEscape(c, asciiOnly) { start := i i += width for i < n { c, width = js_lexer.DecodeWTF8Rune(text[i:]) if !canPrintWithoutEscape(c, asciiOnly) { break } i += width } bytes = append(bytes, text[start:i]...) continue } switch c { case '\b': bytes = append(bytes, "\\b"...) i++ case '\f': bytes = append(bytes, "\\f"...) i++ case '\n': bytes = append(bytes, "\\n"...) i++ case '\r': bytes = append(bytes, "\\r"...) i++ case '\t': bytes = append(bytes, "\\t"...) i++ case '\\': bytes = append(bytes, "\\\\"...) i++ case '"': bytes = append(bytes, "\\\""...) i++ default: i += width if c <= 0xFFFF { bytes = append( bytes, '\\', 'u', hexChars[c>>12], hexChars[(c>>8)&15], hexChars[(c>>4)&15], hexChars[c&15], ) } else { c -= 0x10000 lo := firstHighSurrogate + ((c >> 10) & 0x3FF) hi := firstLowSurrogate + (c & 0x3FF) bytes = append( bytes, '\\', 'u', hexChars[lo>>12], hexChars[(lo>>8)&15], hexChars[(lo>>4)&15], hexChars[lo&15], '\\', 'u', hexChars[hi>>12], hexChars[(hi>>8)&15], hexChars[(hi>>4)&15], hexChars[hi&15], ) } } } return append(bytes, '"') } func QuoteIdentifier(js []byte, name string, unsupportedFeatures compat.JSFeature) []byte { isASCII := false asciiStart := 0 for i, c := range name { if c >= firstASCII && c <= lastASCII { // Fast path: a run of ASCII characters if !isASCII { isASCII = true asciiStart = i } } else { // Slow path: escape non-ACSII characters if isASCII { js = append(js, name[asciiStart:i]...) isASCII = false } if c <= 0xFFFF { js = append(js, '\\', 'u', hexChars[c>>12], hexChars[(c>>8)&15], hexChars[(c>>4)&15], hexChars[c&15]) } else if !unsupportedFeatures.Has(compat.UnicodeEscapes) { js = append(js, fmt.Sprintf("\\u{%X}", c)...) } else { panic("Internal error: Cannot encode identifier: Unicode escapes are unsupported") } } } if isASCII { // Print one final run of ASCII characters js = append(js, name[asciiStart:]...) } return js } func (p *printer) printUnquotedUTF16(text []uint16, quote rune) { temp := make([]byte, utf8.UTFMax) js := p.js i := 0 n := len(text) for i < n { c := text[i] i++ switch c { // Special-case the null character since it may mess with code written in C // that treats null characters as the end of the string. case '\x00': // We don't want "\x001" to be written as "\01" if i < n && text[i] >= '0' && text[i] <= '9' { js = append(js, "\\x00"...) } else { js = append(js, "\\0"...) } // Special-case the bell character since it may cause dumping this file to // the terminal to make a sound, which is undesirable. Note that we can't // use an octal literal to print this shorter since octal literals are not // allowed in strict mode (or in template strings). case '\x07': js = append(js, "\\x07"...) case '\b': js = append(js, "\\b"...) case '\f': js = append(js, "\\f"...) case '\n': if quote == '`' { js = append(js, '\n') } else { js = append(js, "\\n"...) } case '\r': js = append(js, "\\r"...) case '\v': js = append(js, "\\v"...) case '\\': js = append(js, "\\\\"...) case '/': // Avoid generating the sequence "= 2 && text[i-2] == '<' && i+6 <= len(text) { script := "script" matches := true for j := 0; j < 6; j++ { a := text[i+j] b := uint16(script[j]) if a >= 'A' && a <= 'Z' { a += 'a' - 'A' } if a != b { matches = false break } } if matches { js = append(js, '\\') } } js = append(js, '/') case '\'': if quote == '\'' { js = append(js, '\\') } js = append(js, '\'') case '"': if quote == '"' { js = append(js, '\\') } js = append(js, '"') case '`': if quote == '`' { js = append(js, '\\') } js = append(js, '`') case '$': if quote == '`' && i < n && text[i] == '{' { js = append(js, '\\') } js = append(js, '$') case '\u2028': js = append(js, "\\u2028"...) case '\u2029': js = append(js, "\\u2029"...) case '\uFEFF': js = append(js, "\\uFEFF"...) default: switch { // Common case: just append a single byte case c <= lastASCII: js = append(js, byte(c)) // Is this a high surrogate? case c >= firstHighSurrogate && c <= lastHighSurrogate: // Is there a next character? if i < n { c2 := text[i] // Is it a low surrogate? if c2 >= firstLowSurrogate && c2 <= lastLowSurrogate { r := (rune(c) << 10) + rune(c2) + (0x10000 - (firstHighSurrogate << 10) - firstLowSurrogate) i++ // Escape this character if UTF-8 isn't allowed if p.options.ASCIIOnly { if !p.options.UnsupportedFeatures.Has(compat.UnicodeEscapes) { js = append(js, fmt.Sprintf("\\u{%X}", r)...) } else { js = append(js, '\\', 'u', hexChars[c>>12], hexChars[(c>>8)&15], hexChars[(c>>4)&15], hexChars[c&15], '\\', 'u', hexChars[c2>>12], hexChars[(c2>>8)&15], hexChars[(c2>>4)&15], hexChars[c2&15], ) } continue } // Otherwise, encode to UTF-8 width := utf8.EncodeRune(temp, r) js = append(js, temp[:width]...) continue } } // Write an unpaired high surrogate js = append(js, '\\', 'u', hexChars[c>>12], hexChars[(c>>8)&15], hexChars[(c>>4)&15], hexChars[c&15]) // Is this an unpaired low surrogate or four-digit hex escape? case (c >= firstLowSurrogate && c <= lastLowSurrogate) || (p.options.ASCIIOnly && c > 0xFF): js = append(js, '\\', 'u', hexChars[c>>12], hexChars[(c>>8)&15], hexChars[(c>>4)&15], hexChars[c&15]) // Can this be a two-digit hex escape? case p.options.ASCIIOnly: js = append(js, '\\', 'x', hexChars[c>>4], hexChars[c&15]) // Otherwise, just encode to UTF-8 default: width := utf8.EncodeRune(temp, rune(c)) js = append(js, temp[:width]...) } } } p.js = js } // Use JS strings for JSX attributes that need escape characters. Technically // the JSX specification doesn't say anything about using XML character escape // sequences, so JSX implementations may not be able to consume them. See // https://facebook.github.io/jsx/ for the specification. func (p *printer) canPrintTextAsJSXAttribute(text []uint16) (quote string, ok bool) { single := true double := true for _, c := range text { // Use JS strings for control characters if c < firstASCII { return "", false } // Use JS strings if we need to escape non-ASCII characters if p.options.ASCIIOnly && c > lastASCII { return "", false } switch c { case '&': // Use JS strings if the text would need to be escaped with "&" return "", false case '"': double = false if !single { break } case '\'': single = false if !double { break } } } // Prefer duble quotes to single quotes if double { return "\"", true } if single { return "'", true } return "", false } // Use JS strings for text inside JSX elements that need escape characters. // Technically the JSX specification doesn't say anything about using XML // character escape sequences, so JSX implementations may not be able to // consume them. See https://facebook.github.io/jsx/ for the specification. func (p *printer) canPrintTextAsJSXChild(text []uint16) bool { for _, c := range text { // Use JS strings for control characters if c < firstASCII { return false } // Use JS strings if we need to escape non-ASCII characters if p.options.ASCIIOnly && c > lastASCII { return false } switch c { case '&', '<', '>', '{', '}': // Use JS strings if the text would need to be escaped return false } } return true } // JSX tag syntax doesn't support character escapes so non-ASCII identifiers // must be printed as UTF-8 even when the charset is set to ASCII. func (p *printer) printJSXTag(tagOrNil js_ast.Expr) { switch e := tagOrNil.Data.(type) { case *js_ast.EString: p.addSourceMapping(tagOrNil.Loc) p.print(js_lexer.UTF16ToString(e.Value)) case *js_ast.EIdentifier: name := p.renamer.NameForSymbol(e.Ref) p.addSourceMapping(tagOrNil.Loc) p.print(name) case *js_ast.EDot: p.printJSXTag(e.Target) p.print(".") p.addSourceMapping(e.NameLoc) p.print(e.Name) default: if tagOrNil.Data != nil { p.printExpr(tagOrNil, js_ast.LLowest, 0) } } } type printer struct { symbols js_ast.SymbolMap renamer renamer.Renamer importRecords []ast.ImportRecord options Options extractedLegalComments map[string]bool needsSemicolon bool js []byte stmtStart int exportDefaultStart int arrowExprStart int forOfInitStart int prevOp js_ast.OpCode prevOpEnd int prevNumEnd int prevRegExpEnd int callTarget js_ast.E intToBytesBuffer [64]byte builder sourcemap.ChunkBuilder } func (p *printer) print(text string) { p.js = append(p.js, text...) } // This is the same as "print(string(bytes))" without any unnecessary temporary // allocations func (p *printer) printBytes(bytes []byte) { p.js = append(p.js, bytes...) } func (p *printer) printQuotedUTF8(text string, allowBacktick bool) { p.printQuotedUTF16(js_lexer.StringToUTF16(text), allowBacktick) } func (p *printer) addSourceMapping(loc logger.Loc) { if p.options.AddSourceMappings { p.builder.AddSourceMapping(loc, p.js) } } func (p *printer) printIndent() { if !p.options.RemoveWhitespace { for i := 0; i < p.options.Indent; i++ { p.print(" ") } } } func (p *printer) printSymbol(ref js_ast.Ref) { name := p.renamer.NameForSymbol(ref) // Minify "return #foo in bar" to "return#foo in bar" if !strings.HasPrefix(name, "#") { p.printSpaceBeforeIdentifier() } p.printIdentifier(name) } func (p *printer) printClauseAlias(alias string) { if js_lexer.IsIdentifier(alias) { p.printSpaceBeforeIdentifier() p.printIdentifier(alias) } else { p.printQuotedUTF8(alias, false /* allowBacktick */) } } // Note: The functions below check whether something can be printed as an // identifier or if it needs to be quoted (e.g. "x.y" vs. "x['y']") using the // ES5 identifier validity test to maximize cross-platform portability. Even // though newer JavaScript environments can handle more Unicode characters, // there isn't a published document that says which Unicode versions are // supported by which browsers. Even if a character is considered valid in the // latest version of Unicode, we don't know if the browser we're targeting // contains an older version of Unicode or not. So for safety, we quote // anything that isn't guaranteed to be compatible with ES5, the oldest // JavaScript language target that we support. func CanEscapeIdentifier(name string, unsupportedJSFeatures compat.JSFeature, asciiOnly bool) bool { return js_lexer.IsIdentifierES5AndESNext(name) && (!asciiOnly || !unsupportedJSFeatures.Has(compat.UnicodeEscapes) || !js_lexer.ContainsNonBMPCodePoint(name)) } func (p *printer) canPrintIdentifier(name string) bool { return js_lexer.IsIdentifierES5AndESNext(name) && (!p.options.ASCIIOnly || !p.options.UnsupportedFeatures.Has(compat.UnicodeEscapes) || !js_lexer.ContainsNonBMPCodePoint(name)) } func (p *printer) canPrintIdentifierUTF16(name []uint16) bool { return js_lexer.IsIdentifierES5AndESNextUTF16(name) && (!p.options.ASCIIOnly || !p.options.UnsupportedFeatures.Has(compat.UnicodeEscapes) || !js_lexer.ContainsNonBMPCodePointUTF16(name)) } func (p *printer) printIdentifier(name string) { if p.options.ASCIIOnly { p.js = QuoteIdentifier(p.js, name, p.options.UnsupportedFeatures) } else { p.print(name) } } // This is the same as "printIdentifier(StringToUTF16(bytes))" without any // unnecessary temporary allocations func (p *printer) printIdentifierUTF16(name []uint16) { var temp [utf8.UTFMax]byte n := len(name) for i := 0; i < n; i++ { c := rune(name[i]) if c >= firstHighSurrogate && c <= lastHighSurrogate && i+1 < n { if c2 := rune(name[i+1]); c2 >= firstLowSurrogate && c2 <= lastLowSurrogate { c = (c << 10) + c2 + (0x10000 - (firstHighSurrogate << 10) - firstLowSurrogate) i++ } } if p.options.ASCIIOnly && c > lastASCII { if c <= 0xFFFF { p.js = append(p.js, '\\', 'u', hexChars[c>>12], hexChars[(c>>8)&15], hexChars[(c>>4)&15], hexChars[c&15]) } else if !p.options.UnsupportedFeatures.Has(compat.UnicodeEscapes) { p.js = append(p.js, fmt.Sprintf("\\u{%X}", c)...) } else { panic("Internal error: Cannot encode identifier: Unicode escapes are unsupported") } continue } width := utf8.EncodeRune(temp[:], c) p.js = append(p.js, temp[:width]...) } } func (p *printer) printBinding(binding js_ast.Binding) { p.addSourceMapping(binding.Loc) switch b := binding.Data.(type) { case *js_ast.BMissing: case *js_ast.BIdentifier: p.printSymbol(b.Ref) case *js_ast.BArray: p.print("[") if len(b.Items) > 0 { if !b.IsSingleLine { p.options.Indent++ } for i, item := range b.Items { if i != 0 { p.print(",") if b.IsSingleLine { p.printSpace() } } if !b.IsSingleLine { p.printNewline() p.printIndent() } if b.HasSpread && i+1 == len(b.Items) { p.print("...") } p.printBinding(item.Binding) if item.DefaultValueOrNil.Data != nil { p.printSpace() p.print("=") p.printSpace() p.printExpr(item.DefaultValueOrNil, js_ast.LComma, 0) } // Make sure there's a comma after trailing missing items if _, ok := item.Binding.Data.(*js_ast.BMissing); ok && i == len(b.Items)-1 { p.print(",") } } if !b.IsSingleLine { p.options.Indent-- p.printNewline() p.printIndent() } } p.print("]") case *js_ast.BObject: p.print("{") if len(b.Properties) > 0 { if !b.IsSingleLine { p.options.Indent++ } for i, property := range b.Properties { if i != 0 { p.print(",") } if b.IsSingleLine { p.printSpace() } else { p.printNewline() p.printIndent() } if property.IsSpread { p.print("...") } else { if property.IsComputed { p.print("[") p.printExpr(property.Key, js_ast.LComma, 0) p.print("]:") p.printSpace() p.printBinding(property.Value) if property.DefaultValueOrNil.Data != nil { p.printSpace() p.print("=") p.printSpace() p.printExpr(property.DefaultValueOrNil, js_ast.LComma, 0) } continue } if str, ok := property.Key.Data.(*js_ast.EString); ok && !property.PreferQuotedKey && p.canPrintIdentifierUTF16(str.Value) { p.addSourceMapping(property.Key.Loc) p.printSpaceBeforeIdentifier() p.printIdentifierUTF16(str.Value) // Use a shorthand property if the names are the same if id, ok := property.Value.Data.(*js_ast.BIdentifier); ok && js_lexer.UTF16EqualsString(str.Value, p.renamer.NameForSymbol(id.Ref)) { if property.DefaultValueOrNil.Data != nil { p.printSpace() p.print("=") p.printSpace() p.printExpr(property.DefaultValueOrNil, js_ast.LComma, 0) } continue } } else { p.printExpr(property.Key, js_ast.LLowest, 0) } p.print(":") p.printSpace() } p.printBinding(property.Value) if property.DefaultValueOrNil.Data != nil { p.printSpace() p.print("=") p.printSpace() p.printExpr(property.DefaultValueOrNil, js_ast.LComma, 0) } } if !b.IsSingleLine { p.options.Indent-- p.printNewline() p.printIndent() } else if len(b.Properties) > 0 { p.printSpace() } } p.print("}") default: panic(fmt.Sprintf("Unexpected binding of type %T", binding.Data)) } } func (p *printer) printSpace() { if !p.options.RemoveWhitespace { p.print(" ") } } func (p *printer) printNewline() { if !p.options.RemoveWhitespace { p.print("\n") } } func (p *printer) printSpaceBeforeOperator(next js_ast.OpCode) { if p.prevOpEnd == len(p.js) { prev := p.prevOp // "+ + y" => "+ +y" // "+ ++ y" => "+ ++y" // "x + + y" => "x+ +y" // "x ++ + y" => "x+++y" // "x + ++ y" => "x+ ++y" // "-- >" => "-- >" // "< ! --" => " 1 && p.js[len(p.js)-2] == '<') { p.print(" ") } } } func (p *printer) printSemicolonAfterStatement() { if !p.options.RemoveWhitespace { p.print(";\n") } else { p.needsSemicolon = true } } func (p *printer) printSemicolonIfNeeded() { if p.needsSemicolon { p.print(";") p.needsSemicolon = false } } func (p *printer) printSpaceBeforeIdentifier() { buffer := p.js n := len(buffer) if n > 0 && (js_lexer.IsIdentifierContinue(rune(buffer[n-1])) || n == p.prevRegExpEnd) { p.print(" ") } } func (p *printer) printFnArgs(args []js_ast.Arg, hasRestArg bool, isArrow bool) { wrap := true // Minify "(a) => {}" as "a=>{}" if p.options.RemoveWhitespace && !hasRestArg && isArrow && len(args) == 1 { if _, ok := args[0].Binding.Data.(*js_ast.BIdentifier); ok && args[0].DefaultOrNil.Data == nil { wrap = false } } if wrap { p.print("(") } for i, arg := range args { if i != 0 { p.print(",") p.printSpace() } if hasRestArg && i+1 == len(args) { p.print("...") } p.printBinding(arg.Binding) if arg.DefaultOrNil.Data != nil { p.printSpace() p.print("=") p.printSpace() p.printExpr(arg.DefaultOrNil, js_ast.LComma, 0) } } if wrap { p.print(")") } } func (p *printer) printFn(fn js_ast.Fn) { p.printFnArgs(fn.Args, fn.HasRestArg, false /* isArrow */) p.printSpace() p.printBlock(fn.Body.Loc, fn.Body.Stmts) } func (p *printer) printClass(class js_ast.Class) { if class.ExtendsOrNil.Data != nil { p.print(" extends") p.printSpace() p.printExpr(class.ExtendsOrNil, js_ast.LNew-1, 0) } p.printSpace() p.addSourceMapping(class.BodyLoc) p.print("{") p.printNewline() p.options.Indent++ for _, item := range class.Properties { p.printSemicolonIfNeeded() p.printIndent() p.printProperty(item) // Need semicolons after class fields if item.ValueOrNil.Data == nil { p.printSemicolonAfterStatement() } else { p.printNewline() } } p.needsSemicolon = false p.options.Indent-- p.printIndent() p.print("}") } func (p *printer) printProperty(item js_ast.Property) { if item.Kind == js_ast.PropertySpread { p.print("...") p.printExpr(item.ValueOrNil, js_ast.LComma, 0) return } if item.IsStatic { p.print("static") p.printSpace() } switch item.Kind { case js_ast.PropertyGet: p.printSpaceBeforeIdentifier() p.print("get") p.printSpace() case js_ast.PropertySet: p.printSpaceBeforeIdentifier() p.print("set") p.printSpace() } if fn, ok := item.ValueOrNil.Data.(*js_ast.EFunction); item.IsMethod && ok { if fn.Fn.IsAsync { p.printSpaceBeforeIdentifier() p.print("async") p.printSpace() } if fn.Fn.IsGenerator { p.print("*") } } if item.IsComputed { p.print("[") p.printExpr(item.Key, js_ast.LComma, 0) p.print("]") if item.ValueOrNil.Data != nil { if fn, ok := item.ValueOrNil.Data.(*js_ast.EFunction); item.IsMethod && ok { p.printFn(fn.Fn) return } p.print(":") p.printSpace() p.printExpr(item.ValueOrNil, js_ast.LComma, 0) } if item.InitializerOrNil.Data != nil { p.printSpace() p.print("=") p.printSpace() p.printExpr(item.InitializerOrNil, js_ast.LComma, 0) } return } switch key := item.Key.Data.(type) { case *js_ast.EPrivateIdentifier: p.printSymbol(key.Ref) case *js_ast.EString: p.addSourceMapping(item.Key.Loc) if !item.PreferQuotedKey && p.canPrintIdentifierUTF16(key.Value) { p.printSpaceBeforeIdentifier() p.printIdentifierUTF16(key.Value) // Use a shorthand property if the names are the same if !p.options.UnsupportedFeatures.Has(compat.ObjectExtensions) && item.ValueOrNil.Data != nil { switch e := item.ValueOrNil.Data.(type) { case *js_ast.EIdentifier: if js_lexer.UTF16EqualsString(key.Value, p.renamer.NameForSymbol(e.Ref)) { if item.InitializerOrNil.Data != nil { p.printSpace() p.print("=") p.printSpace() p.printExpr(item.InitializerOrNil, js_ast.LComma, 0) } return } case *js_ast.EImportIdentifier: // Make sure we're not using a property access instead of an identifier ref := js_ast.FollowSymbols(p.symbols, e.Ref) symbol := p.symbols.Get(ref) if symbol.NamespaceAlias == nil && js_lexer.UTF16EqualsString(key.Value, p.renamer.NameForSymbol(e.Ref)) { if item.InitializerOrNil.Data != nil { p.printSpace() p.print("=") p.printSpace() p.printExpr(item.InitializerOrNil, js_ast.LComma, 0) } return } } } } else { p.printQuotedUTF16(key.Value, false /* allowBacktick */) } default: p.printExpr(item.Key, js_ast.LLowest, 0) } if item.Kind != js_ast.PropertyNormal { f, ok := item.ValueOrNil.Data.(*js_ast.EFunction) if ok { p.printFn(f.Fn) return } } if item.ValueOrNil.Data != nil { if fn, ok := item.ValueOrNil.Data.(*js_ast.EFunction); item.IsMethod && ok { p.printFn(fn.Fn) return } p.print(":") p.printSpace() p.printExpr(item.ValueOrNil, js_ast.LComma, 0) } if item.InitializerOrNil.Data != nil { p.printSpace() p.print("=") p.printSpace() p.printExpr(item.InitializerOrNil, js_ast.LComma, 0) } } func (p *printer) printQuotedUTF16(data []uint16, allowBacktick bool) { if p.options.UnsupportedFeatures.Has(compat.TemplateLiteral) { allowBacktick = false } singleCost := 0 doubleCost := 0 backtickCost := 0 for i, c := range data { switch c { case '\n': if p.options.MangleSyntax { // The backslash for the newline costs an extra character for old-style // string literals when compared to a template literal backtickCost-- } case '\'': singleCost++ case '"': doubleCost++ case '`': backtickCost++ case '$': // "${" sequences need to be escaped in template literals if i+1 < len(data) && data[i+1] == '{' { backtickCost++ } } } c := "\"" if doubleCost > singleCost { c = "'" if singleCost > backtickCost && allowBacktick { c = "`" } } else if doubleCost > backtickCost && allowBacktick { c = "`" } p.print(c) p.printUnquotedUTF16(data, rune(c[0])) p.print(c) } func (p *printer) printRequireOrImportExpr( importRecordIndex uint32, leadingInteriorComments []js_ast.Comment, level js_ast.L, flags printExprFlags, ) { record := &p.importRecords[importRecordIndex] if level >= js_ast.LNew || (flags&forbidCall) != 0 { p.print("(") defer p.print(")") level = js_ast.LLowest } if !record.SourceIndex.IsValid() { // External "require()" if record.Kind != ast.ImportDynamic { if record.WrapWithToModule { p.printSymbol(p.options.ToModuleRef) p.print("(") defer p.print(")") } // Potentially substitute our own "__require" stub for "require" if record.CallRuntimeRequire { p.printSymbol(p.options.RuntimeRequireRef) } else { p.printSpaceBeforeIdentifier() p.print("require") } p.print("(") p.addSourceMapping(record.Range.Loc) p.printQuotedUTF8(record.Path.Text, true /* allowBacktick */) p.print(")") return } // External "import()" if !p.options.UnsupportedFeatures.Has(compat.DynamicImport) { p.printSpaceBeforeIdentifier() p.print("import(") defer p.print(")") } else { p.printSpaceBeforeIdentifier() p.print("Promise.resolve()") p.printDotThenPrefix() defer p.printDotThenSuffix() // Wrap this with a call to "__toModule()" if this is a CommonJS file if record.WrapWithToModule { p.printSymbol(p.options.ToModuleRef) p.print("(") defer p.print(")") } // Potentially substitute our own "__require" stub for "require" if record.CallRuntimeRequire { p.printSymbol(p.options.RuntimeRequireRef) } else { p.printSpaceBeforeIdentifier() p.print("require") } p.print("(") defer p.print(")") } if len(leadingInteriorComments) > 0 { p.printNewline() p.options.Indent++ for _, comment := range leadingInteriorComments { p.printIndentedComment(comment.Text) } p.printIndent() } p.addSourceMapping(record.Range.Loc) p.printQuotedUTF8(record.Path.Text, true /* allowBacktick */) if !p.options.UnsupportedFeatures.Has(compat.DynamicImport) { p.printImportCallAssertions(record.Assertions) } if len(leadingInteriorComments) > 0 { p.printNewline() p.options.Indent-- p.printIndent() } return } meta := p.options.RequireOrImportMetaForSource(record.SourceIndex.GetIndex()) // Don't need the namespace object if the result is unused anyway if (flags & exprResultIsUnused) != 0 { meta.ExportsRef = js_ast.InvalidRef } // Internal "import()" of async ESM if record.Kind == ast.ImportDynamic && meta.IsWrapperAsync { p.printSymbol(meta.WrapperRef) p.print("()") if meta.ExportsRef != js_ast.InvalidRef { p.printDotThenPrefix() p.printSymbol(meta.ExportsRef) p.printDotThenSuffix() } return } // Internal "require()" or "import()" if record.Kind == ast.ImportDynamic { p.printSpaceBeforeIdentifier() p.print("Promise.resolve()") level = p.printDotThenPrefix() defer p.printDotThenSuffix() } // Make sure the comma operator is propertly wrapped if meta.ExportsRef != js_ast.InvalidRef && level >= js_ast.LComma { p.print("(") defer p.print(")") } // Wrap this with a call to "__toModule()" if this is a CommonJS file if record.WrapWithToModule { p.printSymbol(p.options.ToModuleRef) p.print("(") defer p.print(")") } // Call the wrapper p.printSymbol(meta.WrapperRef) p.print("()") // Return the namespace object if this is an ESM file if meta.ExportsRef != js_ast.InvalidRef { p.print(",") p.printSpace() p.printSymbol(meta.ExportsRef) } } func (p *printer) printDotThenPrefix() js_ast.L { if p.options.UnsupportedFeatures.Has(compat.Arrow) { p.print(".then(function()") p.printSpace() p.print("{") p.printNewline() p.options.Indent++ p.printIndent() p.print("return") p.printSpace() return js_ast.LLowest } else { p.print(".then(()") p.printSpace() p.print("=>") p.printSpace() return js_ast.LComma } } func (p *printer) printDotThenSuffix() { if p.options.UnsupportedFeatures.Has(compat.Arrow) { if !p.options.RemoveWhitespace { p.print(";") } p.printNewline() p.options.Indent-- p.printIndent() p.print("})") } else { p.print(")") } } type printExprFlags uint8 const ( forbidCall printExprFlags = 1 << iota forbidIn hasNonOptionalChainParent exprResultIsUnused isFollowedByOf isInsideForAwait ) func (p *printer) printUndefined(level js_ast.L) { if level >= js_ast.LPrefix { p.print("(void 0)") } else { p.printSpaceBeforeIdentifier() p.print("void 0") p.prevNumEnd = len(p.js) } } func (p *printer) printExpr(expr js_ast.Expr, level js_ast.L, flags printExprFlags) { wasFollowedByOf := (flags & isFollowedByOf) != 0 flags &= ^isFollowedByOf p.addSourceMapping(expr.Loc) switch e := expr.Data.(type) { case *js_ast.EMissing: case *js_ast.EUndefined: p.printUndefined(level) case *js_ast.ESuper: p.printSpaceBeforeIdentifier() p.print("super") case *js_ast.ENull: p.printSpaceBeforeIdentifier() p.print("null") case *js_ast.EThis: p.printSpaceBeforeIdentifier() p.print("this") case *js_ast.ESpread: p.print("...") p.printExpr(e.Value, js_ast.LComma, 0) case *js_ast.ENewTarget: p.printSpaceBeforeIdentifier() p.print("new.target") case *js_ast.EImportMeta: p.printSpaceBeforeIdentifier() p.print("import.meta") case *js_ast.EJSXElement: // Start the opening tag p.print("<") p.printJSXTag(e.TagOrNil) // Print the attributes for _, property := range e.Properties { p.printSpace() if property.Kind == js_ast.PropertySpread { p.print("{...") p.printExpr(property.ValueOrNil, js_ast.LComma, 0) p.print("}") continue } p.printSpaceBeforeIdentifier() p.addSourceMapping(property.Key.Loc) p.print(js_lexer.UTF16ToString(property.Key.Data.(*js_ast.EString).Value)) // Special-case string values if str, ok := property.ValueOrNil.Data.(*js_ast.EString); ok { if quote, ok := p.canPrintTextAsJSXAttribute(str.Value); ok { p.print("=") p.addSourceMapping(property.ValueOrNil.Loc) p.print(quote) p.print(js_lexer.UTF16ToString(str.Value)) p.print(quote) continue } } // Implicit "true" value if boolean, ok := property.ValueOrNil.Data.(*js_ast.EBoolean); ok && boolean.Value && property.WasShorthand { continue } // Generic JS value p.print("={") p.printExpr(property.ValueOrNil, js_ast.LComma, 0) p.print("}") } // End the opening tag if e.TagOrNil.Data != nil && len(e.Children) == 0 { p.printSpace() p.addSourceMapping(e.CloseLoc) p.print("/>") break } p.print(">") isSingleLine := true if !p.options.RemoveWhitespace { isSingleLine = len(e.Children) < 2 if len(e.Children) == 1 { if _, ok := e.Children[0].Data.(*js_ast.EJSXElement); !ok { isSingleLine = true } } } if !isSingleLine { p.options.Indent++ } // Print the children for _, child := range e.Children { if !isSingleLine { p.printNewline() p.printIndent() } if _, ok := child.Data.(*js_ast.EJSXElement); ok { p.printExpr(child, js_ast.LLowest, 0) } else if str, ok := child.Data.(*js_ast.EString); ok && isSingleLine && p.canPrintTextAsJSXChild(str.Value) { p.addSourceMapping(child.Loc) p.print(js_lexer.UTF16ToString(str.Value)) } else { p.print("{") p.printExpr(child, js_ast.LComma, 0) p.print("}") } } // Print the closing tag if !isSingleLine { p.options.Indent-- p.printNewline() p.printIndent() } p.addSourceMapping(e.CloseLoc) p.print("") case *js_ast.ENew: wrap := level >= js_ast.LCall hasPureComment := !p.options.RemoveWhitespace && e.CanBeUnwrappedIfUnused if hasPureComment && level >= js_ast.LPostfix { wrap = true } if wrap { p.print("(") } if hasPureComment { p.print("/* @__PURE__ */ ") } p.printSpaceBeforeIdentifier() p.print("new") p.printSpace() p.printExpr(e.Target, js_ast.LNew, forbidCall) // Omit the "()" when minifying, but only when safe to do so if !p.options.RemoveWhitespace || len(e.Args) > 0 || level >= js_ast.LPostfix { p.print("(") for i, arg := range e.Args { if i != 0 { p.print(",") p.printSpace() } p.printExpr(arg, js_ast.LComma, 0) } p.print(")") } if wrap { p.print(")") } case *js_ast.ECall: wrap := level >= js_ast.LNew || (flags&forbidCall) != 0 var targetFlags printExprFlags if e.OptionalChain == js_ast.OptionalChainNone { targetFlags = hasNonOptionalChainParent } else if (flags & hasNonOptionalChainParent) != 0 { wrap = true } hasPureComment := !p.options.RemoveWhitespace && e.CanBeUnwrappedIfUnused if hasPureComment && level >= js_ast.LPostfix { wrap = true } if wrap { p.print("(") } if hasPureComment { wasStmtStart := p.stmtStart == len(p.js) p.print("/* @__PURE__ */ ") if wasStmtStart { p.stmtStart = len(p.js) } } // We don't ever want to accidentally generate a direct eval expression here p.callTarget = e.Target.Data if !e.IsDirectEval && p.isUnboundEvalIdentifier(e.Target) { if p.options.RemoveWhitespace { p.print("(0,") } else { p.print("(0, ") } p.printExpr(e.Target, js_ast.LPostfix, 0) p.print(")") } else { p.printExpr(e.Target, js_ast.LPostfix, targetFlags) } if e.OptionalChain == js_ast.OptionalChainStart { p.print("?.") } p.print("(") for i, arg := range e.Args { if i != 0 { p.print(",") p.printSpace() } p.printExpr(arg, js_ast.LComma, 0) } p.print(")") if wrap { p.print(")") } case *js_ast.ERequireString: p.printRequireOrImportExpr(e.ImportRecordIndex, nil, level, flags) case *js_ast.ERequireResolveString: wrap := level >= js_ast.LNew || (flags&forbidCall) != 0 if wrap { p.print("(") } p.printSpaceBeforeIdentifier() p.print("require.resolve(") p.printQuotedUTF8(p.importRecords[e.ImportRecordIndex].Path.Text, true /* allowBacktick */) p.print(")") if wrap { p.print(")") } case *js_ast.EImportString: var leadingInteriorComments []js_ast.Comment if !p.options.RemoveWhitespace { leadingInteriorComments = e.LeadingInteriorComments } p.printRequireOrImportExpr(e.ImportRecordIndex, leadingInteriorComments, level, flags) case *js_ast.EImportCall: var leadingInteriorComments []js_ast.Comment if !p.options.RemoveWhitespace { leadingInteriorComments = e.LeadingInteriorComments } wrap := level >= js_ast.LNew || (flags&forbidCall) != 0 if wrap { p.print("(") } p.printSpaceBeforeIdentifier() p.print("import(") if len(leadingInteriorComments) > 0 { p.printNewline() p.options.Indent++ for _, comment := range leadingInteriorComments { p.printIndentedComment(comment.Text) } p.printIndent() } p.printExpr(e.Expr, js_ast.LComma, 0) if e.OptionsOrNil.Data != nil { p.print(",") p.printSpace() p.printExpr(e.OptionsOrNil, js_ast.LComma, 0) } if len(leadingInteriorComments) > 0 { p.printNewline() p.options.Indent-- p.printIndent() } p.print(")") if wrap { p.print(")") } case *js_ast.EDot: wrap := false if e.OptionalChain == js_ast.OptionalChainNone { flags |= hasNonOptionalChainParent } else { if (flags & hasNonOptionalChainParent) != 0 { wrap = true p.print("(") } flags &= ^hasNonOptionalChainParent } p.printExpr(e.Target, js_ast.LPostfix, flags) if p.canPrintIdentifier(e.Name) { if e.OptionalChain != js_ast.OptionalChainStart && p.prevNumEnd == len(p.js) { // "1.toString" is a syntax error, so print "1 .toString" instead p.print(" ") } if e.OptionalChain == js_ast.OptionalChainStart { p.print("?.") } else { p.print(".") } p.addSourceMapping(e.NameLoc) p.printIdentifier(e.Name) } else { if e.OptionalChain == js_ast.OptionalChainStart { p.print("?.") } p.print("[") p.addSourceMapping(e.NameLoc) p.printQuotedUTF8(e.Name, true /* allowBacktick */) p.print("]") } if wrap { p.print(")") } case *js_ast.EIndex: wrap := false if e.OptionalChain == js_ast.OptionalChainNone { flags |= hasNonOptionalChainParent } else { if (flags & hasNonOptionalChainParent) != 0 { wrap = true p.print("(") } flags &= ^hasNonOptionalChainParent } p.printExpr(e.Target, js_ast.LPostfix, flags) if e.OptionalChain == js_ast.OptionalChainStart { p.print("?.") } if private, ok := e.Index.Data.(*js_ast.EPrivateIdentifier); ok { if e.OptionalChain != js_ast.OptionalChainStart { p.print(".") } p.printSymbol(private.Ref) } else { p.print("[") p.printExpr(e.Index, js_ast.LLowest, 0) p.print("]") } if wrap { p.print(")") } case *js_ast.EIf: wrap := level >= js_ast.LConditional if wrap { p.print("(") flags &= ^forbidIn } p.printExpr(e.Test, js_ast.LConditional, flags&forbidIn) p.printSpace() p.print("?") p.printSpace() p.printExpr(e.Yes, js_ast.LYield, 0) p.printSpace() p.print(":") p.printSpace() p.printExpr(e.No, js_ast.LYield, flags&forbidIn) if wrap { p.print(")") } case *js_ast.EArrow: wrap := level >= js_ast.LAssign if wrap { p.print("(") } if e.IsAsync { p.printSpaceBeforeIdentifier() p.print("async") p.printSpace() } p.printFnArgs(e.Args, e.HasRestArg, true /* isArrow */) p.printSpace() p.print("=>") p.printSpace() wasPrinted := false if len(e.Body.Stmts) == 1 && e.PreferExpr { if s, ok := e.Body.Stmts[0].Data.(*js_ast.SReturn); ok && s.ValueOrNil.Data != nil { p.arrowExprStart = len(p.js) p.printExpr(s.ValueOrNil, js_ast.LComma, flags&forbidIn) wasPrinted = true } } if !wasPrinted { p.printBlock(e.Body.Loc, e.Body.Stmts) } if wrap { p.print(")") } case *js_ast.EFunction: n := len(p.js) wrap := p.stmtStart == n || p.exportDefaultStart == n if wrap { p.print("(") } p.printSpaceBeforeIdentifier() if e.Fn.IsAsync { p.print("async ") } p.print("function") if e.Fn.IsGenerator { p.print("*") p.printSpace() } if e.Fn.Name != nil { p.printSymbol(e.Fn.Name.Ref) } p.printFn(e.Fn) if wrap { p.print(")") } case *js_ast.EClass: n := len(p.js) wrap := p.stmtStart == n || p.exportDefaultStart == n if wrap { p.print("(") } p.printSpaceBeforeIdentifier() p.print("class") if e.Class.Name != nil { p.printSymbol(e.Class.Name.Ref) } p.printClass(e.Class) if wrap { p.print(")") } case *js_ast.EArray: p.print("[") if len(e.Items) > 0 { if !e.IsSingleLine { p.options.Indent++ } for i, item := range e.Items { if i != 0 { p.print(",") if e.IsSingleLine { p.printSpace() } } if !e.IsSingleLine { p.printNewline() p.printIndent() } p.printExpr(item, js_ast.LComma, 0) // Make sure there's a comma after trailing missing items _, ok := item.Data.(*js_ast.EMissing) if ok && i == len(e.Items)-1 { p.print(",") } } if !e.IsSingleLine { p.options.Indent-- p.printNewline() p.printIndent() } } p.print("]") case *js_ast.EObject: n := len(p.js) wrap := p.stmtStart == n || p.arrowExprStart == n if wrap { p.print("(") } p.print("{") if len(e.Properties) != 0 { if !e.IsSingleLine { p.options.Indent++ } for i, item := range e.Properties { if i != 0 { p.print(",") } if e.IsSingleLine { p.printSpace() } else { p.printNewline() p.printIndent() } p.printProperty(item) } if !e.IsSingleLine { p.options.Indent-- p.printNewline() p.printIndent() } else if len(e.Properties) > 0 { p.printSpace() } } p.print("}") if wrap { p.print(")") } case *js_ast.EBoolean: if p.options.MangleSyntax { if level >= js_ast.LPrefix { if e.Value { p.print("(!0)") } else { p.print("(!1)") } } else { if e.Value { p.print("!0") } else { p.print("!1") } } } else { p.printSpaceBeforeIdentifier() if e.Value { p.print("true") } else { p.print("false") } } case *js_ast.EString: // If this was originally a template literal, print it as one as long as we're not minifying if e.PreferTemplate && !p.options.MangleSyntax && !p.options.UnsupportedFeatures.Has(compat.TemplateLiteral) { p.print("`") p.printUnquotedUTF16(e.Value, '`') p.print("`") return } p.printQuotedUTF16(e.Value, true /* allowBacktick */) case *js_ast.ETemplate: // Convert no-substitution template literals into strings if it's smaller if p.options.MangleSyntax && e.TagOrNil.Data == nil && len(e.Parts) == 0 { p.printQuotedUTF16(e.HeadCooked, true /* allowBacktick */) return } if e.TagOrNil.Data != nil { // Optional chains are forbidden in template tags if js_ast.IsOptionalChain(e.TagOrNil) { p.print("(") p.printExpr(e.TagOrNil, js_ast.LLowest, 0) p.print(")") } else { p.printExpr(e.TagOrNil, js_ast.LPostfix, 0) } } p.print("`") if e.TagOrNil.Data != nil { p.print(e.HeadRaw) } else { p.printUnquotedUTF16(e.HeadCooked, '`') } for _, part := range e.Parts { p.print("${") p.printExpr(part.Value, js_ast.LLowest, 0) p.print("}") if e.TagOrNil.Data != nil { p.print(part.TailRaw) } else { p.printUnquotedUTF16(part.TailCooked, '`') } } p.print("`") case *js_ast.ERegExp: buffer := p.js n := len(buffer) if n > 0 { // Avoid forming a single-line comment or "= 7 && strings.EqualFold(e.Value[:7], "/script")) { p.print(" ") } } p.print(e.Value) // Need a space before the next identifier to avoid it turning into flags p.prevRegExpEnd = len(p.js) case *js_ast.EBigInt: p.printSpaceBeforeIdentifier() p.print(e.Value) p.print("n") case *js_ast.ENumber: value := e.Value absValue := math.Abs(value) if value != value { p.printSpaceBeforeIdentifier() p.print("NaN") } else if value == positiveInfinity || value == negativeInfinity { wrap := (p.options.MangleSyntax && level >= js_ast.LMultiply) || (value == negativeInfinity && level >= js_ast.LPrefix) if wrap { p.print("(") } if value == negativeInfinity { p.printSpaceBeforeOperator(js_ast.UnOpNeg) p.print("-") } else { p.printSpaceBeforeIdentifier() } if !p.options.MangleSyntax { p.print("Infinity") } else if p.options.RemoveWhitespace { p.print("1/0") } else { p.print("1 / 0") } if wrap { p.print(")") } } else { if !math.Signbit(value) { p.printSpaceBeforeIdentifier() p.printNonNegativeFloat(absValue) // Remember the end of the latest number p.prevNumEnd = len(p.js) } else if level >= js_ast.LPrefix { // Expressions such as "(-1).toString" need to wrap negative numbers. // Instead of testing for "value < 0" we test for "signbit(value)" and // "!isNaN(value)" because we need this to be true for "-0" and "-0 < 0" // is false. p.print("(-") p.printNonNegativeFloat(absValue) p.print(")") } else { p.printSpaceBeforeOperator(js_ast.UnOpNeg) p.print("-") p.printNonNegativeFloat(absValue) // Remember the end of the latest number p.prevNumEnd = len(p.js) } } case *js_ast.EIdentifier: name := p.renamer.NameForSymbol(e.Ref) wrap := len(p.js) == p.forOfInitStart && (name == "let" || (wasFollowedByOf && (flags&isInsideForAwait) == 0 && name == "async")) if wrap { p.print("(") } p.printSpaceBeforeIdentifier() p.printIdentifier(name) if wrap { p.print(")") } case *js_ast.EImportIdentifier: // Potentially use a property access instead of an identifier ref := js_ast.FollowSymbols(p.symbols, e.Ref) symbol := p.symbols.Get(ref) if symbol.ImportItemStatus == js_ast.ImportItemMissing { p.printUndefined(level) } else if symbol.NamespaceAlias != nil { wrap := p.callTarget == e && e.WasOriginallyIdentifier if wrap { if p.options.RemoveWhitespace { p.print("(0,") } else { p.print("(0, ") } } p.printSymbol(symbol.NamespaceAlias.NamespaceRef) alias := symbol.NamespaceAlias.Alias if !e.PreferQuotedKey && p.canPrintIdentifier(alias) { p.print(".") p.printIdentifier(alias) } else { p.print("[") p.printQuotedUTF8(alias, true /* allowBacktick */) p.print("]") } if wrap { p.print(")") } } else { p.printSymbol(e.Ref) } case *js_ast.EAwait: wrap := level >= js_ast.LPrefix if wrap { p.print("(") } p.printSpaceBeforeIdentifier() p.print("await") p.printSpace() p.printExpr(e.Value, js_ast.LPrefix-1, 0) if wrap { p.print(")") } case *js_ast.EYield: wrap := level >= js_ast.LAssign if wrap { p.print("(") } p.printSpaceBeforeIdentifier() p.print("yield") if e.ValueOrNil.Data != nil { if e.IsStar { p.print("*") } p.printSpace() p.printExpr(e.ValueOrNil, js_ast.LYield, 0) } if wrap { p.print(")") } case *js_ast.EUnary: entry := js_ast.OpTable[e.Op] wrap := level >= entry.Level if wrap { p.print("(") } if !e.Op.IsPrefix() { p.printExpr(e.Value, js_ast.LPostfix-1, 0) } if entry.IsKeyword { p.printSpaceBeforeIdentifier() p.print(entry.Text) p.printSpace() } else { p.printSpaceBeforeOperator(e.Op) p.print(entry.Text) p.prevOp = e.Op p.prevOpEnd = len(p.js) } if e.Op.IsPrefix() { p.printExpr(e.Value, js_ast.LPrefix-1, 0) } if wrap { p.print(")") } case *js_ast.EBinary: entry := js_ast.OpTable[e.Op] wrap := level >= entry.Level || (e.Op == js_ast.BinOpIn && (flags&forbidIn) != 0) // Destructuring assignments must be parenthesized if n := len(p.js); p.stmtStart == n || p.arrowExprStart == n { if _, ok := e.Left.Data.(*js_ast.EObject); ok { wrap = true } } if wrap { p.print("(") flags &= ^forbidIn } leftLevel := entry.Level - 1 rightLevel := entry.Level - 1 if e.Op.IsRightAssociative() { leftLevel = entry.Level } if e.Op.IsLeftAssociative() { rightLevel = entry.Level } switch e.Op { case js_ast.BinOpNullishCoalescing: // "??" can't directly contain "||" or "&&" without being wrapped in parentheses if left, ok := e.Left.Data.(*js_ast.EBinary); ok && (left.Op == js_ast.BinOpLogicalOr || left.Op == js_ast.BinOpLogicalAnd) { leftLevel = js_ast.LPrefix } if right, ok := e.Right.Data.(*js_ast.EBinary); ok && (right.Op == js_ast.BinOpLogicalOr || right.Op == js_ast.BinOpLogicalAnd) { rightLevel = js_ast.LPrefix } case js_ast.BinOpPow: // "**" can't contain certain unary expressions if left, ok := e.Left.Data.(*js_ast.EUnary); ok && left.Op.UnaryAssignTarget() == js_ast.AssignTargetNone { leftLevel = js_ast.LCall } else if _, ok := e.Left.Data.(*js_ast.EAwait); ok { leftLevel = js_ast.LCall } else if _, ok := e.Left.Data.(*js_ast.EUndefined); ok { // Undefined is printed as "void 0" leftLevel = js_ast.LCall } else if _, ok := e.Left.Data.(*js_ast.ENumber); ok { // Negative numbers are printed using a unary operator leftLevel = js_ast.LCall } else if p.options.MangleSyntax { // When minifying, booleans are printed as "!0 and "!1" if _, ok := e.Left.Data.(*js_ast.EBoolean); ok { leftLevel = js_ast.LCall } } } // Special-case "#foo in bar" if private, ok := e.Left.Data.(*js_ast.EPrivateIdentifier); ok && e.Op == js_ast.BinOpIn { p.printSymbol(private.Ref) } else { p.printExpr(e.Left, leftLevel, flags&forbidIn) } if e.Op != js_ast.BinOpComma { p.printSpace() } if entry.IsKeyword { p.printSpaceBeforeIdentifier() p.print(entry.Text) } else { p.printSpaceBeforeOperator(e.Op) p.print(entry.Text) p.prevOp = e.Op p.prevOpEnd = len(p.js) } p.printSpace() p.printExpr(e.Right, rightLevel, flags&forbidIn) if wrap { p.print(")") } default: panic(fmt.Sprintf("Unexpected expression of type %T", expr.Data)) } } func (p *printer) isUnboundEvalIdentifier(value js_ast.Expr) bool { if id, ok := value.Data.(*js_ast.EIdentifier); ok { // Using the original name here is ok since unbound symbols are not renamed symbol := p.symbols.Get(js_ast.FollowSymbols(p.symbols, id.Ref)) return symbol.Kind == js_ast.SymbolUnbound && symbol.OriginalName == "eval" } return false } // Convert an integer to a byte slice without any allocations func (p *printer) smallIntToBytes(n int) []byte { wasNegative := n < 0 if wasNegative { // This assumes that -math.MinInt isn't a problem. This is fine because // these integers are floating-point exponents which never go up that high. n = -n } bytes := p.intToBytesBuffer[:] start := len(bytes) // Write out the number from the end to the front for { start-- bytes[start] = '0' + byte(n%10) n /= 10 if n == 0 { break } } // Stick a negative sign on the front if needed if wasNegative { start-- bytes[start] = '-' } return bytes[start:] } func parseSmallInt(bytes []byte) int { wasNegative := bytes[0] == '-' if wasNegative { bytes = bytes[1:] } // Parse the integer without any error checking. This doesn't need to handle // integer overflow because these integers are floating-point exponents which // never go up that high. n := 0 for _, c := range bytes { n = n*10 + int(c-'0') } if wasNegative { return -n } return n } func (p *printer) printNonNegativeFloat(absValue float64) { // We can avoid the slow call to strconv.FormatFloat() for integers less than // 1000 because we know that exponential notation will always be longer than // the integer representation. This is not the case for 1000 which is "1e3". if absValue < 1000 { if asInt := int64(absValue); absValue == float64(asInt) { p.printBytes(p.smallIntToBytes(int(asInt))) return } } // Format this number into a byte slice so we can mutate it in place without // further reallocation result := []byte(strconv.FormatFloat(absValue, 'g', -1, 64)) // Simplify the exponent // "e+05" => "e5" // "e-05" => "e-5" if e := bytes.LastIndexByte(result, 'e'); e != -1 { from := e + 1 to := from switch result[from] { case '+': // Strip off the leading "+" from++ case '-': // Skip past the leading "-" to++ from++ } // Strip off leading zeros for from < len(result) && result[from] == '0' { from++ } result = append(result[:to], result[from:]...) } dot := bytes.IndexByte(result, '.') if dot == 1 && result[0] == '0' { // Simplify numbers starting with "0." afterDot := 2 // Strip off the leading zero when minifying // "0.5" => ".5" if p.options.RemoveWhitespace { result = result[1:] afterDot-- } // Try using an exponent // "0.001" => "1e-3" if result[afterDot] == '0' { i := afterDot + 1 for result[i] == '0' { i++ } remaining := result[i:] exponent := p.smallIntToBytes(afterDot - i - len(remaining)) // Only switch if it's actually shorter if len(result) > len(remaining)+1+len(exponent) { result = append(append(remaining, 'e'), exponent...) } } } else if dot != -1 { // Try to get rid of a "." and maybe also an "e" if e := bytes.LastIndexByte(result, 'e'); e != -1 { integer := result[:dot] fraction := result[dot+1 : e] exponent := parseSmallInt(result[e+1:]) - len(fraction) // Handle small exponents by appending zeros instead if exponent >= 0 && exponent <= 2 { // "1.2e1" => "12" // "1.2e2" => "120" // "1.2e3" => "1200" if len(result) >= len(integer)+len(fraction)+exponent { result = append(integer, fraction...) for i := 0; i < exponent; i++ { result = append(result, '0') } } } else { // "1.2e4" => "12e3" exponent := p.smallIntToBytes(exponent) if len(result) >= len(integer)+len(fraction)+1+len(exponent) { result = append(append(append(integer, fraction...), 'e'), exponent...) } } } } else if result[len(result)-1] == '0' { // Simplify numbers ending with "0" by trying to use an exponent // "1000" => "1e3" i := len(result) - 1 for i > 0 && result[i-1] == '0' { i-- } remaining := result[:i] exponent := p.smallIntToBytes(len(result) - i) // Only switch if it's actually shorter if len(result) > len(remaining)+1+len(exponent) { result = append(append(remaining, 'e'), exponent...) } } p.printBytes(result) } func (p *printer) printDeclStmt(isExport bool, keyword string, decls []js_ast.Decl) { p.printIndent() p.printSpaceBeforeIdentifier() if isExport { p.print("export ") } p.printDecls(keyword, decls, 0) p.printSemicolonAfterStatement() } func (p *printer) printForLoopInit(init js_ast.Stmt, flags printExprFlags) { switch s := init.Data.(type) { case *js_ast.SExpr: p.printExpr(s.Value, js_ast.LLowest, flags|exprResultIsUnused) case *js_ast.SLocal: switch s.Kind { case js_ast.LocalVar: p.printDecls("var", s.Decls, flags) case js_ast.LocalLet: p.printDecls("let", s.Decls, flags) case js_ast.LocalConst: p.printDecls("const", s.Decls, flags) } default: panic("Internal error") } } func (p *printer) printDecls(keyword string, decls []js_ast.Decl, flags printExprFlags) { p.print(keyword) p.printSpace() for i, decl := range decls { if i != 0 { p.print(",") p.printSpace() } p.printBinding(decl.Binding) if decl.ValueOrNil.Data != nil { p.printSpace() p.print("=") p.printSpace() p.printExpr(decl.ValueOrNil, js_ast.LComma, flags) } } } func (p *printer) printBody(body js_ast.Stmt) { if block, ok := body.Data.(*js_ast.SBlock); ok { p.printSpace() p.printBlock(body.Loc, block.Stmts) p.printNewline() } else { p.printNewline() p.options.Indent++ p.printStmt(body) p.options.Indent-- } } func (p *printer) printBlock(loc logger.Loc, stmts []js_ast.Stmt) { p.addSourceMapping(loc) p.print("{") p.printNewline() p.options.Indent++ for _, stmt := range stmts { p.printSemicolonIfNeeded() p.printStmt(stmt) } p.options.Indent-- p.needsSemicolon = false p.printIndent() p.print("}") } func wrapToAvoidAmbiguousElse(s js_ast.S) bool { for { switch current := s.(type) { case *js_ast.SIf: if current.NoOrNil.Data == nil { return true } s = current.NoOrNil.Data case *js_ast.SFor: s = current.Body.Data case *js_ast.SForIn: s = current.Body.Data case *js_ast.SForOf: s = current.Body.Data case *js_ast.SWhile: s = current.Body.Data case *js_ast.SWith: s = current.Body.Data default: return false } } } func (p *printer) printIf(s *js_ast.SIf) { p.printSpaceBeforeIdentifier() p.print("if") p.printSpace() p.print("(") p.printExpr(s.Test, js_ast.LLowest, 0) p.print(")") if yes, ok := s.Yes.Data.(*js_ast.SBlock); ok { p.printSpace() p.printBlock(s.Yes.Loc, yes.Stmts) if s.NoOrNil.Data != nil { p.printSpace() } else { p.printNewline() } } else if wrapToAvoidAmbiguousElse(s.Yes.Data) { p.printSpace() p.print("{") p.printNewline() p.options.Indent++ p.printStmt(s.Yes) p.options.Indent-- p.needsSemicolon = false p.printIndent() p.print("}") if s.NoOrNil.Data != nil { p.printSpace() } else { p.printNewline() } } else { p.printNewline() p.options.Indent++ p.printStmt(s.Yes) p.options.Indent-- if s.NoOrNil.Data != nil { p.printIndent() } } if s.NoOrNil.Data != nil { p.printSemicolonIfNeeded() p.printSpaceBeforeIdentifier() p.print("else") if no, ok := s.NoOrNil.Data.(*js_ast.SBlock); ok { p.printSpace() p.printBlock(s.NoOrNil.Loc, no.Stmts) p.printNewline() } else if no, ok := s.NoOrNil.Data.(*js_ast.SIf); ok { p.printIf(no) } else { p.printNewline() p.options.Indent++ p.printStmt(s.NoOrNil) p.options.Indent-- } } } func escapeClosingScriptTag(text string) string { i := strings.Index(text, "= 7 && strings.EqualFold(text[:7], "/script") { b.WriteByte('\\') } i = strings.Index(text, " 0 { p.print(",") } p.printSpace() p.addSourceMapping(entry.KeyLoc) if !entry.PreferQuotedKey && p.canPrintIdentifierUTF16(entry.Key) { p.printSpaceBeforeIdentifier() p.printIdentifierUTF16(entry.Key) } else { p.printQuotedUTF16(entry.Key, false /* allowBacktick */) } p.print(":") p.printSpace() p.addSourceMapping(entry.ValueLoc) p.printQuotedUTF16(entry.Value, false /* allowBacktick */) } if len(assertions) > 0 { p.printSpace() } p.print("}") } func (p *printer) printStmt(stmt js_ast.Stmt) { p.addSourceMapping(stmt.Loc) switch s := stmt.Data.(type) { case *js_ast.SComment: text := s.Text if s.IsLegalComment { switch p.options.LegalComments { case config.LegalCommentsNone: return case config.LegalCommentsEndOfFile, config.LegalCommentsLinkedWithComment, config.LegalCommentsExternalWithoutComment: if p.extractedLegalComments == nil { p.extractedLegalComments = make(map[string]bool) } p.extractedLegalComments[text] = true return } } p.printIndentedComment(text) case *js_ast.SFunction: p.printIndent() p.printSpaceBeforeIdentifier() if s.IsExport { p.print("export ") } if s.Fn.IsAsync { p.print("async ") } p.print("function") if s.Fn.IsGenerator { p.print("*") p.printSpace() } p.printSymbol(s.Fn.Name.Ref) p.printFn(s.Fn) p.printNewline() case *js_ast.SClass: p.printIndent() p.printSpaceBeforeIdentifier() if s.IsExport { p.print("export ") } p.print("class") p.printSymbol(s.Class.Name.Ref) p.printClass(s.Class) p.printNewline() case *js_ast.SEmpty: p.printIndent() p.print(";") p.printNewline() case *js_ast.SExportDefault: p.printIndent() p.printSpaceBeforeIdentifier() p.print("export default") p.printSpace() switch s2 := s.Value.Data.(type) { case *js_ast.SExpr: // Functions and classes must be wrapped to avoid confusion with their statement forms p.exportDefaultStart = len(p.js) p.printExpr(s2.Value, js_ast.LComma, 0) p.printSemicolonAfterStatement() return case *js_ast.SFunction: p.printSpaceBeforeIdentifier() if s2.Fn.IsAsync { p.print("async ") } p.print("function") if s2.Fn.IsGenerator { p.print("*") p.printSpace() } if s2.Fn.Name != nil { p.printSymbol(s2.Fn.Name.Ref) } p.printFn(s2.Fn) p.printNewline() case *js_ast.SClass: p.printSpaceBeforeIdentifier() p.print("class") if s2.Class.Name != nil { p.printSymbol(s2.Class.Name.Ref) } p.printClass(s2.Class) p.printNewline() default: panic("Internal error") } case *js_ast.SExportStar: p.printIndent() p.printSpaceBeforeIdentifier() p.print("export") p.printSpace() p.print("*") p.printSpace() if s.Alias != nil { p.print("as") p.printSpace() p.printClauseAlias(s.Alias.OriginalName) p.printSpace() p.printSpaceBeforeIdentifier() } p.print("from") p.printSpace() p.printPath(s.ImportRecordIndex) p.printSemicolonAfterStatement() case *js_ast.SExportClause: p.printIndent() p.printSpaceBeforeIdentifier() p.print("export") p.printSpace() p.print("{") if !s.IsSingleLine { p.options.Indent++ } for i, item := range s.Items { if i != 0 { p.print(",") } if s.IsSingleLine { p.printSpace() } else { p.printNewline() p.printIndent() } name := p.renamer.NameForSymbol(item.Name.Ref) p.printIdentifier(name) if name != item.Alias { p.print(" as") p.printSpace() p.printClauseAlias(item.Alias) } } if !s.IsSingleLine { p.options.Indent-- p.printNewline() p.printIndent() } else if len(s.Items) > 0 { p.printSpace() } p.print("}") p.printSemicolonAfterStatement() case *js_ast.SExportFrom: p.printIndent() p.printSpaceBeforeIdentifier() p.print("export") p.printSpace() p.print("{") if !s.IsSingleLine { p.options.Indent++ } for i, item := range s.Items { if i != 0 { p.print(",") } if s.IsSingleLine { p.printSpace() } else { p.printNewline() p.printIndent() } p.printClauseAlias(item.OriginalName) if item.OriginalName != item.Alias { p.printSpace() p.printSpaceBeforeIdentifier() p.print("as") p.printSpace() p.printClauseAlias(item.Alias) } } if !s.IsSingleLine { p.options.Indent-- p.printNewline() p.printIndent() } else if len(s.Items) > 0 { p.printSpace() } p.print("}") p.printSpace() p.print("from") p.printSpace() p.printPath(s.ImportRecordIndex) p.printSemicolonAfterStatement() case *js_ast.SLocal: switch s.Kind { case js_ast.LocalConst: p.printDeclStmt(s.IsExport, "const", s.Decls) case js_ast.LocalLet: p.printDeclStmt(s.IsExport, "let", s.Decls) case js_ast.LocalVar: p.printDeclStmt(s.IsExport, "var", s.Decls) } case *js_ast.SIf: p.printIndent() p.printIf(s) case *js_ast.SDoWhile: p.printIndent() p.printSpaceBeforeIdentifier() p.print("do") if block, ok := s.Body.Data.(*js_ast.SBlock); ok { p.printSpace() p.printBlock(s.Body.Loc, block.Stmts) p.printSpace() } else { p.printNewline() p.options.Indent++ p.printStmt(s.Body) p.printSemicolonIfNeeded() p.options.Indent-- p.printIndent() } p.print("while") p.printSpace() p.print("(") p.printExpr(s.Test, js_ast.LLowest, 0) p.print(")") p.printSemicolonAfterStatement() case *js_ast.SForIn: p.printIndent() p.printSpaceBeforeIdentifier() p.print("for") p.printSpace() p.print("(") p.printForLoopInit(s.Init, forbidIn) p.printSpace() p.printSpaceBeforeIdentifier() p.print("in") p.printSpace() p.printExpr(s.Value, js_ast.LLowest, 0) p.print(")") p.printBody(s.Body) case *js_ast.SForOf: p.printIndent() p.printSpaceBeforeIdentifier() p.print("for") if s.IsAwait { p.print(" await") } p.printSpace() p.print("(") p.forOfInitStart = len(p.js) flags := forbidIn | isFollowedByOf if s.IsAwait { flags |= isInsideForAwait } p.printForLoopInit(s.Init, flags) p.printSpace() p.printSpaceBeforeIdentifier() p.print("of") p.printSpace() p.printExpr(s.Value, js_ast.LComma, 0) p.print(")") p.printBody(s.Body) case *js_ast.SWhile: p.printIndent() p.printSpaceBeforeIdentifier() p.print("while") p.printSpace() p.print("(") p.printExpr(s.Test, js_ast.LLowest, 0) p.print(")") p.printBody(s.Body) case *js_ast.SWith: p.printIndent() p.printSpaceBeforeIdentifier() p.print("with") p.printSpace() p.print("(") p.printExpr(s.Value, js_ast.LLowest, 0) p.print(")") p.printBody(s.Body) case *js_ast.SLabel: p.printIndent() p.printSymbol(s.Name.Ref) p.print(":") p.printBody(s.Stmt) case *js_ast.STry: p.printIndent() p.printSpaceBeforeIdentifier() p.print("try") p.printSpace() p.printBlock(s.BodyLoc, s.Body) if s.Catch != nil { p.printSpace() p.print("catch") if s.Catch.BindingOrNil.Data != nil { p.printSpace() p.print("(") p.printBinding(s.Catch.BindingOrNil) p.print(")") } p.printSpace() p.printBlock(s.Catch.Loc, s.Catch.Body) } if s.Finally != nil { p.printSpace() p.print("finally") p.printSpace() p.printBlock(s.Finally.Loc, s.Finally.Stmts) } p.printNewline() case *js_ast.SFor: p.printIndent() p.printSpaceBeforeIdentifier() p.print("for") p.printSpace() p.print("(") if s.InitOrNil.Data != nil { p.printForLoopInit(s.InitOrNil, forbidIn) } p.print(";") p.printSpace() if s.TestOrNil.Data != nil { p.printExpr(s.TestOrNil, js_ast.LLowest, 0) } p.print(";") p.printSpace() if s.UpdateOrNil.Data != nil { p.printExpr(s.UpdateOrNil, js_ast.LLowest, 0) } p.print(")") p.printBody(s.Body) case *js_ast.SSwitch: p.printIndent() p.printSpaceBeforeIdentifier() p.print("switch") p.printSpace() p.print("(") p.printExpr(s.Test, js_ast.LLowest, 0) p.print(")") p.printSpace() p.print("{") p.printNewline() p.options.Indent++ for _, c := range s.Cases { p.printSemicolonIfNeeded() p.printIndent() if c.ValueOrNil.Data != nil { p.print("case") p.printSpace() p.printExpr(c.ValueOrNil, js_ast.LLogicalAnd, 0) } else { p.print("default") } p.print(":") if len(c.Body) == 1 { if block, ok := c.Body[0].Data.(*js_ast.SBlock); ok { p.printSpace() p.printBlock(c.Body[0].Loc, block.Stmts) p.printNewline() continue } } p.printNewline() p.options.Indent++ for _, stmt := range c.Body { p.printSemicolonIfNeeded() p.printStmt(stmt) } p.options.Indent-- } p.options.Indent-- p.printIndent() p.print("}") p.printNewline() p.needsSemicolon = false case *js_ast.SImport: itemCount := 0 p.printIndent() p.printSpaceBeforeIdentifier() p.print("import") p.printSpace() if s.DefaultName != nil { p.printSymbol(s.DefaultName.Ref) itemCount++ } if s.Items != nil { if itemCount > 0 { p.print(",") p.printSpace() } p.print("{") if !s.IsSingleLine { p.options.Indent++ } for i, item := range *s.Items { if i != 0 { p.print(",") } if s.IsSingleLine { p.printSpace() } else { p.printNewline() p.printIndent() } p.printClauseAlias(item.Alias) name := p.renamer.NameForSymbol(item.Name.Ref) if name != item.Alias { p.printSpace() p.printSpaceBeforeIdentifier() p.print("as ") p.printIdentifier(name) } } if !s.IsSingleLine { p.options.Indent-- p.printNewline() p.printIndent() } else if len(*s.Items) > 0 { p.printSpace() } p.print("}") itemCount++ } if s.StarNameLoc != nil { if itemCount > 0 { p.print(",") p.printSpace() } p.print("*") p.printSpace() p.print("as ") p.printSymbol(s.NamespaceRef) itemCount++ } if itemCount > 0 { p.printSpace() p.printSpaceBeforeIdentifier() p.print("from") p.printSpace() } p.printPath(s.ImportRecordIndex) p.printSemicolonAfterStatement() case *js_ast.SBlock: p.printIndent() p.printBlock(stmt.Loc, s.Stmts) p.printNewline() case *js_ast.SDebugger: p.printIndent() p.printSpaceBeforeIdentifier() p.print("debugger") p.printSemicolonAfterStatement() case *js_ast.SDirective: p.printIndent() p.printSpaceBeforeIdentifier() p.printQuotedUTF16(s.Value, false /* allowBacktick */) p.printSemicolonAfterStatement() case *js_ast.SBreak: p.printIndent() p.printSpaceBeforeIdentifier() p.print("break") if s.Label != nil { p.print(" ") p.printSymbol(s.Label.Ref) } p.printSemicolonAfterStatement() case *js_ast.SContinue: p.printIndent() p.printSpaceBeforeIdentifier() p.print("continue") if s.Label != nil { p.print(" ") p.printSymbol(s.Label.Ref) } p.printSemicolonAfterStatement() case *js_ast.SReturn: p.printIndent() p.printSpaceBeforeIdentifier() p.print("return") if s.ValueOrNil.Data != nil { p.printSpace() p.printExpr(s.ValueOrNil, js_ast.LLowest, 0) } p.printSemicolonAfterStatement() case *js_ast.SThrow: p.printIndent() p.printSpaceBeforeIdentifier() p.print("throw") p.printSpace() p.printExpr(s.Value, js_ast.LLowest, 0) p.printSemicolonAfterStatement() case *js_ast.SExpr: p.printIndent() p.stmtStart = len(p.js) p.printExpr(s.Value, js_ast.LLowest, exprResultIsUnused) p.printSemicolonAfterStatement() default: panic(fmt.Sprintf("Unexpected statement of type %T", stmt.Data)) } } type Options struct { OutputFormat config.Format RemoveWhitespace bool MangleSyntax bool ASCIIOnly bool LegalComments config.LegalComments AddSourceMappings bool Indent int ToModuleRef js_ast.Ref RuntimeRequireRef js_ast.Ref UnsupportedFeatures compat.JSFeature RequireOrImportMetaForSource func(uint32) RequireOrImportMeta // If we're writing out a source map, this table of line start indices lets // us do binary search on to figure out what line a given AST node came from LineOffsetTables []sourcemap.LineOffsetTable // This will be present if the input file had a source map. In that case we // want to map all the way back to the original input file(s). InputSourceMap *sourcemap.SourceMap } type RequireOrImportMeta struct { // CommonJS files will return the "require_*" wrapper function and an invalid // exports object reference. Lazily-initialized ESM files will return the // "init_*" wrapper function and the exports object for that file. WrapperRef js_ast.Ref ExportsRef js_ast.Ref IsWrapperAsync bool } type PrintResult struct { JS []byte // This source map chunk just contains the VLQ-encoded offsets for the "JS" // field above. It's not a full source map. The bundler will be joining many // source map chunks together to form the final source map. SourceMapChunk sourcemap.Chunk ExtractedLegalComments map[string]bool } func Print(tree js_ast.AST, symbols js_ast.SymbolMap, r renamer.Renamer, options Options) PrintResult { p := &printer{ symbols: symbols, renamer: r, importRecords: tree.ImportRecords, options: options, stmtStart: -1, exportDefaultStart: -1, arrowExprStart: -1, forOfInitStart: -1, prevOpEnd: -1, prevNumEnd: -1, prevRegExpEnd: -1, builder: sourcemap.MakeChunkBuilder(options.InputSourceMap, options.LineOffsetTables), } // Add the top-level directive if present if tree.Directive != "" { p.printQuotedUTF8(tree.Directive, options.ASCIIOnly) p.print(";") p.printNewline() } for _, part := range tree.Parts { for _, stmt := range part.Stmts { p.printStmt(stmt) p.printSemicolonIfNeeded() } } return PrintResult{ JS: p.js, ExtractedLegalComments: p.extractedLegalComments, SourceMapChunk: p.builder.GenerateChunk(p.js), } }