Something went wrong. Try again.
A native-speed rewrite of the roblox-ts compiler in golang
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314package sourcemap
import ( "encoding/base64" "slices" "strings"
"rotor/tsgo/core" "rotor/tsgo/debug" "rotor/tsgo/json" "rotor/tsgo/scanner" "rotor/tsgo/stringutil" "rotor/tsgo/tspath")
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.ComputePositionOfLineAndUTF16Character( 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.ComputePositionOfLineAndUTF16Character( 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}