diff --git a/index.js b/index.js index cdf3bc9..d200b64 100644 --- a/index.js +++ b/index.js @@ -19,5 +19,7 @@ var initJspm = require('./src/init'); initJspm.$inject = ['config.files', 'config.basePath', 'config.jspm', 'config.client', 'emitter']; module.exports = { - 'framework:jspm': ['factory', initJspm] + 'preprocessor:jspm': ['factory', require('./src/preprocessor')], + 'framework:jspm': ['factory', initJspm], + 'reporter:jspm': ['type', require('./src/reporter')] }; diff --git a/package.json b/package.json index 27d8d5b..fa6da17 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,12 @@ "url": "git@github.com:Workiva/karma-jspm.git" }, "dependencies": { - "glob": "~3.2" + "glob": "~3.2", + "inline-source-map-comment": "^1.0.5", + "istanbul": "^0.4.3", + "karma-coverage": "^1.0.0", + "minimatch": "^3.0.0", + "remap-istanbul": "^0.6.3" }, "devDependencies": { "gulp": "^3.8.7", diff --git a/src/adapter.js b/src/adapter.js index 1268629..80e9668 100644 --- a/src/adapter.js +++ b/src/adapter.js @@ -47,6 +47,19 @@ System.bundles = []; } + var systemInstantiate = System.instantiate; + var http = location.protocol; + var slashes = http.concat('//'); + var host = slashes.concat(window.location.host) + '/base/'; + System.instantiate = function(load) { + var fileKey = load.address.replace(host, ''); + if (karma.config.jspm.coverageFiles[fileKey]) { + var re = new RegExp('file://' + karma.config.jspm.basePath + '/','g'); + load.source = karma.config.jspm.coverageFiles[fileKey].content.replace(re, host); + } + return systemInstantiate.call(SystemJS, load); + } + // Load everything specified in loadFiles in the specified order var promiseChain = Promise.resolve(); for (var i = 0; i < karma.config.jspm.expandedFiles.length; i++) { diff --git a/src/preprocessor.js b/src/preprocessor.js new file mode 100644 index 0000000..ab0f582 --- /dev/null +++ b/src/preprocessor.js @@ -0,0 +1,105 @@ +var fs = require('fs') +var jspm = require('jspm') +var istanbul = require('istanbul') +var rimraf = require('rimraf') +var inlineSourceMap = require('inline-source-map-comment') + +function getFileKey(filename, basePath) { + if(!basePath) throw new Error('Please supply a base path!') + return filename.replace(basePath + '/', '') +} + +function getOSFilePath(filename) { + return filename.replace('file://', '') +} + +function CoveragePreprocessor(basePath, client, reporters, helper) { + // if coverage reporter is not used, do not preprocess the files + if (!helper._.includes(reporters, 'jspm')) { + return function (content, _, done) { + done(content) + } + } + + // create a jspm namespace to pass data to the browsers + if (!client.jspm) client.jspm = {} + // store instrumented sources to be executed by browser + client.jspm.coverageFiles = {} + // store basePath used to generate the key for each source in the obj above + client.jspm.basePath = basePath + // temp folder to store instrumented files for sourcemap remapping + client.jspm.tempDirectory = basePath + '/__karma-jspm-tmp__/' + + // make temp directory + var tempDirectory = client.jspm.tempDirectory + if (!fs.existsSync(tempDirectory)) fs.mkdirSync(tempDirectory) + + // create systemjs hook to allow Istanbul to instrument transpiled sources + var instrument = new istanbul.Instrumenter() + var systemJS = new jspm.Loader() + var systemInstantiate = systemJS.instantiate + // "instantiate" is the hook that provides the transpiled source + systemJS.instantiate = function(load) { + try { + // create a unique key to store the sources of modules for the browser + var fileKey = getFileKey(getOSFilePath(load.address), basePath) + // exclude the dependency modules (i.e. libraries) from instrumentation + if (client.jspm.coverageFiles[fileKey]) { + var filename + // arrange sourcemaps + if (load.metadata.sourceMap) { + // put file's transpiled counterpart in temp folder + filename = tempDirectory + encodeURIComponent(fileKey) + // keeping sourcesContent causes duplicate reports + delete load.metadata.sourceMap.sourcesContent + // this is the file being "instrumented" + load.metadata.sourceMap.file = tempDirectory + encodeURIComponent(fileKey) + // removing "file://" from paths + load.metadata.sourceMap.sources = load.metadata.sourceMap.sources.map( + filename => getOSFilePath(filename) + ) + // write transpiled file with an inlined-sourceMap to temp directory + fs.writeFileSync( + filename, + load.source + '\n' + inlineSourceMap(load.metadata.sourceMap) + ) + } else { + // keep file as is since it doesn't have a sourcemap to be remaped + filename = getOSFilePath(load.address) + } + // instrument with istanbul + client.jspm.coverageFiles[fileKey] = instrument.instrumentSync( + load.source, + // make the path-like file key into something that can be used as a name + filename + ) + } + } catch (err) { + console.log(err) + // remove temp directory since something went wrong + rimraf.sync(tempDirectory) + } + // call the original "instantiate" hook function + return systemInstantiate.call(systemJS, load) + } + + return function (content, file, done) { + // only files to be instrumented pass through here + // store this information to allow "instantiate" to exclude + // dependency modules (i.e. libraries) + client.jspm.coverageFiles[getFileKey(file.path, basePath)] = true + // import modules + systemJS.import(file.path).then(function () { + done(content) + }).catch(function(err) { + console.log(err) + // remove temp directory since something went wrong + rimraf.sync(tempDirectory) + }) + } +} + +CoveragePreprocessor.$inject = ['config.basePath','config.client','config.reporters','helper'] + +// PUBLISH +module.exports = CoveragePreprocessor diff --git a/src/reporter.js b/src/reporter.js new file mode 100644 index 0000000..36f2144 --- /dev/null +++ b/src/reporter.js @@ -0,0 +1,32 @@ +var fs = require('fs') +var jspm = require('jspm') +var istanbul = require('istanbul') +var rimraf = require('rimraf') +var remapIstanbul = require('remap-istanbul') +var karmaCoverageReport = require('karma-coverage/lib/reporter') + +function CoverageReporter(rootConfig, helper, logger, emitter) { + var config = rootConfig.coverageReporter = rootConfig.coverageReporter || {} + config._onWriteReport = function (collector) { + try { + collector = remapIstanbul.remap(collector.getFinalCoverage()) + } catch(e) { + log.error(e) + } + return collector + } + config._onExit = function (done) { + try { + rimraf.sync(rootConfig.client.jspm.tempDirectory) + } catch(e) { + log.error(e) + } + done() + } + karmaCoverageReport.call(this, rootConfig, helper, logger, emitter) +} + +CoverageReporter.$inject = karmaCoverageReport.$inject + +// PUBLISH +module.exports = CoverageReporter