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) // Coordinates in source maps are stored using relative offsets for size // reasons. When joining together chunks of a source map that were emitted // in parallel for different parts of a file, we need to fix up the first // segment of each chunk to be relative to the end of the previous chunk. type SourceMapState struct { // This isn't stored in the source map. It's only used by the bundler to join // source map chunks together correctly. GeneratedLine int // These are stored in the source map in VLQ format. GeneratedColumn int SourceIndex int OriginalLine int OriginalColumn int } // Source map chunks are computed in parallel for speed. Each chunk is relative // to the zero state instead of being relative to the end state of the previous // chunk, since it's impossible to know the end state of the previous chunk in // a parallel computation. // // After all chunks are computed, they are joined together in a second pass. // This rewrites the first mapping in each chunk to be relative to the end // state of the previous chunk. func AppendSourceMapChunk(j *Joiner, prevEndState SourceMapState, startState SourceMapState, sourceMap []byte) { // Handle line breaks in between this mapping and the previous one if startState.GeneratedLine != 0 { j.AddBytes(bytes.Repeat([]byte{';'}, startState.GeneratedLine)) prevEndState.GeneratedColumn = 0 } // Skip past any leading semicolons, which indicate line breaks semicolons := 0 for sourceMap[semicolons] == ';' { semicolons++ } if semicolons > 0 { j.AddBytes(sourceMap[:semicolons]) sourceMap = sourceMap[semicolons:] prevEndState.GeneratedColumn = 0 startState.GeneratedColumn = 0 } // Strip off the first mapping from the buffer. The first mapping should be // for the start of the original file (the printer always generates one for // the start of the file). generatedColumn, i := sourcemap.DecodeVLQ(sourceMap, 0) sourceIndex, i := sourcemap.DecodeVLQ(sourceMap, i) originalLine, i := sourcemap.DecodeVLQ(sourceMap, i) originalColumn, i := sourcemap.DecodeVLQ(sourceMap, i) sourceMap = sourceMap[i:] // Rewrite the first mapping to be relative to the end state of the previous // chunk. We now know what the end state is because we're in the second pass // where all chunks have already been generated. startState.SourceIndex += sourceIndex startState.GeneratedColumn += generatedColumn startState.OriginalLine += originalLine startState.OriginalColumn += originalColumn j.AddBytes(appendMapping(nil, j.lastByte, prevEndState, startState)) // Then append everything after that without modification. j.AddBytes(sourceMap) } // Rewrite the source map to remove everything before "offset" and have the // generated position start from (0, 0) at that point. This is used when // erasing the variable declaration keyword from the start of a file. func RemovePrefixFromSourceMapChunk(buffer []byte, offset int) []byte { // Avoid an out-of-bounds array access if the offset is 0. This doesn't // happen normally but can happen if a source is remapped from another // source map that is missing mappings. This is the case with some file // in the "ant-design" project. if offset == 0 { return buffer } state := SourceMapState{} i := 0 // Accumulate mappings before the break point for i < offset { if buffer[i] == ';' { i++ continue } var si, ol, oc int _, i = sourcemap.DecodeVLQ(buffer, i) si, i = sourcemap.DecodeVLQ(buffer, i) ol, i = sourcemap.DecodeVLQ(buffer, i) oc, i = sourcemap.DecodeVLQ(buffer, i) state.SourceIndex += si state.OriginalLine += ol state.OriginalColumn += oc if i < len(buffer) && buffer[i] == ',' { i++ } } // Rewrite the mapping at the break point var si, ol, oc int _, i = sourcemap.DecodeVLQ(buffer, i) si, i = sourcemap.DecodeVLQ(buffer, i) ol, i = sourcemap.DecodeVLQ(buffer, i) oc, i = sourcemap.DecodeVLQ(buffer, i) state.SourceIndex += si state.OriginalLine += ol state.OriginalColumn += oc // Splice it in front of the buffer assuming it's big enough first := appendMapping(nil, 0, SourceMapState{}, state) buffer = buffer[i-len(first):] copy(buffer, first) return buffer } func appendMapping(buffer []byte, lastByte byte, prevState SourceMapState, currentState SourceMapState) []byte { // Put commas in between mappings if lastByte != 0 && lastByte != ';' && lastByte != '"' { buffer = append(buffer, ',') } // Record the generated column (the line is recorded using ';' elsewhere) buffer = append(buffer, sourcemap.EncodeVLQ(currentState.GeneratedColumn-prevState.GeneratedColumn)...) prevState.GeneratedColumn = currentState.GeneratedColumn // Record the generated source buffer = append(buffer, sourcemap.EncodeVLQ(currentState.SourceIndex-prevState.SourceIndex)...) prevState.SourceIndex = currentState.SourceIndex // Record the original line buffer = append(buffer, sourcemap.EncodeVLQ(currentState.OriginalLine-prevState.OriginalLine)...) prevState.OriginalLine = currentState.OriginalLine // Record the original column buffer = append(buffer, sourcemap.EncodeVLQ(currentState.OriginalColumn-prevState.OriginalColumn)...) prevState.OriginalColumn = currentState.OriginalColumn return buffer } // This provides an efficient way to join lots of big string and byte slices // together. It avoids the cost of repeatedly reallocating as the buffer grows // by measuring exactly how big the buffer should be and then allocating once. // This is a measurable speedup. type Joiner struct { lastByte byte strings []joinerString bytes []joinerBytes length uint32 } type joinerString struct { data string offset uint32 } type joinerBytes struct { data []byte offset uint32 } func (j *Joiner) AddString(data string) { if len(data) > 0 { j.lastByte = data[len(data)-1] } j.strings = append(j.strings, joinerString{data, j.length}) j.length += uint32(len(data)) } func (j *Joiner) AddBytes(data []byte) { if len(data) > 0 { j.lastByte = data[len(data)-1] } j.bytes = append(j.bytes, joinerBytes{data, j.length}) j.length += uint32(len(data)) } func (j *Joiner) LastByte() byte { return j.lastByte } func (j *Joiner) Length() uint32 { return j.length } func (j *Joiner) Done() []byte { buffer := make([]byte, j.length) for _, item := range j.strings { copy(buffer[item.offset:], item.data) } for _, item := range j.bytes { copy(buffer[item.offset:], item.data) } return buffer } 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) printQuotedUTF16(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 '\'': 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 } type printer struct { symbols js_ast.SymbolMap renamer renamer.Renamer importRecords []ast.ImportRecord options PrintOptions extractedComments map[string]bool needsSemicolon bool js []byte stmtStart int exportDefaultStart int arrowExprStart int prevOp js_ast.OpCode prevOpEnd int prevNumEnd int prevRegExpEnd int intToBytesBuffer [64]byte // For source maps sourceMap []byte prevLoc logger.Loc prevState SourceMapState lastGeneratedUpdate int generatedColumn int hasPrevState bool lineOffsetTables []lineOffsetTable // For splicing adjacent files together finalLocalSemi int firstDeclByteOffset uint32 firstDeclSourceMapOffset uint32 hasDecl bool // This is a workaround for a bug in the popular "source-map" library: // https://github.com/mozilla/source-map/issues/261. The library will // sometimes return null when querying a source map unless every line // starts with a mapping at column zero. // // The workaround is to replicate the previous mapping if a line ends // up not starting with a mapping. This is done lazily because we want // to avoid replicating the previous mapping if we don't need to. lineStartsWithMapping bool coverLinesWithoutMappings bool } type lineOffsetTable struct { byteOffsetToStartOfLine int32 // The source map specification is very loose and does not specify what // column numbers actually mean. The popular "source-map" library from Mozilla // appears to interpret them as counts of UTF-16 code units, so we generate // those too for compatibility. // // We keep mapping tables around to accelerate conversion from byte offsets // to UTF-16 code unit counts. However, this mapping takes up a lot of memory // and generates a lot of garbage. Since most JavaScript is ASCII and the // mapping for ASCII is 1:1, we avoid creating a table for ASCII-only lines // as an optimization. byteOffsetToFirstNonASCII int32 columnsForNonASCII []int32 } 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) { value := js_lexer.StringToUTF16(text) c := p.bestQuoteCharForString(value, allowBacktick) p.print(c) p.printQuotedUTF16(value, rune(c[0])) p.print(c) } func (p *printer) addSourceMapping(loc logger.Loc) { if p.options.SourceForSourceMap == nil || loc == p.prevLoc { return } p.prevLoc = loc // Binary search to find the line lineOffsetTables := p.lineOffsetTables count := len(lineOffsetTables) originalLine := 0 for count > 0 { step := count / 2 i := originalLine + step if lineOffsetTables[i].byteOffsetToStartOfLine <= loc.Start { originalLine = i + 1 count = count - step - 1 } else { count = step } } originalLine-- // Use the line to compute the column line := &lineOffsetTables[originalLine] originalColumn := int(loc.Start - line.byteOffsetToStartOfLine) if line.columnsForNonASCII != nil && originalColumn >= int(line.byteOffsetToFirstNonASCII) { originalColumn = int(line.columnsForNonASCII[originalColumn-int(line.byteOffsetToFirstNonASCII)]) } p.updateGeneratedLineAndColumn() // If this line doesn't start with a mapping and we're about to add a mapping // that's not at the start, insert a mapping first so the line starts with one. if p.coverLinesWithoutMappings && !p.lineStartsWithMapping && p.generatedColumn > 0 && p.hasPrevState { p.appendMappingWithoutRemapping(SourceMapState{ GeneratedLine: p.prevState.GeneratedLine, GeneratedColumn: 0, SourceIndex: p.prevState.SourceIndex, OriginalLine: p.prevState.OriginalLine, OriginalColumn: p.prevState.OriginalColumn, }) } p.appendMapping(SourceMapState{ GeneratedLine: p.prevState.GeneratedLine, GeneratedColumn: p.generatedColumn, OriginalLine: originalLine, OriginalColumn: originalColumn, }) // This line now has a mapping on it, so don't insert another one p.lineStartsWithMapping = true } // Scan over the printed text since the last source mapping and update the // generated line and column numbers func (p *printer) updateGeneratedLineAndColumn() { for i, c := range string(p.js[p.lastGeneratedUpdate:]) { switch c { case '\r', '\n', '\u2028', '\u2029': // Handle Windows-specific "\r\n" newlines if c == '\r' { newlineCheck := p.lastGeneratedUpdate + i + 1 if newlineCheck < len(p.js) && p.js[newlineCheck] == '\n' { continue } } // If we're about to move to the next line and the previous line didn't have // any mappings, add a mapping at the start of the previous line. if p.coverLinesWithoutMappings && !p.lineStartsWithMapping && p.hasPrevState { p.appendMappingWithoutRemapping(SourceMapState{ GeneratedLine: p.prevState.GeneratedLine, GeneratedColumn: 0, SourceIndex: p.prevState.SourceIndex, OriginalLine: p.prevState.OriginalLine, OriginalColumn: p.prevState.OriginalColumn, }) } p.prevState.GeneratedLine++ p.prevState.GeneratedColumn = 0 p.generatedColumn = 0 p.sourceMap = append(p.sourceMap, ';') // This new line doesn't have a mapping yet p.lineStartsWithMapping = false default: // Mozilla's "source-map" library counts columns using UTF-16 code units if c <= 0xFFFF { p.generatedColumn++ } else { p.generatedColumn += 2 } } } p.lastGeneratedUpdate = len(p.js) } func generateLineOffsetTables(contents string, approximateLineCount int32) []lineOffsetTable { var columnsForNonASCII []int32 byteOffsetToFirstNonASCII := int32(0) lineByteOffset := 0 columnByteOffset := 0 column := int32(0) // Preallocate the top-level table using the approximate line count from the lexer lineOffsetTables := make([]lineOffsetTable, 0, approximateLineCount) for i, c := range contents { // Mark the start of the next line if column == 0 { lineByteOffset = i } // Start the mapping if this character is non-ASCII if c > 0x7F && columnsForNonASCII == nil { columnByteOffset = i - lineByteOffset byteOffsetToFirstNonASCII = int32(columnByteOffset) columnsForNonASCII = []int32{} } // Update the per-byte column offsets if columnsForNonASCII != nil { for lineBytesSoFar := i - lineByteOffset; columnByteOffset <= lineBytesSoFar; columnByteOffset++ { columnsForNonASCII = append(columnsForNonASCII, column) } } switch c { case '\r', '\n', '\u2028', '\u2029': // Handle Windows-specific "\r\n" newlines if c == '\r' { if i+1 < len(contents) && contents[i+1] == '\n' { column++ continue } } lineOffsetTables = append(lineOffsetTables, lineOffsetTable{ byteOffsetToStartOfLine: int32(lineByteOffset), byteOffsetToFirstNonASCII: byteOffsetToFirstNonASCII, columnsForNonASCII: columnsForNonASCII, }) columnByteOffset = 0 byteOffsetToFirstNonASCII = 0 columnsForNonASCII = nil column = 0 default: // Mozilla's "source-map" library counts columns using UTF-16 code units if c <= 0xFFFF { column++ } else { column += 2 } } } // Mark the start of the next line if column == 0 { lineByteOffset = len(contents) } // Do one last update for the column at the end of the file if columnsForNonASCII != nil { for lineBytesSoFar := len(contents) - lineByteOffset; columnByteOffset <= lineBytesSoFar; columnByteOffset++ { columnsForNonASCII = append(columnsForNonASCII, column) } } lineOffsetTables = append(lineOffsetTables, lineOffsetTable{ byteOffsetToStartOfLine: int32(lineByteOffset), byteOffsetToFirstNonASCII: byteOffsetToFirstNonASCII, columnsForNonASCII: columnsForNonASCII, }) return lineOffsetTables } func (p *printer) appendMapping(currentState SourceMapState) { // If the input file had a source map, map all the way back to the original if p.options.InputSourceMap != nil { mapping := p.options.InputSourceMap.Find( int32(currentState.OriginalLine), int32(currentState.OriginalColumn)) // Some locations won't have a mapping if mapping == nil { return } currentState.SourceIndex = int(mapping.SourceIndex) currentState.OriginalLine = int(mapping.OriginalLine) currentState.OriginalColumn = int(mapping.OriginalColumn) } p.appendMappingWithoutRemapping(currentState) } func (p *printer) appendMappingWithoutRemapping(currentState SourceMapState) { var lastByte byte if len(p.sourceMap) != 0 { lastByte = p.sourceMap[len(p.sourceMap)-1] } p.sourceMap = appendMapping(p.sourceMap, lastByte, p.prevState, currentState) p.prevState = currentState p.hasPrevState = true } 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) { p.printSpaceBeforeIdentifier() p.printIdentifier(p.renamer.NameForSymbol(ref)) } func CanQuoteIdentifier(name string, options *config.Options) bool { return js_lexer.IsIdentifier(name) && (!options.ASCIIOnly || !options.UnsupportedJSFeatures.Has(compat.UnicodeEscapes) || !js_lexer.ContainsNonBMPCodePoint(name)) } func (p *printer) canPrintIdentifier(name string) bool { return js_lexer.IsIdentifier(name) && (!p.options.ASCIIOnly || !p.options.UnsupportedFeatures.Has(compat.UnicodeEscapes) || !js_lexer.ContainsNonBMPCodePoint(name)) } func (p *printer) canPrintIdentifierUTF16(name []uint16) bool { return js_lexer.IsIdentifierUTF16(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) { temp := make([]byte, utf8.UTFMax) 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) addSourceMappingForDecl(loc logger.Loc) { if !p.hasDecl { p.hasDecl = true p.firstDeclByteOffset = uint32(len(p.js)) p.firstDeclSourceMapOffset = uint32(len(p.sourceMap)) p.prevLoc.Start = -1 // Force a new source mapping to be generated p.addSourceMapping(loc) } } func (p *printer) printBinding(binding js_ast.Binding) { switch b := binding.Data.(type) { case *js_ast.BMissing: case *js_ast.BIdentifier: p.printSpaceBeforeIdentifier() p.addSourceMappingForDecl(binding.Loc) p.printSymbol(b.Ref) case *js_ast.BArray: p.addSourceMappingForDecl(binding.Loc) 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.DefaultValue != nil { p.printSpace() p.print("=") p.printSpace() p.printExpr(*item.DefaultValue, 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.addSourceMappingForDecl(binding.Loc) 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() } } if !b.IsSingleLine { 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.DefaultValue != nil { p.printSpace() p.print("=") p.printSpace() p.printExpr(*property.DefaultValue, js_ast.LComma, 0) } continue } if str, ok := property.Key.Data.(*js_ast.EString); ok { if 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.DefaultValue != nil { p.printSpace() p.print("=") p.printSpace() p.printExpr(*property.DefaultValue, js_ast.LComma, 0) } continue } } else { p.printExpr(property.Key, js_ast.LLowest, 0) } } else { p.printExpr(property.Key, js_ast.LLowest, 0) } p.print(":") p.printSpace() } p.printBinding(property.Value) if property.DefaultValue != nil { p.printSpace() p.print("=") p.printSpace() p.printExpr(*property.DefaultValue, js_ast.LComma, 0) } } if !b.IsSingleLine { p.options.Indent-- p.printNewline() p.printIndent() } } 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].Default == 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.Default != nil { p.printSpace() p.print("=") p.printSpace() p.printExpr(*arg.Default, 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.Stmts) } func (p *printer) printClass(class js_ast.Class) { if class.Extends != nil { p.print(" extends") p.printSpace() p.printExpr(*class.Extends, js_ast.LNew-1, 0) } p.printSpace() 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.Value == 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.Value, 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 item.Value != nil { if fn, ok := item.Value.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.Value != nil { if fn, ok := item.Value.Data.(*js_ast.EFunction); item.IsMethod && ok { p.printFn(fn.Fn) return } p.print(":") p.printSpace() p.printExpr(*item.Value, js_ast.LComma, 0) } if item.Initializer != nil { p.printSpace() p.print("=") p.printSpace() p.printExpr(*item.Initializer, 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 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.Value != nil { switch e := item.Value.Data.(type) { case *js_ast.EIdentifier: if js_lexer.UTF16EqualsString(key.Value, p.renamer.NameForSymbol(e.Ref)) { if item.Initializer != nil { p.printSpace() p.print("=") p.printSpace() p.printExpr(*item.Initializer, 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.Initializer != nil { p.printSpace() p.print("=") p.printSpace() p.printExpr(*item.Initializer, js_ast.LComma, 0) } return } } } } else { c := p.bestQuoteCharForString(key.Value, false /* allowBacktick */) p.print(c) p.printQuotedUTF16(key.Value, rune(c[0])) p.print(c) } default: p.printExpr(item.Key, js_ast.LLowest, 0) } if item.Kind != js_ast.PropertyNormal { f, ok := item.Value.Data.(*js_ast.EFunction) if ok { p.printFn(f.Fn) return } } if item.Value != nil { if fn, ok := item.Value.Data.(*js_ast.EFunction); item.IsMethod && ok { p.printFn(fn.Fn) return } p.print(":") p.printSpace() p.printExpr(*item.Value, js_ast.LComma, 0) } if item.Initializer != nil { p.printSpace() p.print("=") p.printSpace() p.printExpr(*item.Initializer, js_ast.LComma, 0) } } func (p *printer) bestQuoteCharForString(data []uint16, allowBacktick bool) string { 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 = "`" } return c } type requireCallArgs struct { isES6Import bool mustReturnPromise bool } func (p *printer) printRequireOrImportExpr(importRecordIndex uint32, leadingInteriorComments []js_ast.Comment) { record := &p.importRecords[importRecordIndex] p.printSpaceBeforeIdentifier() // Preserve "import()" expressions that don't point inside the bundle if record.SourceIndex == nil && record.Kind == ast.ImportDynamic && p.options.OutputFormat.KeepES6ImportExportSyntax() { p.print("import(") if len(leadingInteriorComments) > 0 { p.printNewline() p.options.Indent++ for _, comment := range leadingInteriorComments { p.printIndentedComment(comment.Text) } p.printIndent() } p.printQuotedUTF8(record.Path.Text, true /* allowBacktick */) if len(leadingInteriorComments) > 0 { p.printNewline() p.options.Indent-- p.printIndent() } p.print(")") return } // Make sure "import()" expressions return promises if record.Kind == ast.ImportDynamic { if p.options.RemoveWhitespace { p.print("Promise.resolve().then(()=>") } else { p.print("Promise.resolve().then(() => ") } } // Make sure CommonJS imports are converted to ES6 if necessary if record.WrapWithToModule { p.printSymbol(p.options.ToModuleRef) p.print("(") } // If this import points inside the bundle, then call the "require()" // function for that module directly. The linker must ensure that the // module's require function exists by this point. Otherwise, fall back to a // bare "require()" call. Then it's up to the user to provide it. if record.SourceIndex != nil { p.printSymbol(p.options.WrapperRefForSource(*record.SourceIndex)) p.print("()") } else { p.print("require(") p.printQuotedUTF8(record.Path.Text, true /* allowBacktick */) p.print(")") } if record.WrapWithToModule { p.print(")") } if record.Kind == ast.ImportDynamic { p.print(")") } } const ( forbidCall = 1 << iota forbidIn hasNonOptionalChainParent ) 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 int) { 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.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 targetFlags := 0 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 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.ERequire: wrap := level >= js_ast.LNew || (flags&forbidCall) != 0 if wrap { p.print("(") } p.printRequireOrImportExpr(e.ImportRecordIndex, nil) if wrap { p.print(")") } case *js_ast.ERequireResolve: 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.EImport: wrap := level >= js_ast.LNew || (flags&forbidCall) != 0 if wrap { p.print("(") } var leadingInteriorComments []js_ast.Comment if !p.options.RemoveWhitespace { leadingInteriorComments = e.LeadingInteriorComments } if e.ImportRecordIndex != nil { p.printRequireOrImportExpr(*e.ImportRecordIndex, leadingInteriorComments) } else { // Handle non-string expressions p.printSpaceBeforeIdentifier() p.print("import(") if len(leadingInteriorComments) > 0 { p.printNewline() p.options.Indent++ for _, comment := range e.LeadingInteriorComments { p.printIndentedComment(comment.Text) } p.printIndent() } p.printExpr(e.Expr, 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 e.OptionalChain == js_ast.OptionalChainStart { p.print("?") } 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(" ") } p.print(".") p.addSourceMapping(e.NameLoc) p.printIdentifier(e.Name) } else { 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.Value != nil { p.arrowExprStart = len(p.js) p.printExpr(*s.Value, js_ast.LComma, 0) wasPrinted = true } } if !wasPrinted { p.printBlock(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() } } if !e.IsSingleLine { p.printNewline() p.printIndent() } p.printProperty(item) } if !e.IsSingleLine { p.options.Indent-- p.printNewline() p.printIndent() } } 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.printQuotedUTF16(e.Value, '`') p.print("`") return } c := p.bestQuoteCharForString(e.Value, true /* allowBacktick */) p.print(c) p.printQuotedUTF16(e.Value, rune(c[0])) p.print(c) case *js_ast.ETemplate: // Convert no-substitution template literals into strings if it's smaller if p.options.MangleSyntax && e.Tag == nil && len(e.Parts) == 0 { c := p.bestQuoteCharForString(e.Head, true /* allowBacktick */) p.print(c) p.printQuotedUTF16(e.Head, rune(c[0])) p.print(c) return } if e.Tag != nil { p.printExpr(*e.Tag, js_ast.LPostfix, 0) } p.print("`") if e.Tag != nil { p.print(e.HeadRaw) } else { p.printQuotedUTF16(e.Head, '`') } for _, part := range e.Parts { p.print("${") p.printExpr(part.Value, js_ast.LLowest, 0) p.print("}") if e.Tag != nil { p.print(part.TailRaw) } else { p.printQuotedUTF16(part.Tail, '`') } } p.print("`") case *js_ast.ERegExp: buffer := p.js n := len(buffer) // Avoid forming a single-line comment if n > 0 && buffer[n-1] == '/' { 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 { p.printSpaceBeforeIdentifier() p.print("Infinity") } else if value == negativeInfinity { if level >= js_ast.LPrefix { p.print("(-Infinity)") } else { p.printSpaceBeforeOperator(js_ast.UnOpNeg) p.print("-Infinity") } } 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: p.printSpaceBeforeIdentifier() p.printSymbol(e.Ref) 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 { p.printSymbol(symbol.NamespaceAlias.NamespaceRef) alias := symbol.NamespaceAlias.Alias if p.canPrintIdentifier(alias) { p.print(".") p.printIdentifier(alias) } else { p.print("[") p.printQuotedUTF8(alias, true /* allowBacktick */) 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, 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.Value != nil { if e.IsStar { p.print("*") } p.printSpace() p.printExpr(*e.Value, 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.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 } } } 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.updateGeneratedLineAndColumn() p.finalLocalSemi = p.generatedColumn p.printSemicolonAfterStatement() } func (p *printer) printForLoopInit(init js_ast.Stmt) { switch s := init.Data.(type) { case *js_ast.SExpr: p.printExpr(s.Value, js_ast.LLowest, forbidIn) case *js_ast.SLocal: switch s.Kind { case js_ast.LocalVar: p.printDecls("var", s.Decls, forbidIn) case js_ast.LocalLet: p.printDecls("let", s.Decls, forbidIn) case js_ast.LocalConst: p.printDecls("const", s.Decls, forbidIn) } default: panic("Internal error") } } func (p *printer) printDecls(keyword string, decls []js_ast.Decl, flags int) { p.print(keyword) p.printSpace() for i, decl := range decls { if i != 0 { p.print(",") p.printSpace() } p.printBinding(decl.Binding) if decl.Value != nil { p.printSpace() p.print("=") p.printSpace() p.printExpr(*decl.Value, 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(block.Stmts) p.printNewline() } else { p.printNewline() p.options.Indent++ p.printStmt(body) p.options.Indent-- } } func (p *printer) printBlock(stmts []js_ast.Stmt) { 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.No == nil { return true } s = current.No.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(yes.Stmts) if s.No != 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.No != nil { p.printSpace() } else { p.printNewline() } } else { p.printNewline() p.options.Indent++ p.printStmt(s.Yes) p.options.Indent-- if s.No != nil { p.printIndent() } } if s.No != nil { p.printSemicolonIfNeeded() p.printSpaceBeforeIdentifier() p.print("else") if no, ok := s.No.Data.(*js_ast.SBlock); ok { p.printSpace() p.printBlock(no.Stmts) p.printNewline() } else if no, ok := s.No.Data.(*js_ast.SIf); ok { p.printIf(no) } else { p.printNewline() p.options.Indent++ p.printStmt(*s.No) p.options.Indent-- } } } func (p *printer) printIndentedComment(text string) { if strings.HasPrefix(text, "/*") { // Re-indent multi-line comments for { newline := strings.IndexByte(text, '\n') if newline == -1 { break } p.printIndent() p.print(text[:newline+1]) text = text[newline+1:] } p.printIndent() p.print(text) p.printNewline() } else { // Print a mandatory newline after single-line comments p.printIndent() p.print(text) p.print("\n") } } 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 p.options.ExtractComments { if p.extractedComments == nil { p.extractedComments = make(map[string]bool) } p.extractedComments[text] = true break } 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() if s.Value.Expr != nil { // Functions and classes must be wrapped to avoid confusion with their statement forms p.exportDefaultStart = len(p.js) p.printExpr(*s.Value.Expr, js_ast.LComma, 0) p.printSemicolonAfterStatement() return } switch s2 := s.Value.Stmt.Data.(type) { 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.printSpaceBeforeIdentifier() p.printIdentifier(s.Alias.OriginalName) p.printSpace() p.printSpaceBeforeIdentifier() } p.print("from") p.printSpace() p.printQuotedUTF8(p.importRecords[s.ImportRecordIndex].Path.Text, false /* allowBacktick */) 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() } } if !s.IsSingleLine { p.printNewline() p.printIndent() } name := p.renamer.NameForSymbol(item.Name.Ref) p.printIdentifier(name) if name != item.Alias { p.print(" as ") p.printIdentifier(item.Alias) } } if !s.IsSingleLine { p.options.Indent-- p.printNewline() p.printIndent() } 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() } } if !s.IsSingleLine { p.printNewline() p.printIndent() } p.printIdentifier(item.OriginalName) if item.OriginalName != item.Alias { p.print(" as ") p.printIdentifier(item.Alias) } } if !s.IsSingleLine { p.options.Indent-- p.printNewline() p.printIndent() } p.print("}") p.printSpace() p.print("from") p.printSpace() p.printQuotedUTF8(p.importRecords[s.ImportRecordIndex].Path.Text, false /* allowBacktick */) 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(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) 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.printForLoopInit(s.Init) 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.Body) if s.Catch != nil { p.printSpace() p.print("catch") if s.Catch.Binding != nil { p.printSpace() p.print("(") p.printBinding(*s.Catch.Binding) p.print(")") } p.printSpace() p.printBlock(s.Catch.Body) } if s.Finally != nil { p.printSpace() p.print("finally") p.printSpace() p.printBlock(s.Finally.Stmts) } p.printNewline() case *js_ast.SFor: p.printIndent() p.printSpaceBeforeIdentifier() p.print("for") p.printSpace() p.print("(") if s.Init != nil { p.printForLoopInit(*s.Init) } p.print(";") p.printSpace() if s.Test != nil { p.printExpr(*s.Test, js_ast.LLowest, 0) } p.print(";") p.printSpace() if s.Update != nil { p.printExpr(*s.Update, 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.Value != nil { p.print("case") p.printSpace() p.printExpr(*c.Value, 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(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() } } if !s.IsSingleLine { p.printNewline() p.printIndent() } p.printIdentifier(item.Alias) name := p.renamer.NameForSymbol(item.Name.Ref) if name != item.Alias { p.print(" as ") p.printIdentifier(name) } } if !s.IsSingleLine { p.options.Indent-- p.printNewline() p.printIndent() } 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.printQuotedUTF8(p.importRecords[s.ImportRecordIndex].Path.Text, false /* allowBacktick */) p.printSemicolonAfterStatement() case *js_ast.SBlock: p.printIndent() p.printBlock(s.Stmts) p.printNewline() case *js_ast.SDebugger: p.printIndent() p.printSpaceBeforeIdentifier() p.print("debugger") p.printSemicolonAfterStatement() case *js_ast.SDirective: c := p.bestQuoteCharForString(s.Value, false /* allowBacktick */) p.printIndent() p.printSpaceBeforeIdentifier() p.print(c) p.printQuotedUTF16(s.Value, rune(c[0])) p.print(c) 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.Value != nil { p.printSpace() p.printExpr(*s.Value, 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, 0) p.printSemicolonAfterStatement() default: panic(fmt.Sprintf("Unexpected statement of type %T", stmt.Data)) } } func (p *printer) shouldIgnoreSourceMap() bool { for _, c := range p.sourceMap { if c != ';' { return false } } return true } type PrintOptions struct { OutputFormat config.Format RemoveWhitespace bool MangleSyntax bool ASCIIOnly bool ExtractComments bool Indent int ToModuleRef js_ast.Ref WrapperRefForSource func(uint32) js_ast.Ref UnsupportedFeatures compat.JSFeature // This contains the contents of the input file to map back to in the source // map. If it's nil that means we're not generating source maps. SourceForSourceMap *logger.Source // 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 QuotedSource struct { // These are quoted ahead of time instead of during source map generation so // the quoting happens in parallel instead of in serial QuotedPath []byte QuotedContents []byte } type SourceMapChunk struct { Buffer []byte // There may be more than one source for this chunk if the file being printed // has an associated source map. In that case the "source index" values in // the buffer are 0-based indices into this array. The source index of the // first mapping will be adjusted when the chunks are joined together. Since // the source indices are encoded using a delta from the previous source // index, none of the other source indices need to be modified while joining. QuotedSources []QuotedSource // This end state will be used to rewrite the start of the following source // map chunk so that the delta-encoded VLQ numbers are preserved. EndState SourceMapState // There probably isn't a source mapping at the end of the file (nor should // there be) but if we're appending another source map chunk after this one, // we'll need to know how many characters were in the last line we generated. FinalGeneratedColumn int // Like "FinalGeneratedColumn" but for the generated column of the last // semicolon for a "SLocal" statement. FinalLocalSemi int ShouldIgnore bool } func createPrinter( symbols js_ast.SymbolMap, r renamer.Renamer, importRecords []ast.ImportRecord, options PrintOptions, approximateLineCount int32, ) *printer { p := &printer{ symbols: symbols, renamer: r, importRecords: importRecords, options: options, stmtStart: -1, exportDefaultStart: -1, arrowExprStart: -1, prevOpEnd: -1, prevNumEnd: -1, prevRegExpEnd: -1, prevLoc: logger.Loc{Start: -1}, // We automatically repeat the previous source mapping if we ever generate // a line that doesn't start with a mapping. This helps give files more // complete mapping coverage without gaps. // // However, we probably shouldn't do this if the input file has a nested // source map that we will be remapping through. We have no idea what state // that source map is in and it could be pretty scrambled. // // I've seen cases where blindly repeating the last mapping for subsequent // lines gives very strange and unhelpful results with source maps from // other tools. coverLinesWithoutMappings: options.InputSourceMap == nil, } // If we're writing out a source map, prepare a table of line start indices // to do binary search on to figure out what line a given AST node came from if options.SourceForSourceMap != nil { p.lineOffsetTables = generateLineOffsetTables(options.SourceForSourceMap.Contents, approximateLineCount) } return p } 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 SourceMapChunk ExtractedComments map[string]bool // These are used when stripping off the leading variable declaration FirstDeclByteOffset uint32 FirstDeclSourceMapOffset uint32 } func Print(tree js_ast.AST, symbols js_ast.SymbolMap, r renamer.Renamer, options PrintOptions) PrintResult { p := createPrinter(symbols, r, tree.ImportRecords, options, tree.ApproximateLineCount) for _, part := range tree.Parts { for _, stmt := range part.Stmts { p.printStmt(stmt) p.printSemicolonIfNeeded() } } p.updateGeneratedLineAndColumn() return PrintResult{ JS: p.js, ExtractedComments: p.extractedComments, SourceMapChunk: SourceMapChunk{ Buffer: p.sourceMap, QuotedSources: quotedSources(&tree, &options), EndState: p.prevState, FinalGeneratedColumn: p.generatedColumn, FinalLocalSemi: p.finalLocalSemi, ShouldIgnore: p.shouldIgnoreSourceMap(), }, FirstDeclByteOffset: p.firstDeclByteOffset, FirstDeclSourceMapOffset: p.firstDeclSourceMapOffset, } } func PrintExpr(expr js_ast.Expr, symbols js_ast.SymbolMap, r renamer.Renamer, options PrintOptions) PrintResult { p := createPrinter(symbols, r, nil, options, 0) p.printExpr(expr, js_ast.LLowest, 0) p.updateGeneratedLineAndColumn() return PrintResult{ JS: p.js, ExtractedComments: p.extractedComments, SourceMapChunk: SourceMapChunk{ Buffer: p.sourceMap, QuotedSources: quotedSources(nil, &options), EndState: p.prevState, FinalGeneratedColumn: p.generatedColumn, ShouldIgnore: p.shouldIgnoreSourceMap(), }, } } func quotedSources(tree *js_ast.AST, options *PrintOptions) []QuotedSource { if options.SourceForSourceMap == nil { return nil } if sm := options.InputSourceMap; sm != nil { results := make([]QuotedSource, len(sm.Sources)) for i, source := range sm.Sources { contents := []byte("null") if i < len(sm.SourcesContent) { if value := sm.SourcesContent[i]; value != nil { contents = QuoteForJSON(*value, options.ASCIIOnly) } } results[i] = QuotedSource{ QuotedPath: QuoteForJSON(source, options.ASCIIOnly), QuotedContents: contents, } } return results } return []QuotedSource{{ QuotedPath: QuoteForJSON(options.SourceForSourceMap.PrettyPath, options.ASCIIOnly), QuotedContents: QuoteForJSON(options.SourceForSourceMap.Contents, options.ASCIIOnly), }} }