wct-istanbul
Version:
Istanbul coverage reporting for projects being tested by web-component-tester
235 lines (211 loc) • 7.43 kB
JavaScript
var _ = require('lodash');
var minimatch = require('minimatch');
var fs = require('fs');
var path = require('path');
var babylon = require('babylon');
var babylonPlugins = [
'objectRestSpread',
'asyncGenerators',
'dynamicImport',
'importMeta',
];
var babel = require('@babel/core');
var babelPlugins = [
require('@babel/plugin-syntax-object-rest-spread'),
require('@babel/plugin-syntax-async-generators'),
require('@babel/plugin-syntax-dynamic-import'),
require('@babel/plugin-syntax-import-meta'),
require('babel-plugin-istanbul'),
];
var parseurl = require('parseurl');
var scriptHook = require('html-script-hook');
var contentType = require('content-type');
var transformResponse = require('./transform-middleware.js').transformResponse;
// helpers
var cache = {};
function patchCoverageVariable(code) {
return code.replace(/coverage ?= ?global\[gcv\] ?\|\| ?\(global\[gcv\] ?= ?{}\)/g, 'coverage = WCT.share[gcv] = global[gcv] || (global[gcv] = {})');
}
// babel instrumentation
function instrumentBabel(code, filename) {
let ast;
try {
ast = babylon.parse(code, {
sourceType: 'script',
plugins: babylonPlugins,
});
}
catch (e) {
if (e.message.match(/import.*export.*module/)) {
ast = babylon.parse(code, {
sourceType: 'module',
plugins: babylonPlugins,
});
}
else {
throw e;
}
}
let { code: instrumented } = babel.transformFromAst(ast, code, { filename: filename, presets: [], plugins: babelPlugins });
if (typeof instrumented === 'undefined') {
return code;
}
return patchCoverageVariable(instrumented);
}
/**
* @license
* Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
// From https://github.com/Polymer/tools/blob/master/packages/polyserve/src/make_app.ts
function arrayStartsWith(array, prefix) {
for (let i = 0; i < prefix.length; i++) {
if (i >= array.length || array[i] !== prefix[i]) {
return false;
}
}
return true;
}
function getFilePath(root, req, pluginOptions, emitter, options) {
if (options) {
// For web-component-tester@^6.6.0
// From https://github.com/Polymer/tools/blob/master/packages/polyserve/src/make_app.ts
const rootPath = path.resolve(options.root);
const baseComponentDir = options.componentDir;
const componentDir = path.resolve(root, baseComponentDir);
const packageName = options.packageName;
const url = parseurl(req);
let splitPath = url.pathname.split('/').slice(1);
const splitPackagePath = packageName.split('/');
if (arrayStartsWith(splitPath, splitPackagePath)) {
if (rootPath) {
splitPath = [rootPath].concat(splitPath.slice(splitPackagePath.length));
}
else {
splitPath = splitPath.slice(splitPackagePath.length);
}
}
else {
splitPath = [componentDir].concat(splitPath);
}
return splitPath.join('/');
}
else {
// For @t2ym/web-component-tester@6.0.2
let pathname = parseurl(req).pathname;
let rootDirName = root.split(/[\/\\]/).pop();
let componentPathName = pathname.split(/\//)[1];
return req._filePath || (rootDirName === componentPathName ? path.join(root, '..', pathname) : path.join(root, pathname));
}
}
function instrumentHtml(html, req, root, pluginOptions, emitter, options){
var asset = req.url;
var htmlFilePath = getFilePath(root, req, pluginOptions, emitter, options);
if ( !cache[asset] ){
cache[asset] = scriptHook (html, {scriptCallback: gotScript});
}
function gotScript(code, loc) {
return instrumentBabel(code, htmlFilePath);
}
return cache[asset];
}
function instrumentAsset(code, req, root, pluginOptions, emitter, options){
var asset = req.url;
var assetPath = getFilePath(root, req, pluginOptions, emitter, options);
if ( !cache[asset] ){
cache[asset] = instrumentBabel(code, assetPath);
}
return cache[asset];
}
// from polyserve
const javaScriptMimeTypes = [
'application/javascript',
'application/ecmascript',
'text/javascript',
'text/ecmascript'
];
const htmlMimeType = 'text/html';
const compileMimeTypes = [
htmlMimeType
].concat(javaScriptMimeTypes);
function getContentType(response) {
const contentTypeHeader = response.getHeader('Content-Type');
return contentTypeHeader && contentType.parse(contentTypeHeader).type;
}
/**
* Middleware that serves an instrumented asset based on user
* configuration of coverage
*/
function coverageMiddleware(root, pluginOptions, emitter, options, chain = false, last = false) {
return transformResponse({
chain: chain,
last: last,
shouldTransform(_request, response) {
return response.statusCode >= 200 && response.statusCode < 300 &&
compileMimeTypes.indexOf(getContentType(response)) >= 0;
},
transform(request, response, body) {
const relativePath = parseurl(request).pathname;
const contentType = getContentType(response);
const source = body;
// always ignore platform files in addition to user's blacklist
var blacklist = ['/web-component-tester/*'].concat(pluginOptions.exclude);
var whitelist = pluginOptions.include;
// check asset against rules
var process = match(relativePath, whitelist) && !match(relativePath, blacklist);
if (process) {
if (contentType === htmlMimeType) {
body = instrumentHtml(source, request, root, pluginOptions, emitter, options);
}
if (javaScriptMimeTypes.indexOf(contentType) !== -1) {
body = instrumentAsset(source, request, root, pluginOptions, emitter, options);
}
emitter.emit('log:debug', 'coverage', 'instrument', relativePath);
}
else {
// no transformation
emitter.emit('log:debug', 'coverage', 'skip ', relativePath);
}
return body;
}
});
}
/**
* Returns true if the supplied string mini-matches any of the supplied patterns
*/
function match(str, rules) {
return _.some(rules, minimatch.bind(null, str));
}
function _getFilePathFromWaterfall(waterfall, request) {
var requestPath = parseurl(request).pathname;
var pathLookup = _.find(waterfall, function(pathLookup) {
return requestPath.indexOf(pathLookup.prefix) === 0;
});
return requestPath.replace(pathLookup.prefix, pathLookup.target);
}
// Lifted from https://github.com/PolymerLabs/serve-waterfall
/**
* @param {Mappings} mappings The mappings to serve.
* @param {string} root The root directory paths are relative to.
* @return {Array<{prefix: string, target: string}>}
*/
function _buildWaterfall(pathLookups, root) {
var basename = path.basename(root);
var waterfall = _.map(pathLookups, function(pathLookup) {
var prefix = Object.keys(pathLookup)[0];
return {
prefix: prefix.replace('<basename>', basename),
target: path.resolve(root, pathLookup[prefix]),
};
});
return waterfall;
}
module.exports = coverageMiddleware;