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