• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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