map-store.js 7.1 KB

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