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