UNPKG

remap-istanbul

Version:

A tool for remapping Istanbul coverage via Source Maps

274 lines (230 loc) 8.32 kB
const { Collector } = require('istanbul'); const path = require('path'); const fs = require('fs'); const { SourceMapConsumer } = require('source-map'); const SparseCoverageCollector = require('./SparseCoverageCollector'); const getMapping = require('./getMapping'); const remapFunction = require('./remapFunction'); const remapBranch = require('./remapBranch'); const sourceMapRegEx = /(?:\/{2}[#@]{1,2}|\/\*)\s+sourceMappingURL\s*=\s*(data:(?:[^;]+;)*(base64)?,)?(\S+)(?:\n\s*)?/; class CoverageTransformer { constructor(options) { this.basePath = options.basePath; this.warn = options.warn || console.warn; this.warnMissingSourceMaps = (typeof options.warnMissingSourceMaps !== 'undefined') ? options.warnMissingSourceMaps : true; this.exclude = () => false; if (options.exclude) { if (typeof options.exclude === 'function') { this.exclude = options.exclude; } else if (typeof options.exclude === 'string') { this.exclude = (fileName) => fileName.indexOf(options.exclude) > -1; } else { this.exclude = (fileName) => fileName.match(options.exclude); } } this.mapFileName = options.mapFileName || ((fileName) => fileName); this.useAbsolutePaths = !!options.useAbsolutePaths; this.readJSON = options.readJSON || function readJSON(filePath) { if (!fs.existsSync(filePath)) { this.warn(Error(`Could not find file: "${filePath}"`)); return null; } return JSON.parse(fs.readFileSync(filePath)); }; this.readFile = options.readFile || function readFile(filePath) { if (!fs.existsSync(filePath)) { this.warn(new Error(`Could not find file: "${filePath}"`)); return ''; } return fs.readFileSync(filePath); }; this.sourceStore = options.sources; this.sparseCoverageCollector = new SparseCoverageCollector(); } addFileCoverage(filePath, fileCoverage) { if (this.exclude(filePath)) { this.warn(`Excluding: "${filePath}"`); return; } let rawSourceMap; let sourceMapDir = path.dirname(filePath); let codeIsArray = true; if (fileCoverage.inputSourceMap) { rawSourceMap = fileCoverage.inputSourceMap; } else { /* coverage.json can sometimes include the code inline */ let codeFromFile = false; let jsText = fileCoverage.code; if (!jsText) { jsText = this.readFile(filePath); codeFromFile = true; } if (Array.isArray(jsText)) { /* sometimes the source is an array */ jsText = jsText.join('\n'); } else { codeIsArray = false; } let match = sourceMapRegEx.exec(jsText); if (!match && !codeFromFile) { codeIsArray = false; jsText = this.readFile(filePath); match = sourceMapRegEx.exec(jsText); } if (match) { if (match[1]) { if (match[2]) { rawSourceMap = JSON.parse((Buffer.from(match[3], 'base64').toString('utf8'))); } else { rawSourceMap = JSON.parse(decodeURIComponent(match[3])); } } else { const sourceMapPath = path.join(sourceMapDir, match[3]); rawSourceMap = this.readJSON(sourceMapPath); sourceMapDir = path.dirname(sourceMapPath); } } } if (!rawSourceMap) { /* We couldn't find a source map, so will copy coverage after warning. */ if (this.warnMissingSourceMaps) { this.warn(new Error(`Could not find source map for: "${filePath}"`)); } try { fileCoverage.code = String(fs.readFileSync(filePath)).split('\n'); } catch (error) { this.warn(new Error(`Could not find source for : "${filePath}"`)); } this.sparseCoverageCollector.setCoverage(filePath, fileCoverage); return; } sourceMapDir = this.basePath || sourceMapDir; // Clean up source map paths: // * prepend sourceRoot if it is set // * replace relative paths in source maps with absolute rawSourceMap.sources = rawSourceMap.sources.map((srcPath) => { let tempVal = srcPath; if (rawSourceMap.sourceRoot) { tempVal = /\/$/g.test(rawSourceMap.sourceRoot) ? rawSourceMap.sourceRoot + srcPath : srcPath; } return tempVal.substr(0, 1) === '.' ? path.resolve(sourceMapDir, tempVal) : tempVal; }); let sourceMap = new SourceMapConsumer(rawSourceMap); /* if there are inline sources and a store to put them into, we will populate it */ const inlineSourceMap = {}; let origSourceFilename; let origFileName; let fileName; if (sourceMap.sourcesContent) { origSourceFilename = rawSourceMap.sources[0]; if (origSourceFilename && path.extname(origSourceFilename) !== '' && rawSourceMap.sources.length === 1) { origFileName = rawSourceMap.file || rawSourceMap.sources[0]; fileName = filePath.replace(new RegExp(path.extname(origFileName) + '$'), path.extname(origSourceFilename)); rawSourceMap.file = fileName; rawSourceMap.sources = [fileName]; rawSourceMap.sourceRoot = ''; sourceMap = new SourceMapConsumer(rawSourceMap); } sourceMap.sourcesContent.forEach((source, idx) => { inlineSourceMap[sourceMap.sources[idx]] = true; this.sparseCoverageCollector.setSourceCode( sourceMap.sources[idx], codeIsArray ? source.split('\n') : source ); if (this.sourceStore) { this.sourceStore.set(sourceMap.sources[idx], source); } }); } const resolvePath = (source) => { let resolvedSource = source in inlineSourceMap ? source : path.resolve(sourceMapDir, source); if (!this.useAbsolutePaths && !(source in inlineSourceMap)) { resolvedSource = path.relative(process.cwd(), resolvedSource); } return resolvedSource; }; const getMappingResolved = (location) => { const mapping = getMapping(sourceMap, location); if (!mapping) return null; return Object.assign(mapping, { source: resolvePath(mapping.source) }); }; Object.keys(fileCoverage.branchMap).forEach((index) => { const genItem = fileCoverage.branchMap[index]; const hits = fileCoverage.b[index]; const info = remapBranch(genItem, getMappingResolved); if (info) { this.sparseCoverageCollector.updateBranch(info.source, info.srcItem, hits); } }); Object.keys(fileCoverage.fnMap).forEach((index) => { const genItem = fileCoverage.fnMap[index]; const hits = fileCoverage.f[index]; const info = remapFunction(genItem, getMappingResolved); if (info) { this.sparseCoverageCollector.updateFunction(info.source, info.srcItem, hits); } }); Object.keys(fileCoverage.statementMap).forEach((index) => { const genItem = fileCoverage.statementMap[index]; const hits = fileCoverage.s[index]; const mapping = getMappingResolved(genItem); if (mapping) { this.sparseCoverageCollector.updateStatement(mapping.source, mapping.loc, hits); } }); // todo: refactor exposing implementation details const srcCoverage = this.sparseCoverageCollector.getFinalCoverage(); if (sourceMap.sourcesContent && this.basePath && origFileName) { // Convert path to use base path option const getPath = filePath => { const absolutePath = path.resolve(this.basePath, filePath); if (!this.useAbsolutePaths) { return path.relative(process.cwd(), absolutePath); } return absolutePath; }; const fullSourceMapPath = getPath( origFileName.replace(path.extname(origFileName), path.extname(origSourceFilename)) ); srcCoverage[fullSourceMapPath] = srcCoverage[fileName]; srcCoverage[fullSourceMapPath].path = fullSourceMapPath; delete srcCoverage[fileName]; } } addCoverage(item) { Object.keys(item) .forEach((filePath) => { const fileCoverage = item[filePath]; this.addFileCoverage(filePath, fileCoverage); }); } getFinalCoverage() { const collector = new Collector(); const srcCoverage = this.sparseCoverageCollector.getFinalCoverage(); Object.keys(srcCoverage) .filter((filePath) => !this.exclude(filePath)) .forEach((filename) => { const coverage = Object.assign({}, srcCoverage[filename]); coverage.path = this.mapFileName(filename); if (this.sourceStore && coverage.path !== filename) { const source = this.sourceStore.get(filename); this.sourceStore.set(coverage.path, source); } collector.add({ [coverage.path]: coverage, }); }); /* refreshes the line counts for reports */ collector.getFinalCoverage(); return collector; } } module.exports = CoverageTransformer;