123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- /*
- Copyright 2015, Yahoo Inc.
- Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
- */
- 'use strict';
-
- const path = require('path');
- const fs = require('fs');
- const debug = require('debug')('istanbuljs');
- const { SourceMapConsumer } = require('source-map');
- const pathutils = require('./pathutils');
- const { SourceMapTransformer } = require('./transformer');
-
- /**
- * Tracks source maps for registered files
- */
- class MapStore {
- /**
- * @param {Object} opts [opts=undefined] options.
- * @param {Boolean} opts.verbose [opts.verbose=false] verbose mode
- * @param {String} opts.baseDir [opts.baseDir=null] alternate base directory
- * to resolve sourcemap files
- * @param {Class} opts.SourceStore [opts.SourceStore=Map] class to use for
- * SourceStore. Must support `get`, `set` and `clear` methods.
- * @param {Array} opts.sourceStoreOpts [opts.sourceStoreOpts=[]] arguments
- * to use in the SourceStore constructor.
- * @constructor
- */
- constructor(opts) {
- opts = {
- baseDir: null,
- verbose: false,
- SourceStore: Map,
- sourceStoreOpts: [],
- ...opts
- };
- this.baseDir = opts.baseDir;
- this.verbose = opts.verbose;
- this.sourceStore = new opts.SourceStore(...opts.sourceStoreOpts);
- this.data = Object.create(null);
- this.sourceFinder = this.sourceFinder.bind(this);
- }
-
- /**
- * Registers a source map URL with this store. It makes some input sanity checks
- * and silently fails on malformed input.
- * @param transformedFilePath - the file path for which the source map is valid.
- * This must *exactly* match the path stashed for the coverage object to be
- * useful.
- * @param sourceMapUrl - the source map URL, **not** a comment
- */
- registerURL(transformedFilePath, sourceMapUrl) {
- const d = 'data:';
-
- if (
- sourceMapUrl.length > d.length &&
- sourceMapUrl.substring(0, d.length) === d
- ) {
- const b64 = 'base64,';
- const pos = sourceMapUrl.indexOf(b64);
- if (pos > 0) {
- this.data[transformedFilePath] = {
- type: 'encoded',
- data: sourceMapUrl.substring(pos + b64.length)
- };
- } else {
- debug(`Unable to interpret source map URL: ${sourceMapUrl}`);
- }
-
- return;
- }
-
- const dir = path.dirname(path.resolve(transformedFilePath));
- const file = path.resolve(dir, sourceMapUrl);
- this.data[transformedFilePath] = { type: 'file', data: file };
- }
-
- /**
- * Registers a source map object with this store. Makes some basic sanity checks
- * and silently fails on malformed input.
- * @param transformedFilePath - the file path for which the source map is valid
- * @param sourceMap - the source map object
- */
- registerMap(transformedFilePath, sourceMap) {
- if (sourceMap && sourceMap.version) {
- this.data[transformedFilePath] = {
- type: 'object',
- data: sourceMap
- };
- } else {
- debug(
- 'Invalid source map object: ' +
- JSON.stringify(sourceMap, null, 2)
- );
- }
- }
-
- /**
- * Retrieve a source map object from this store.
- * @param filePath - the file path for which the source map is valid
- * @returns {Object} a parsed source map object
- */
- getSourceMapSync(filePath) {
- try {
- if (!this.data[filePath]) {
- return;
- }
-
- const d = this.data[filePath];
- if (d.type === 'file') {
- return JSON.parse(fs.readFileSync(d.data, 'utf8'));
- }
-
- if (d.type === 'encoded') {
- return JSON.parse(Buffer.from(d.data, 'base64').toString());
- }
-
- /* The caller might delete properties */
- return {
- ...d.data
- };
- } catch (error) {
- debug('Error returning source map for ' + filePath);
- debug(error.stack);
-
- return;
- }
- }
-
- /**
- * Add inputSourceMap property to coverage data
- * @param coverageData - the __coverage__ object
- * @returns {Object} a parsed source map object
- */
- addInputSourceMapsSync(coverageData) {
- Object.entries(coverageData).forEach(([filePath, data]) => {
- if (data.inputSourceMap) {
- return;
- }
-
- const sourceMap = this.getSourceMapSync(filePath);
- if (sourceMap) {
- data.inputSourceMap = sourceMap;
- /* This huge property is not needed. */
- delete data.inputSourceMap.sourcesContent;
- }
- });
- }
-
- sourceFinder(filePath) {
- const content = this.sourceStore.get(filePath);
- if (content !== undefined) {
- return content;
- }
-
- if (path.isAbsolute(filePath)) {
- return fs.readFileSync(filePath, 'utf8');
- }
-
- return fs.readFileSync(
- pathutils.asAbsolute(filePath, this.baseDir),
- 'utf8'
- );
- }
-
- /**
- * Transforms the coverage map provided into one that refers to original
- * sources when valid mappings have been registered with this store.
- * @param {CoverageMap} coverageMap - the coverage map to transform
- * @returns {Promise<CoverageMap>} the transformed coverage map
- */
- async transformCoverage(coverageMap) {
- const hasInputSourceMaps = coverageMap
- .files()
- .some(
- file => coverageMap.fileCoverageFor(file).data.inputSourceMap
- );
-
- if (!hasInputSourceMaps && Object.keys(this.data).length === 0) {
- return coverageMap;
- }
-
- const transformer = new SourceMapTransformer(
- async (filePath, coverage) => {
- try {
- const obj =
- coverage.data.inputSourceMap ||
- this.getSourceMapSync(filePath);
- if (!obj) {
- return null;
- }
-
- const smc = new SourceMapConsumer(obj);
- smc.sources.forEach(s => {
- const content = smc.sourceContentFor(s);
- if (content) {
- const sourceFilePath = pathutils.relativeTo(
- s,
- filePath
- );
- this.sourceStore.set(sourceFilePath, content);
- }
- });
-
- return smc;
- } catch (error) {
- debug('Error returning source map for ' + filePath);
- debug(error.stack);
-
- return null;
- }
- }
- );
-
- return await transformer.transform(coverageMap);
- }
-
- /**
- * Disposes temporary resources allocated by this map store
- */
- dispose() {
- this.sourceStore.clear();
- }
- }
-
- module.exports = { MapStore };
|