UNPKG

7.32 kBJavaScriptView Raw
1/*
2 Copyright 2015, Yahoo Inc.
3 Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
4 */
5'use strict';
6
7const path = require('path');
8const fs = require('fs');
9const debug = require('debug')('istanbuljs');
10const { SourceMapConsumer } = require('source-map');
11const pathutils = require('./pathutils');
12const { SourceMapTransformer } = require('./transformer');
13
14/**
15 * Tracks source maps for registered files
16 */
17class MapStore {
18 /**
19 * @param {Object} opts [opts=undefined] options.
20 * @param {Boolean} opts.verbose [opts.verbose=false] verbose mode
21 * @param {String} opts.baseDir [opts.baseDir=null] alternate base directory
22 * to resolve sourcemap files
23 * @param {Class} opts.SourceStore [opts.SourceStore=Map] class to use for
24 * SourceStore. Must support `get`, `set` and `clear` methods.
25 * @param {Array} opts.sourceStoreOpts [opts.sourceStoreOpts=[]] arguments
26 * to use in the SourceStore constructor.
27 * @constructor
28 */
29 constructor(opts) {
30 opts = {
31 baseDir: null,
32 verbose: false,
33 SourceStore: Map,
34 sourceStoreOpts: [],
35 ...opts
36 };
37 this.baseDir = opts.baseDir;
38 this.verbose = opts.verbose;
39 this.sourceStore = new opts.SourceStore(...opts.sourceStoreOpts);
40 this.data = Object.create(null);
41 this.sourceFinder = this.sourceFinder.bind(this);
42 }
43
44 /**
45 * Registers a source map URL with this store. It makes some input sanity checks
46 * and silently fails on malformed input.
47 * @param transformedFilePath - the file path for which the source map is valid.
48 * This must *exactly* match the path stashed for the coverage object to be
49 * useful.
50 * @param sourceMapUrl - the source map URL, **not** a comment
51 */
52 registerURL(transformedFilePath, sourceMapUrl) {
53 const d = 'data:';
54
55 if (
56 sourceMapUrl.length > d.length &&
57 sourceMapUrl.substring(0, d.length) === d
58 ) {
59 const b64 = 'base64,';
60 const pos = sourceMapUrl.indexOf(b64);
61 if (pos > 0) {
62 this.data[transformedFilePath] = {
63 type: 'encoded',
64 data: sourceMapUrl.substring(pos + b64.length)
65 };
66 } else {
67 debug(`Unable to interpret source map URL: ${sourceMapUrl}`);
68 }
69
70 return;
71 }
72
73 const dir = path.dirname(path.resolve(transformedFilePath));
74 const file = path.resolve(dir, sourceMapUrl);
75 this.data[transformedFilePath] = { type: 'file', data: file };
76 }
77
78 /**
79 * Registers a source map object with this store. Makes some basic sanity checks
80 * and silently fails on malformed input.
81 * @param transformedFilePath - the file path for which the source map is valid
82 * @param sourceMap - the source map object
83 */
84 registerMap(transformedFilePath, sourceMap) {
85 if (sourceMap && sourceMap.version) {
86 this.data[transformedFilePath] = {
87 type: 'object',
88 data: sourceMap
89 };
90 } else {
91 debug(
92 'Invalid source map object: ' +
93 JSON.stringify(sourceMap, null, 2)
94 );
95 }
96 }
97
98 /**
99 * Retrieve a source map object from this store.
100 * @param filePath - the file path for which the source map is valid
101 * @returns {Object} a parsed source map object
102 */
103 getSourceMapSync(filePath) {
104 try {
105 if (!this.data[filePath]) {
106 return;
107 }
108
109 const d = this.data[filePath];
110 if (d.type === 'file') {
111 return JSON.parse(fs.readFileSync(d.data, 'utf8'));
112 }
113
114 if (d.type === 'encoded') {
115 return JSON.parse(Buffer.from(d.data, 'base64').toString());
116 }
117
118 /* The caller might delete properties */
119 return {
120 ...d.data
121 };
122 } catch (error) {
123 debug('Error returning source map for ' + filePath);
124 debug(error.stack);
125
126 return;
127 }
128 }
129
130 /**
131 * Add inputSourceMap property to coverage data
132 * @param coverageData - the __coverage__ object
133 * @returns {Object} a parsed source map object
134 */
135 addInputSourceMapsSync(coverageData) {
136 Object.entries(coverageData).forEach(([filePath, data]) => {
137 if (data.inputSourceMap) {
138 return;
139 }
140
141 const sourceMap = this.getSourceMapSync(filePath);
142 if (sourceMap) {
143 data.inputSourceMap = sourceMap;
144 /* This huge property is not needed. */
145 delete data.inputSourceMap.sourcesContent;
146 }
147 });
148 }
149
150 sourceFinder(filePath) {
151 const content = this.sourceStore.get(filePath);
152 if (content !== undefined) {
153 return content;
154 }
155
156 if (path.isAbsolute(filePath)) {
157 return fs.readFileSync(filePath, 'utf8');
158 }
159
160 return fs.readFileSync(
161 pathutils.asAbsolute(filePath, this.baseDir),
162 'utf8'
163 );
164 }
165
166 /**
167 * Transforms the coverage map provided into one that refers to original
168 * sources when valid mappings have been registered with this store.
169 * @param {CoverageMap} coverageMap - the coverage map to transform
170 * @returns {Promise<CoverageMap>} the transformed coverage map
171 */
172 async transformCoverage(coverageMap) {
173 const hasInputSourceMaps = coverageMap
174 .files()
175 .some(
176 file => coverageMap.fileCoverageFor(file).data.inputSourceMap
177 );
178
179 if (!hasInputSourceMaps && Object.keys(this.data).length === 0) {
180 return coverageMap;
181 }
182
183 const transformer = new SourceMapTransformer(
184 async (filePath, coverage) => {
185 try {
186 const obj =
187 coverage.data.inputSourceMap ||
188 this.getSourceMapSync(filePath);
189 if (!obj) {
190 return null;
191 }
192
193 const smc = new SourceMapConsumer(obj);
194 smc.sources.forEach(s => {
195 const content = smc.sourceContentFor(s);
196 if (content) {
197 const sourceFilePath = pathutils.relativeTo(
198 s,
199 filePath
200 );
201 this.sourceStore.set(sourceFilePath, content);
202 }
203 });
204
205 return smc;
206 } catch (error) {
207 debug('Error returning source map for ' + filePath);
208 debug(error.stack);
209
210 return null;
211 }
212 }
213 );
214
215 return await transformer.transform(coverageMap);
216 }
217
218 /**
219 * Disposes temporary resources allocated by this map store
220 */
221 dispose() {
222 this.sourceStore.clear();
223 }
224}
225
226module.exports = { MapStore };