UNPKG

monocart-coverage-reports

Version:

A code coverage tool to generate native V8 reports or Istanbul reports.

277 lines (220 loc) 7.01 kB
const fs = require('fs'); const path = require('path'); const EC = require('eight-colors'); const { fileURLToPath, pathToFileURL } = require('url'); const Concurrency = require('../platform/concurrency.js'); const { convertSourceMap } = require('../packages/monocart-coverage-vendor.js'); const { flattenSourceMaps } = require('./flatten-source-maps.js'); const Util = require('../utils/util.js'); const loadSourceMap = async (url = '') => { if (url.startsWith('file:')) { const p = fileURLToPath(url); const content = Util.readFileSync(p); if (!content) { Util.logDebug(EC.red(`failed to load sourcemap ${p}`)); return; } return JSON.parse(content); } const [err, res] = await Util.request(url); if (err) { Util.logDebug(EC.red(`${err.message} ${url}`)); return; } const content = res.data; // could be string not json, if Content-Type is application/octet-stream if (typeof content === 'string') { return JSON.parse(content); } return content; }; const getSourceMapUrl = (content, url) => { const m = content.match(convertSourceMap.mapFileCommentRegex); if (!m) { return; } const comment = m.pop(); const r = convertSourceMap.mapFileCommentRegex.exec(comment); // for some odd reason //# .. captures in 1 and /* .. */ in 2 const filename = r[1] || r[2]; const urlObj = Util.resolveUrl(filename, url); if (urlObj) { return urlObj.toString(); } const mapUrl = Util.resolveUrl(filename, pathToFileURL(url).toString()); if (mapUrl) { return mapUrl.toString(); } }; const resolveSourcesContent = (data, url) => { const { sources, sourcesContent } = data; // sources [1,2,3] // sourcesContent could be [null, null, "content"] // some of contents could be missed let hasSourceContent = false; sources.forEach((file, i) => { if (typeof sourcesContent[i] === 'string') { hasSourceContent = true; return; } const sourceUrl = Util.resolveUrl(file, url); if (sourceUrl) { let sourcePath = sourceUrl.toString(); // could be no `file:` if (sourcePath.startsWith('file:')) { sourcePath = fileURLToPath(sourcePath); } const content = Util.readFileSync(path.resolve(sourcePath)); if (typeof content === 'string') { sourcesContent[i] = content; hasSourceContent = true; return; } } sourcesContent[i] = ''; Util.logDebug(EC.red(`failed to load source content: ${file}`)); }); if (hasSourceContent) { return data; } }; const checkSourcesContent = (data) => { const { sourcesContent, sources } = data; if (!sourcesContent) { data.sourcesContent = []; return false; } // all should be string, could be [null] const contents = sourcesContent.filter((content) => typeof content === 'string'); if (contents.length === sources.length) { return true; } return false; }; const resolveSectionedSourceMap = (data, url, sections) => { let hasSourceContent = false; sections.forEach((item) => { // offset: { line: 1, column: 0 }, // map: { sources, sourcesContent } const map = item.map; if (checkSourcesContent(map)) { hasSourceContent = true; return; } const done = resolveSourcesContent(map, url); if (done) { hasSourceContent = true; } }); if (hasSourceContent) { return flattenSourceMaps(data); } }; const resolveSourceMap = (data, url) => { if (!data) { return; } const { sections, sources, mappings } = data; if (sections) { return resolveSectionedSourceMap(data, url, sections); } if (!sources || !mappings) { return; } // check sources content if (checkSourcesContent(data)) { return data; } // load sources content by sources return resolveSourcesContent(data, url); }; const getInlineSourceMap = (content) => { let smc; try { smc = convertSourceMap.fromSource(content); } catch (e) { // ignore "//# sourceMappingURL=" in a string // console.log(e); } return smc; }; const collectSourceMaps = async (v8list, options) => { const sourceList = []; const sourcemapList = []; const concurrency = new Concurrency(); for (const item of v8list) { const { type, url, id, source, sourceMap } = item; // source and sourceMap will be saved as separated file (could be cached) // just keep functions coverage ( could be multiple times, will be merged ) // so remove source and sourceMap delete item.source; delete item.sourceMap; // source and sourceMap already saved const { cachePath } = Util.getCacheFileInfo('source', id, options.cacheDir); if (fs.existsSync(cachePath)) { continue; } // save source and sourceMap to separated json file const sourceData = { id, url, source, sourceMap }; // remove comments if not debug if (!Util.isDebug()) { sourceData.source = convertSourceMap.removeComments(source); } // check sourceMap only for js if (type === 'js' && !sourceData.sourceMap) { // from inline sync const smc = getInlineSourceMap(source); if (smc) { sourceData.sourceMap = resolveSourceMap(smc.sourcemap, url); sourcemapList.push({ url, inline: true }); sourceList.push(sourceData); continue; } // from url async const sourceMapUrl = getSourceMapUrl(source, item.url); if (sourceMapUrl) { concurrency.addItem({ sourceMapUrl, sourceData }); continue; } } // no need check sourceMap sourceList.push(sourceData); } // from url concurrency await concurrency.start(async (item) => { const { sourceMapUrl, sourceData } = item; const { url } = sourceData; const data = await loadSourceMap(sourceMapUrl); if (data) { sourceData.sourceMap = resolveSourceMap(data, url); sourcemapList.push({ url, sourceMapUrl }); } sourceList.push(sourceData); }); return { sourceList, sourcemapList }; }; module.exports = { collectSourceMaps, resolveSourceMap };