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