|
|
/* 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 };
|