Files
esbuild/internal/js_printer/js_printer.go
2020-11-10 01:09:42 -08:00

3242 lines
76 KiB
Go

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"
// "-- >" => "-- >"
// "< ! --" => "<! --"
if ((prev == js_ast.BinOpAdd || prev == js_ast.UnOpPos) && (next == js_ast.BinOpAdd || next == js_ast.UnOpPos || next == js_ast.UnOpPreInc)) ||
((prev == js_ast.BinOpSub || prev == js_ast.UnOpNeg) && (next == js_ast.BinOpSub || next == js_ast.UnOpNeg || next == js_ast.UnOpPreDec)) ||
(prev == js_ast.UnOpPostDec && next == js_ast.BinOpGt) ||
(prev == js_ast.UnOpNot && next == js_ast.UnOpPreDec && len(p.js) > 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),
}}
}