314 lines
9.3 KiB
Go
314 lines
9.3 KiB
Go
package sourcemap
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"slices"
|
|
"strings"
|
|
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/debug"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
|
|
"github.com/go-json-experiment/json"
|
|
)
|
|
|
|
type Host interface {
|
|
UseCaseSensitiveFileNames() bool
|
|
GetECMALineInfo(fileName string) *ECMALineInfo
|
|
ReadFile(fileName string) (string, bool)
|
|
}
|
|
|
|
// Similar to `Mapping`, but position-based.
|
|
type MappedPosition struct {
|
|
generatedPosition int
|
|
sourcePosition int
|
|
sourceIndex SourceIndex
|
|
nameIndex NameIndex
|
|
}
|
|
|
|
const (
|
|
missingPosition = -1
|
|
)
|
|
|
|
func (m *MappedPosition) isSourceMappedPosition() bool {
|
|
return m.sourceIndex != MissingSource && m.sourcePosition != missingPosition
|
|
}
|
|
|
|
type SourceMappedPosition = MappedPosition
|
|
|
|
// Maps source positions to generated positions and vice versa.
|
|
type DocumentPositionMapper struct {
|
|
useCaseSensitiveFileNames bool
|
|
|
|
sourceFileAbsolutePaths []string
|
|
sourceToSourceIndexMap map[string]SourceIndex
|
|
generatedAbsoluteFilePath string
|
|
|
|
generatedMappings []*MappedPosition
|
|
sourceMappings map[SourceIndex][]*SourceMappedPosition
|
|
}
|
|
|
|
func createDocumentPositionMapper(host Host, sourceMap *RawSourceMap, mapPath string) *DocumentPositionMapper {
|
|
mapDirectory := tspath.GetDirectoryPath(mapPath)
|
|
var sourceRoot string
|
|
if sourceMap.SourceRoot != "" {
|
|
sourceRoot = tspath.GetNormalizedAbsolutePath(sourceMap.SourceRoot, mapDirectory)
|
|
} else {
|
|
sourceRoot = mapDirectory
|
|
}
|
|
generatedAbsoluteFilePath := tspath.GetNormalizedAbsolutePath(sourceMap.File, mapDirectory)
|
|
sourceFileAbsolutePaths := core.Map(sourceMap.Sources, func(source string) string {
|
|
return tspath.GetNormalizedAbsolutePath(source, sourceRoot)
|
|
})
|
|
useCaseSensitiveFileNames := host.UseCaseSensitiveFileNames()
|
|
sourceToSourceIndexMap := make(map[string]SourceIndex, len(sourceFileAbsolutePaths))
|
|
for i, source := range sourceFileAbsolutePaths {
|
|
sourceToSourceIndexMap[tspath.GetCanonicalFileName(source, useCaseSensitiveFileNames)] = SourceIndex(i)
|
|
}
|
|
|
|
var decodedMappings []*MappedPosition
|
|
var generatedMappings []*MappedPosition
|
|
sourceMappings := make(map[SourceIndex][]*SourceMappedPosition)
|
|
|
|
// getDecodedMappings()
|
|
decoder := DecodeMappings(sourceMap.Mappings)
|
|
for mapping := range decoder.Values() {
|
|
// processMapping()
|
|
generatedPosition := -1
|
|
lineInfo := host.GetECMALineInfo(generatedAbsoluteFilePath)
|
|
if lineInfo != nil {
|
|
generatedPosition = scanner.ComputePositionOfLineAndCharacterEx(
|
|
lineInfo.lineStarts,
|
|
mapping.GeneratedLine,
|
|
mapping.GeneratedCharacter,
|
|
&lineInfo.text,
|
|
true, /*allowEdits*/
|
|
)
|
|
}
|
|
|
|
sourcePosition := -1
|
|
if mapping.IsSourceMapping() {
|
|
lineInfo := host.GetECMALineInfo(sourceFileAbsolutePaths[mapping.SourceIndex])
|
|
if lineInfo != nil {
|
|
pos := scanner.ComputePositionOfLineAndCharacterEx(
|
|
lineInfo.lineStarts,
|
|
mapping.SourceLine,
|
|
mapping.SourceCharacter,
|
|
&lineInfo.text,
|
|
true, /*allowEdits*/
|
|
)
|
|
sourcePosition = pos
|
|
}
|
|
}
|
|
|
|
decodedMappings = append(decodedMappings, &MappedPosition{
|
|
generatedPosition: generatedPosition,
|
|
sourceIndex: mapping.SourceIndex,
|
|
sourcePosition: sourcePosition,
|
|
nameIndex: mapping.NameIndex,
|
|
})
|
|
}
|
|
if decoder.Error() != nil {
|
|
decodedMappings = nil
|
|
}
|
|
|
|
// getSourceMappings()
|
|
for _, mapping := range decodedMappings {
|
|
if !mapping.isSourceMappedPosition() {
|
|
continue
|
|
}
|
|
sourceIndex := mapping.sourceIndex
|
|
list := sourceMappings[sourceIndex]
|
|
list = append(list, &SourceMappedPosition{
|
|
generatedPosition: mapping.generatedPosition,
|
|
sourceIndex: sourceIndex,
|
|
sourcePosition: mapping.sourcePosition,
|
|
nameIndex: mapping.nameIndex,
|
|
})
|
|
sourceMappings[sourceIndex] = list
|
|
}
|
|
for i, list := range sourceMappings {
|
|
slices.SortFunc(list, func(a, b *SourceMappedPosition) int {
|
|
debug.Assert(a.sourceIndex == b.sourceIndex, "All source mappings should have the same source index")
|
|
return a.sourcePosition - b.sourcePosition
|
|
})
|
|
sourceMappings[i] = core.DeduplicateSorted(list, func(a, b *SourceMappedPosition) bool {
|
|
return a.generatedPosition == b.generatedPosition &&
|
|
a.sourceIndex == b.sourceIndex &&
|
|
a.sourcePosition == b.sourcePosition
|
|
})
|
|
}
|
|
|
|
// getGeneratedMappings()
|
|
generatedMappings = decodedMappings
|
|
slices.SortFunc(generatedMappings, func(a, b *MappedPosition) int {
|
|
return a.generatedPosition - b.generatedPosition
|
|
})
|
|
generatedMappings = core.DeduplicateSorted(generatedMappings, func(a, b *MappedPosition) bool {
|
|
return a.generatedPosition == b.generatedPosition &&
|
|
a.sourceIndex == b.sourceIndex &&
|
|
a.sourcePosition == b.sourcePosition
|
|
})
|
|
|
|
return &DocumentPositionMapper{
|
|
useCaseSensitiveFileNames: useCaseSensitiveFileNames,
|
|
sourceFileAbsolutePaths: sourceFileAbsolutePaths,
|
|
sourceToSourceIndexMap: sourceToSourceIndexMap,
|
|
generatedAbsoluteFilePath: generatedAbsoluteFilePath,
|
|
generatedMappings: generatedMappings,
|
|
sourceMappings: sourceMappings,
|
|
}
|
|
}
|
|
|
|
type DocumentPosition struct {
|
|
FileName string
|
|
Pos int
|
|
}
|
|
|
|
func (d *DocumentPositionMapper) GetSourcePosition(loc *DocumentPosition) *DocumentPosition {
|
|
if d == nil {
|
|
return nil
|
|
}
|
|
if len(d.generatedMappings) == 0 {
|
|
return nil
|
|
}
|
|
|
|
targetIndex, _ := slices.BinarySearchFunc(d.generatedMappings, loc.Pos, func(m *MappedPosition, pos int) int {
|
|
return m.generatedPosition - pos
|
|
})
|
|
|
|
if targetIndex < 0 || targetIndex >= len(d.generatedMappings) {
|
|
return nil
|
|
}
|
|
|
|
mapping := d.generatedMappings[targetIndex]
|
|
if !mapping.isSourceMappedPosition() {
|
|
return nil
|
|
}
|
|
|
|
// Closest position
|
|
return &DocumentPosition{
|
|
FileName: d.sourceFileAbsolutePaths[mapping.sourceIndex],
|
|
Pos: mapping.sourcePosition,
|
|
}
|
|
}
|
|
|
|
func (d *DocumentPositionMapper) GetGeneratedPosition(loc *DocumentPosition) *DocumentPosition {
|
|
if d == nil {
|
|
return nil
|
|
}
|
|
sourceIndex, ok := d.sourceToSourceIndexMap[tspath.GetCanonicalFileName(loc.FileName, d.useCaseSensitiveFileNames)]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
if sourceIndex < 0 || int(sourceIndex) >= len(d.sourceMappings) {
|
|
return nil
|
|
}
|
|
sourceMappings := d.sourceMappings[sourceIndex]
|
|
targetIndex, _ := slices.BinarySearchFunc(sourceMappings, loc.Pos, func(m *SourceMappedPosition, pos int) int {
|
|
return m.sourcePosition - pos
|
|
})
|
|
|
|
if targetIndex < 0 || targetIndex >= len(sourceMappings) {
|
|
return nil
|
|
}
|
|
|
|
mapping := sourceMappings[targetIndex]
|
|
if mapping.sourceIndex != sourceIndex {
|
|
return nil
|
|
}
|
|
|
|
// Closest position
|
|
return &DocumentPosition{
|
|
FileName: d.generatedAbsoluteFilePath,
|
|
Pos: mapping.generatedPosition,
|
|
}
|
|
}
|
|
|
|
func GetDocumentPositionMapper(host Host, generatedFileName string) *DocumentPositionMapper {
|
|
mapFileName := tryGetSourceMappingURL(host, generatedFileName)
|
|
if mapFileName != "" {
|
|
if base64Object, matched := tryParseBase64Url(mapFileName); matched {
|
|
if base64Object != "" {
|
|
if decoded, err := base64.StdEncoding.DecodeString(base64Object); err == nil {
|
|
return convertDocumentToSourceMapper(host, string(decoded), generatedFileName)
|
|
}
|
|
}
|
|
// Not a data URL we can parse, skip it
|
|
mapFileName = ""
|
|
}
|
|
}
|
|
|
|
var possibleMapLocations []string
|
|
if mapFileName != "" {
|
|
possibleMapLocations = append(possibleMapLocations, mapFileName)
|
|
}
|
|
possibleMapLocations = append(possibleMapLocations, generatedFileName+".map")
|
|
for _, location := range possibleMapLocations {
|
|
mapFileName := tspath.GetNormalizedAbsolutePath(location, tspath.GetDirectoryPath(generatedFileName))
|
|
if mapFileContents, ok := host.ReadFile(mapFileName); ok {
|
|
return convertDocumentToSourceMapper(host, mapFileContents, mapFileName)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func convertDocumentToSourceMapper(host Host, contents string, mapFileName string) *DocumentPositionMapper {
|
|
sourceMap := tryParseRawSourceMap(contents)
|
|
if sourceMap == nil || len(sourceMap.Sources) == 0 || sourceMap.File == "" || sourceMap.Mappings == "" {
|
|
// invalid map
|
|
return nil
|
|
}
|
|
|
|
// Don't support source maps that contain inlined sources
|
|
if core.Some(sourceMap.SourcesContent, func(s *string) bool { return s != nil }) {
|
|
return nil
|
|
}
|
|
|
|
return createDocumentPositionMapper(host, sourceMap, mapFileName)
|
|
}
|
|
|
|
func tryParseRawSourceMap(contents string) *RawSourceMap {
|
|
sourceMap := &RawSourceMap{}
|
|
err := json.Unmarshal([]byte(contents), sourceMap)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if sourceMap.Version != 3 {
|
|
return nil
|
|
}
|
|
return sourceMap
|
|
}
|
|
|
|
func tryGetSourceMappingURL(host Host, fileName string) string {
|
|
lineInfo := host.GetECMALineInfo(fileName)
|
|
return TryGetSourceMappingURL(lineInfo)
|
|
}
|
|
|
|
// Equivalent to /^data:(?:application\/json;(?:charset=[uU][tT][fF]-8;)?base64,([A-Za-z0-9+/=]+)$)?/
|
|
func tryParseBase64Url(url string) (parseableUrl string, isBase64Url bool) {
|
|
var found bool
|
|
if url, found = strings.CutPrefix(url, `data:`); !found {
|
|
return "", false
|
|
}
|
|
if url, found = strings.CutPrefix(url, `application/json;`); !found {
|
|
return "", true
|
|
}
|
|
if url, found = strings.CutPrefix(url, `charset=`); found {
|
|
if !strings.EqualFold(url[:len(`utf-8;`)], `utf-8;`) {
|
|
return "", true
|
|
}
|
|
url = url[len(`utf-8;`):]
|
|
}
|
|
if url, found = strings.CutPrefix(url, `base64,`); !found {
|
|
return "", true
|
|
}
|
|
for _, r := range url {
|
|
if !(stringutil.IsASCIILetter(r) || stringutil.IsDigit(r) || r == '+' || r == '/' || r == '=') {
|
|
return "", true
|
|
}
|
|
}
|
|
return url, true
|
|
}
|