mirror of
https://github.com/hanzoai/esbuild.git
synced 2026-09-24 16:58:50 +00:00
437 lines
13 KiB
Go
437 lines
13 KiB
Go
package js_parser
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/evanw/esbuild/internal/ast"
|
|
"github.com/evanw/esbuild/internal/helpers"
|
|
"github.com/evanw/esbuild/internal/js_ast"
|
|
"github.com/evanw/esbuild/internal/logger"
|
|
"github.com/evanw/esbuild/internal/sourcemap"
|
|
)
|
|
|
|
// New specification: https://tc39.es/ecma426/
|
|
// Old specification: https://sourcemaps.info/spec.html
|
|
func ParseSourceMap(log logger.Log, source logger.Source) *sourcemap.SourceMap {
|
|
expr, ok := ParseJSON(log, source, JSONOptions{ErrorSuffix: " in source map"})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
obj, ok := expr.Data.(*js_ast.EObject)
|
|
tracker := logger.MakeLineColumnTracker(&source)
|
|
if !ok {
|
|
log.AddError(&tracker, logger.Range{Loc: expr.Loc}, "Invalid source map")
|
|
return nil
|
|
}
|
|
|
|
type sourceMapSection struct {
|
|
lineOffset int32
|
|
columnOffset int32
|
|
sourceMap *js_ast.EObject
|
|
}
|
|
|
|
var sections []sourceMapSection
|
|
hasSections := false
|
|
|
|
for _, prop := range obj.Properties {
|
|
if !helpers.UTF16EqualsString(prop.Key.Data.(*js_ast.EString).Value, "sections") {
|
|
continue
|
|
}
|
|
|
|
if value, ok := prop.ValueOrNil.Data.(*js_ast.EArray); ok {
|
|
for _, item := range value.Items {
|
|
if element, ok := item.Data.(*js_ast.EObject); ok {
|
|
var sectionLineOffset int32
|
|
var sectionColumnOffset int32
|
|
var sectionSourceMap *js_ast.EObject
|
|
|
|
for _, sectionProp := range element.Properties {
|
|
switch helpers.UTF16ToString(sectionProp.Key.Data.(*js_ast.EString).Value) {
|
|
case "offset":
|
|
if offsetValue, ok := sectionProp.ValueOrNil.Data.(*js_ast.EObject); ok {
|
|
for _, offsetProp := range offsetValue.Properties {
|
|
switch helpers.UTF16ToString(offsetProp.Key.Data.(*js_ast.EString).Value) {
|
|
case "line":
|
|
if lineValue, ok := offsetProp.ValueOrNil.Data.(*js_ast.ENumber); ok {
|
|
sectionLineOffset = int32(lineValue.Value)
|
|
}
|
|
|
|
case "column":
|
|
if columnValue, ok := offsetProp.ValueOrNil.Data.(*js_ast.ENumber); ok {
|
|
sectionColumnOffset = int32(columnValue.Value)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
log.AddError(&tracker, logger.Range{Loc: sectionProp.ValueOrNil.Loc}, "Expected \"offset\" to be an object")
|
|
return nil
|
|
}
|
|
|
|
case "map":
|
|
if mapValue, ok := sectionProp.ValueOrNil.Data.(*js_ast.EObject); ok {
|
|
sectionSourceMap = mapValue
|
|
} else {
|
|
log.AddError(&tracker, logger.Range{Loc: sectionProp.ValueOrNil.Loc}, "Expected \"map\" to be an object")
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
if sectionSourceMap != nil {
|
|
sections = append(sections, sourceMapSection{
|
|
lineOffset: sectionLineOffset,
|
|
columnOffset: sectionColumnOffset,
|
|
sourceMap: sectionSourceMap,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
log.AddError(&tracker, logger.Range{Loc: prop.ValueOrNil.Loc}, "Expected \"sections\" to be an array")
|
|
return nil
|
|
}
|
|
|
|
hasSections = true
|
|
break
|
|
}
|
|
|
|
if !hasSections {
|
|
sections = append(sections, sourceMapSection{
|
|
sourceMap: obj,
|
|
})
|
|
}
|
|
|
|
var sources []string
|
|
var sourcesContent []sourcemap.SourceContent
|
|
var names []string
|
|
var mappings mappingArray
|
|
var generatedLine int32
|
|
var generatedColumn int32
|
|
needSort := false
|
|
|
|
for _, section := range sections {
|
|
var sourcesArray []js_ast.Expr
|
|
var sourcesContentArray []js_ast.Expr
|
|
var namesArray []js_ast.Expr
|
|
var mappingsRaw []uint16
|
|
var mappingsStart int32
|
|
var sourceRoot string
|
|
hasVersion := false
|
|
|
|
for _, prop := range section.sourceMap.Properties {
|
|
switch helpers.UTF16ToString(prop.Key.Data.(*js_ast.EString).Value) {
|
|
case "version":
|
|
if value, ok := prop.ValueOrNil.Data.(*js_ast.ENumber); ok && value.Value == 3 {
|
|
hasVersion = true
|
|
}
|
|
|
|
case "mappings":
|
|
if value, ok := prop.ValueOrNil.Data.(*js_ast.EString); ok {
|
|
mappingsRaw = value.Value
|
|
mappingsStart = prop.ValueOrNil.Loc.Start + 1
|
|
}
|
|
|
|
case "sourceRoot":
|
|
if value, ok := prop.ValueOrNil.Data.(*js_ast.EString); ok {
|
|
sourceRoot = helpers.UTF16ToString(value.Value)
|
|
}
|
|
|
|
case "sources":
|
|
if value, ok := prop.ValueOrNil.Data.(*js_ast.EArray); ok {
|
|
sourcesArray = value.Items
|
|
}
|
|
|
|
case "sourcesContent":
|
|
if value, ok := prop.ValueOrNil.Data.(*js_ast.EArray); ok {
|
|
sourcesContentArray = value.Items
|
|
}
|
|
|
|
case "names":
|
|
if value, ok := prop.ValueOrNil.Data.(*js_ast.EArray); ok {
|
|
namesArray = value.Items
|
|
}
|
|
}
|
|
}
|
|
|
|
// Silently ignore the section if the version was missing or incorrect
|
|
if !hasVersion {
|
|
continue
|
|
}
|
|
|
|
mappingsLen := len(mappingsRaw)
|
|
sourcesLen := len(sourcesArray)
|
|
namesLen := len(namesArray)
|
|
|
|
// Silently ignore the section if the source map is pointless (i.e. empty)
|
|
if mappingsLen == 0 || sourcesLen == 0 {
|
|
continue
|
|
}
|
|
|
|
if section.lineOffset < generatedLine || (section.lineOffset == generatedLine && section.columnOffset < generatedColumn) {
|
|
needSort = true
|
|
}
|
|
|
|
lineOffset := section.lineOffset
|
|
columnOffset := section.columnOffset
|
|
sourceOffset := int32(len(sources))
|
|
nameOffset := int32(len(names))
|
|
|
|
generatedLine = lineOffset
|
|
generatedColumn = columnOffset
|
|
sourceIndex := sourceOffset
|
|
var originalLine int32
|
|
var originalColumn int32
|
|
originalName := nameOffset
|
|
|
|
current := 0
|
|
errorText := ""
|
|
errorLen := 0
|
|
|
|
// Parse the mappings
|
|
for current < mappingsLen {
|
|
// Handle a line break
|
|
if mappingsRaw[current] == ';' {
|
|
generatedLine++
|
|
generatedColumn = 0
|
|
current++
|
|
continue
|
|
}
|
|
|
|
// Read the generated column
|
|
generatedColumnDelta, i, ok := sourcemap.DecodeVLQUTF16(mappingsRaw[current:])
|
|
if !ok {
|
|
errorText = "Missing generated column"
|
|
errorLen = i
|
|
break
|
|
}
|
|
if generatedColumnDelta < 0 {
|
|
// This would mess up binary search
|
|
needSort = true
|
|
}
|
|
generatedColumn += generatedColumnDelta
|
|
if (generatedLine == lineOffset && generatedColumn < columnOffset) || generatedColumn < 0 {
|
|
errorText = fmt.Sprintf("Invalid generated column value: %d", generatedColumn)
|
|
errorLen = i
|
|
break
|
|
}
|
|
current += i
|
|
|
|
// According to the specification, it's valid for a mapping to have 1,
|
|
// 4, or 5 variable-length fields. Having one field means there's no
|
|
// original location information, which is pretty useless. Just ignore
|
|
// those entries.
|
|
if current == mappingsLen {
|
|
break
|
|
}
|
|
switch mappingsRaw[current] {
|
|
case ',':
|
|
current++
|
|
continue
|
|
case ';':
|
|
continue
|
|
}
|
|
|
|
// Read the original source
|
|
sourceIndexDelta, i, ok := sourcemap.DecodeVLQUTF16(mappingsRaw[current:])
|
|
if !ok {
|
|
errorText = "Missing source index"
|
|
errorLen = i
|
|
break
|
|
}
|
|
sourceIndex += sourceIndexDelta
|
|
if sourceIndex < sourceOffset || sourceIndex >= sourceOffset+int32(sourcesLen) {
|
|
errorText = fmt.Sprintf("Invalid source index value: %d", sourceIndex)
|
|
errorLen = i
|
|
break
|
|
}
|
|
current += i
|
|
|
|
// Read the original line
|
|
originalLineDelta, i, ok := sourcemap.DecodeVLQUTF16(mappingsRaw[current:])
|
|
if !ok {
|
|
errorText = "Missing original line"
|
|
errorLen = i
|
|
break
|
|
}
|
|
originalLine += originalLineDelta
|
|
if originalLine < 0 {
|
|
errorText = fmt.Sprintf("Invalid original line value: %d", originalLine)
|
|
errorLen = i
|
|
break
|
|
}
|
|
current += i
|
|
|
|
// Read the original column
|
|
originalColumnDelta, i, ok := sourcemap.DecodeVLQUTF16(mappingsRaw[current:])
|
|
if !ok {
|
|
errorText = "Missing original column"
|
|
errorLen = i
|
|
break
|
|
}
|
|
originalColumn += originalColumnDelta
|
|
if originalColumn < 0 {
|
|
errorText = fmt.Sprintf("Invalid original column value: %d", originalColumn)
|
|
errorLen = i
|
|
break
|
|
}
|
|
current += i
|
|
|
|
// Read the original name
|
|
var optionalName ast.Index32
|
|
if originalNameDelta, i, ok := sourcemap.DecodeVLQUTF16(mappingsRaw[current:]); ok {
|
|
originalName += originalNameDelta
|
|
if originalName < nameOffset || originalName >= nameOffset+int32(namesLen) {
|
|
errorText = fmt.Sprintf("Invalid name index value: %d", originalName)
|
|
errorLen = i
|
|
break
|
|
}
|
|
optionalName = ast.MakeIndex32(uint32(originalName))
|
|
current += i
|
|
}
|
|
|
|
// Handle the next character
|
|
if current < mappingsLen {
|
|
if c := mappingsRaw[current]; c == ',' {
|
|
current++
|
|
} else if c != ';' {
|
|
errorText = fmt.Sprintf("Invalid character after mapping: %q",
|
|
helpers.UTF16ToString(mappingsRaw[current:current+1]))
|
|
errorLen = 1
|
|
break
|
|
}
|
|
}
|
|
|
|
mappings = append(mappings, sourcemap.Mapping{
|
|
GeneratedLine: generatedLine,
|
|
GeneratedColumn: generatedColumn,
|
|
SourceIndex: sourceIndex,
|
|
OriginalLine: originalLine,
|
|
OriginalColumn: originalColumn,
|
|
OriginalName: optionalName,
|
|
})
|
|
}
|
|
|
|
if errorText != "" {
|
|
r := logger.Range{Loc: logger.Loc{Start: mappingsStart + int32(current)}, Len: int32(errorLen)}
|
|
log.AddID(logger.MsgID_SourceMap_InvalidSourceMappings, logger.Warning, &tracker, r,
|
|
fmt.Sprintf("Bad \"mappings\" data in source map at character %d: %s", current, errorText))
|
|
return nil
|
|
}
|
|
|
|
// Try resolving relative source URLs into absolute source URLs.
|
|
// See https://tc39.es/ecma426/#resolving-sources for details.
|
|
var sourceURLPrefix string
|
|
var baseURL *url.URL
|
|
if sourceRoot != "" {
|
|
if index := strings.LastIndexByte(sourceRoot, '/'); index != -1 {
|
|
sourceURLPrefix = sourceRoot[:index+1]
|
|
} else {
|
|
sourceURLPrefix = sourceRoot + "/"
|
|
}
|
|
}
|
|
if source.KeyPath.Namespace == "file" {
|
|
baseURL = helpers.FileURLFromFilePath(source.KeyPath.Text)
|
|
}
|
|
|
|
for _, item := range sourcesArray {
|
|
if element, ok := item.Data.(*js_ast.EString); ok {
|
|
sourcePath := sourceURLPrefix + helpers.UTF16ToString(element.Value)
|
|
sourceURL, err := url.Parse(sourcePath)
|
|
|
|
// Ignore URL parse errors (such as "%XY" being an invalid escape)
|
|
if err != nil {
|
|
sources = append(sources, sourcePath)
|
|
continue
|
|
}
|
|
|
|
// Resolve this URL relative to the enclosing directory
|
|
if baseURL != nil {
|
|
sourceURL = baseURL.ResolveReference(sourceURL)
|
|
}
|
|
sources = append(sources, sourceURL.String())
|
|
} else {
|
|
sources = append(sources, "")
|
|
}
|
|
}
|
|
|
|
if len(sourcesContentArray) > 0 {
|
|
// It's possible that one of the source maps inside "sections" has
|
|
// different lengths for the "sources" and "sourcesContent" arrays.
|
|
// This is bad because we need to us a single index to get the name
|
|
// of the source from "sources[i]" and the content of the source
|
|
// from "sourcesContent[i]".
|
|
//
|
|
// So if a previous source map had a shorter "sourcesContent" array
|
|
// than its "sources" array (or if the previous source map just had
|
|
// no "sourcesContent" array), expand our aggregated array to the
|
|
// right length by padding it out with empty entries.
|
|
sourcesContent = append(sourcesContent, make([]sourcemap.SourceContent, int(sourceOffset)-len(sourcesContent))...)
|
|
|
|
for i, item := range sourcesContentArray {
|
|
// Make sure we don't ever record more "sourcesContent" entries
|
|
// than there are "sources" entries, which is possible because
|
|
// these are two separate arrays in the source map JSON. We need
|
|
// to avoid this because that would mess up our shared indexing
|
|
// of the "sources" and "sourcesContent" arrays. See the above
|
|
// comment for more details.
|
|
if i == sourcesLen {
|
|
break
|
|
}
|
|
|
|
if element, ok := item.Data.(*js_ast.EString); ok {
|
|
sourcesContent = append(sourcesContent, sourcemap.SourceContent{
|
|
Value: element.Value,
|
|
Quoted: source.TextForRange(source.RangeOfString(item.Loc)),
|
|
})
|
|
} else {
|
|
sourcesContent = append(sourcesContent, sourcemap.SourceContent{})
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, item := range namesArray {
|
|
if element, ok := item.Data.(*js_ast.EString); ok {
|
|
names = append(names, helpers.UTF16ToString(element.Value))
|
|
} else {
|
|
names = append(names, "")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Silently fail if the source map is pointless (i.e. empty)
|
|
if len(sources) == 0 || len(mappings) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if needSort {
|
|
// If we get here, some mappings are out of order. Lines can't be out of
|
|
// order by construction but columns can. This is a pretty rare situation
|
|
// because almost all source map generators always write out mappings in
|
|
// order as they write the output instead of scrambling the order.
|
|
sort.Stable(mappings)
|
|
}
|
|
|
|
return &sourcemap.SourceMap{
|
|
Sources: sources,
|
|
SourcesContent: sourcesContent,
|
|
Mappings: mappings,
|
|
Names: names,
|
|
}
|
|
}
|
|
|
|
// This type is just so we can use Go's native sort function
|
|
type mappingArray []sourcemap.Mapping
|
|
|
|
func (a mappingArray) Len() int { return len(a) }
|
|
func (a mappingArray) Swap(i int, j int) { a[i], a[j] = a[j], a[i] }
|
|
|
|
func (a mappingArray) Less(i int, j int) bool {
|
|
ai := a[i]
|
|
aj := a[j]
|
|
return ai.GeneratedLine < aj.GeneratedLine || (ai.GeneratedLine == aj.GeneratedLine && ai.GeneratedColumn <= aj.GeneratedColumn)
|
|
}
|