1import * as ts from "./_namespaces/ts"; 2import { 3 base64decode, computeLineAndCharacterOfPosition, createDocumentPositionMapper, createGetCanonicalFileName, 4 DocumentPosition, DocumentPositionMapper, DocumentPositionMapperHost, Extension, 5 getDeclarationEmitOutputFilePathWorker, getDirectoryPath, getLineInfo, getLineStarts, getNormalizedAbsolutePath, 6 identitySourceMapConsumer, isDeclarationFileName, isString, LineAndCharacter, LineInfo, Map, outFile, Program, 7 removeFileExtension, SourceFileLike, sys, tryGetSourceMappingURL, tryParseRawSourceMap, 8} from "./_namespaces/ts"; 9 10const base64UrlRegExp = /^data:(?:application\/json(?:;charset=[uU][tT][fF]-8);base64,([A-Za-z0-9+\/=]+)$)?/; 11 12/** @internal */ 13export interface SourceMapper { 14 toLineColumnOffset(fileName: string, position: number): LineAndCharacter; 15 tryGetSourcePosition(info: DocumentPosition): DocumentPosition | undefined; 16 tryGetGeneratedPosition(info: DocumentPosition): DocumentPosition | undefined; 17 clearCache(): void; 18} 19 20/** @internal */ 21export interface SourceMapperHost { 22 useCaseSensitiveFileNames(): boolean; 23 getCurrentDirectory(): string; 24 getProgram(): Program | undefined; 25 fileExists?(path: string): boolean; 26 readFile?(path: string, encoding?: string): string | undefined; 27 getSourceFileLike?(fileName: string): SourceFileLike | undefined; 28 getDocumentPositionMapper?(generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined; 29 log(s: string): void; 30} 31 32/** @internal */ 33export function getSourceMapper(host: SourceMapperHost): SourceMapper { 34 const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames()); 35 const currentDirectory = host.getCurrentDirectory(); 36 const sourceFileLike = new Map<string, SourceFileLike | false>(); 37 const documentPositionMappers = new Map<string, DocumentPositionMapper>(); 38 return { tryGetSourcePosition, tryGetGeneratedPosition, toLineColumnOffset, clearCache }; 39 40 function toPath(fileName: string) { 41 return ts.toPath(fileName, currentDirectory, getCanonicalFileName); 42 } 43 44 function getDocumentPositionMapper(generatedFileName: string, sourceFileName?: string) { 45 const path = toPath(generatedFileName); 46 const value = documentPositionMappers.get(path); 47 if (value) return value; 48 49 let mapper: DocumentPositionMapper | undefined; 50 if (host.getDocumentPositionMapper) { 51 mapper = host.getDocumentPositionMapper(generatedFileName, sourceFileName); 52 } 53 else if (host.readFile) { 54 const file = getSourceFileLike(generatedFileName); 55 mapper = file && ts.getDocumentPositionMapper( 56 { getSourceFileLike, getCanonicalFileName, log: s => host.log(s) }, 57 generatedFileName, 58 getLineInfo(file.text, getLineStarts(file)), 59 f => !host.fileExists || host.fileExists(f) ? host.readFile!(f) : undefined 60 ); 61 } 62 documentPositionMappers.set(path, mapper || identitySourceMapConsumer); 63 return mapper || identitySourceMapConsumer; 64 } 65 66 function tryGetSourcePosition(info: DocumentPosition): DocumentPosition | undefined { 67 if (!isDeclarationFileName(info.fileName)) return undefined; 68 69 const file = getSourceFile(info.fileName); 70 if (!file) return undefined; 71 72 const newLoc = getDocumentPositionMapper(info.fileName).getSourcePosition(info); 73 return !newLoc || newLoc === info ? undefined : tryGetSourcePosition(newLoc) || newLoc; 74 } 75 76 function tryGetGeneratedPosition(info: DocumentPosition): DocumentPosition | undefined { 77 if (isDeclarationFileName(info.fileName)) return undefined; 78 79 const sourceFile = getSourceFile(info.fileName); 80 if (!sourceFile) return undefined; 81 82 const program = host.getProgram()!; 83 // If this is source file of project reference source (instead of redirect) there is no generated position 84 if (program.isSourceOfProjectReferenceRedirect(sourceFile.fileName)) { 85 return undefined; 86 } 87 88 const options = program.getCompilerOptions(); 89 const outPath = outFile(options); 90 91 const declarationPath = outPath ? 92 removeFileExtension(outPath) + Extension.Dts : 93 getDeclarationEmitOutputFilePathWorker(info.fileName, program.getCompilerOptions(), currentDirectory, program.getCommonSourceDirectory(), getCanonicalFileName); 94 if (declarationPath === undefined) return undefined; 95 96 const newLoc = getDocumentPositionMapper(declarationPath, info.fileName).getGeneratedPosition(info); 97 return newLoc === info ? undefined : newLoc; 98 } 99 100 function getSourceFile(fileName: string) { 101 const program = host.getProgram(); 102 if (!program) return undefined; 103 104 const path = toPath(fileName); 105 // file returned here could be .d.ts when asked for .ts file if projectReferences and module resolution created this source file 106 const file = program.getSourceFileByPath(path); 107 return file && file.resolvedPath === path ? file : undefined; 108 } 109 110 function getOrCreateSourceFileLike(fileName: string): SourceFileLike | undefined { 111 const path = toPath(fileName); 112 const fileFromCache = sourceFileLike.get(path); 113 if (fileFromCache !== undefined) return fileFromCache ? fileFromCache : undefined; 114 115 if (!host.readFile || host.fileExists && !host.fileExists(path)) { 116 sourceFileLike.set(path, false); 117 return undefined; 118 } 119 120 // And failing that, check the disk 121 const text = host.readFile(path); 122 const file = text ? createSourceFileLike(text) : false; 123 sourceFileLike.set(path, file); 124 return file ? file : undefined; 125 } 126 127 // This can be called from source mapper in either source program or program that includes generated file 128 function getSourceFileLike(fileName: string) { 129 return !host.getSourceFileLike ? 130 getSourceFile(fileName) || getOrCreateSourceFileLike(fileName) : 131 host.getSourceFileLike(fileName); 132 } 133 134 function toLineColumnOffset(fileName: string, position: number): LineAndCharacter { 135 const file = getSourceFileLike(fileName)!; // TODO: GH#18217 136 return file.getLineAndCharacterOfPosition(position); 137 } 138 139 function clearCache(): void { 140 sourceFileLike.clear(); 141 documentPositionMappers.clear(); 142 } 143} 144 145/** 146 * string | undefined to contents of map file to create DocumentPositionMapper from it 147 * DocumentPositionMapper | false to give back cached DocumentPositionMapper 148 * 149 * @internal 150 */ 151export type ReadMapFile = (mapFileName: string, mapFileNameFromDts: string | undefined) => string | undefined | DocumentPositionMapper | false; 152 153/** @internal */ 154export function getDocumentPositionMapper( 155 host: DocumentPositionMapperHost, 156 generatedFileName: string, 157 generatedFileLineInfo: LineInfo, 158 readMapFile: ReadMapFile) { 159 let mapFileName = tryGetSourceMappingURL(generatedFileLineInfo); 160 if (mapFileName) { 161 const match = base64UrlRegExp.exec(mapFileName); 162 if (match) { 163 if (match[1]) { 164 const base64Object = match[1]; 165 return convertDocumentToSourceMapper(host, base64decode(sys, base64Object), generatedFileName); 166 } 167 // Not a data URL we can parse, skip it 168 mapFileName = undefined; 169 } 170 } 171 const possibleMapLocations: string[] = []; 172 if (mapFileName) { 173 possibleMapLocations.push(mapFileName); 174 } 175 possibleMapLocations.push(generatedFileName + ".map"); 176 const originalMapFileName = mapFileName && getNormalizedAbsolutePath(mapFileName, getDirectoryPath(generatedFileName)); 177 for (const location of possibleMapLocations) { 178 const mapFileName = getNormalizedAbsolutePath(location, getDirectoryPath(generatedFileName)); 179 const mapFileContents = readMapFile(mapFileName, originalMapFileName); 180 if (isString(mapFileContents)) { 181 return convertDocumentToSourceMapper(host, mapFileContents, mapFileName); 182 } 183 if (mapFileContents !== undefined) { 184 return mapFileContents || undefined; 185 } 186 } 187 return undefined; 188} 189 190function convertDocumentToSourceMapper(host: DocumentPositionMapperHost, contents: string, mapFileName: string) { 191 const map = tryParseRawSourceMap(contents); 192 if (!map || !map.sources || !map.file || !map.mappings) { 193 // obviously invalid map 194 return undefined; 195 } 196 197 // Dont support sourcemaps that contain inlined sources 198 if (map.sourcesContent && map.sourcesContent.some(isString)) return undefined; 199 200 return createDocumentPositionMapper(host, map, mapFileName); 201} 202 203function createSourceFileLike(text: string, lineMap?: SourceFileLike["lineMap"]): SourceFileLike { 204 return { 205 text, 206 lineMap, 207 getLineAndCharacterOfPosition(pos: number) { 208 return computeLineAndCharacterOfPosition(getLineStarts(this), pos); 209 } 210 }; 211} 212