diff --git a/README.md b/README.md index 2d75c403..e814325a 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ Configuration is optional. It should be put in a file at `config/coverage.js` (` - `parallel`: Defaults to `false`. Should be set to true if parallel testing is being used, for example when using [ember-exam](https://github.com/trentmwillis/ember-exam) with the `--parallel` flag. This will generate the coverage reports in directories suffixed with `_` to avoid overwriting other threads reports. These reports can be joined by using the `ember coverage-merge` command (potentially as part of the [posttest hook](https://docs.npmjs.com/misc/scripts) in your `package.json`). +- `includeTranspiledSources`: Defaults to `[]`. Should include a list of transpiled JavaScript source extensions to be included in the coverage instrumentation. However, the compiled output is what will be instrumented so this will only be a close approximation of the source coverage. + #### Example ```js module.exports = { diff --git a/index.js b/index.js index 7904e6df..638328d1 100644 --- a/index.js +++ b/index.js @@ -37,7 +37,7 @@ module.exports = { return undefined; }, - preprocessTree: function(type, tree) { + postprocessTree: function(type, tree) { var useBabelInstrumenter = this._getConfig().useBabelInstrumenter === true; var babelPlugins = this._getConfig().babelPlugins; @@ -53,11 +53,8 @@ module.exports = { annotation: 'Instrumenting for code coverage', appName: this._parentName(), appRoot: this.parent.root, - babelOptions: this.app.options.babel, isAddon: this.project.isEmberCLIAddon(), - useBabelInstrumenter: useBabelInstrumenter, - babelPlugins: babelPlugins, - templateExtensions: this.registry.extensionsForType('template') + preCompiledExtensions: this.registry.extensionsForType('template').concat(this._getTranspiledSourceExtensions()) }); return new BroccoliMergeTrees([tree, instrumentedNode], { overwrite: true }); @@ -96,7 +93,18 @@ module.exports = { return true; } - return this._doesTemplateFileExist(relativePath); + return this._doesTemplateFileExist(relativePath) || this._doesFileExistAsTranspilationSource(relativePath); + }, + + /** + * Checks if a file exists as a transpiled source specified in the addon configuration + * @param {String} relativePath path to file within current app + * @returns {Boolean} whether or not the file exists within the current app + * @private + */ + _doesFileExistAsTranspilationSource: function(relativePath) { + var sourceExtensions = this._getTranspiledSourceExtensions(); + return this._doesPrecompiledFileExist(relativePath, sourceExtensions); }, /** @@ -146,18 +154,19 @@ module.exports = { }, /** - * Check if a template file exists within the current app/addon - * Note: Template files are already compiled into JavaScript files so we must - * check for the pre-compiled .hbs file - * @param {String} relativePath - path to file within current app/addon - * @returns {Boolean} whether or not the file exists within the current app/addon + * Checks if a file exists as a precompilation source + * @param {String} relativePath path to the file within the current app/addon + * @param {String[]} extensions list of precompilation extensions that the file may exist as + * @returns {boolean} Flag indicating whether the file exists with any of the precompilation extensions + * @private */ - _doesTemplateFileExist: function(relativePath) { - var templateExtensions = this.registry.extensionsForType('template'); + _doesPrecompiledFileExist: function(relativePath, extensions) { + var sourceExtensions = Array.isArray(extensions) ? extensions : []; + var extension, extensionPath; - for (var i = 0, len = templateExtensions.length; i < len; i++) { - var extension = templateExtensions[i]; - var extensionPath = relativePath.replace('.js', '.' + extension); + for (var i = 0, len = sourceExtensions.length; i < len; i++) { + extension = sourceExtensions[i]; + extensionPath = relativePath.replace('.js', '.' + extension); if (this._existsSync(extensionPath)) { return true; @@ -167,6 +176,18 @@ module.exports = { return false; }, + /** + * Check if a template file exists within the current app/addon + * Note: Template files are already compiled into JavaScript files so we must + * check for the pre-compiled .hbs file + * @param {String} relativePath - path to file within current app/addon + * @returns {Boolean} whether or not the file exists within the current app/addon + */ + _doesTemplateFileExist: function(relativePath) { + var templateExtensions = this.registry.extensionsForType('template'); + return this._doesPrecompiledFileExist(relativePath, templateExtensions); + }, + /** * Thin wrapper around exists-sync that allows easy stubbing in tests * @param {String} path - path to check existence of @@ -209,6 +230,15 @@ module.exports = { return config(this.project.configPath()); }, + /** + * Gets the list of transpiled source extensions from the host configuration options + * @returns {String[]} list of transpilation source extensions if provided + * @private + */ + _getTranspiledSourceExtensions: function() { + return this._getConfig().includeTranspiledSources || []; + }, + /** * Get paths to exclude from coverage * @returns {Array} exclude paths diff --git a/lib/config.js b/lib/config.js index b8887f71..c20ff97c 100644 --- a/lib/config.js +++ b/lib/config.js @@ -51,7 +51,8 @@ function getDefaultConfig() { reporters: [ 'html', 'lcov' - ] + ], + includeTranspiledSources: [] }; } diff --git a/lib/coverage-instrumenter.js b/lib/coverage-instrumenter.js index 1df7eb11..bd713746 100644 --- a/lib/coverage-instrumenter.js +++ b/lib/coverage-instrumenter.js @@ -3,17 +3,17 @@ require('string.prototype.startswith'); var existsSync = require('exists-sync'); var Filter = require('broccoli-filter'); -var BabelInstrumenter = require('./babel-istanbul-instrumenter'); -var Instrumenter = require('istanbul').Instrumenter; +var EmberInstrumenter = require('./ember-instrumenter'); var path = require('path'); -function getPathForRealFile(relativePath, root, templateExtensions) { + +function getPathForRealFile(relativePath, root, extensions) { if (existsSync(path.join(root, relativePath))) { return relativePath } - for (var i = 0, len = templateExtensions.length; i < len; i++) { - var extension = templateExtensions[i]; + for (var i = 0, len = extensions.length; i < len; i++) { + var extension = extensions[i]; var templatePath = relativePath.replace('.js', '.' + extension); if (existsSync(templatePath)) { @@ -24,7 +24,7 @@ function getPathForRealFile(relativePath, root, templateExtensions) { return null; } -function fixPath(relativePath, name, root, templateExtensions, isAddon) { +function fixPath(relativePath, name, root, extensions, isAddon) { // Handle addons if (isAddon) { // Handle addons (served from dummy app) @@ -32,8 +32,8 @@ function fixPath(relativePath, name, root, templateExtensions, isAddon) { relativePath = relativePath.replace('dummy', 'app'); var dummyPath = path.join('tests', 'dummy', relativePath); return ( - getPathForRealFile(dummyPath, root, templateExtensions) || - getPathForRealFile(relativePath, root, templateExtensions) || + getPathForRealFile(dummyPath, root, extensions) || + getPathForRealFile(relativePath, root, extensions) || relativePath ); } @@ -43,13 +43,13 @@ function fixPath(relativePath, name, root, templateExtensions, isAddon) { if (regex.test(relativePath)) { relativePath = relativePath.replace(regex, 'addon/'); return ( - getPathForRealFile(relativePath, root, templateExtensions) || + getPathForRealFile(relativePath, root, extensions) || relativePath ); } } else { relativePath = relativePath.replace(name, 'app'); - return getPathForRealFile(relativePath, root, templateExtensions) || relativePath; + return getPathForRealFile(relativePath, root, extensions) || relativePath; } return relativePath; @@ -63,22 +63,8 @@ function CoverageInstrumenter(inputNode, options) { this._appName = options.appName; this._appRoot = options.appRoot; this._isAddon = options.isAddon; - this._useBabelInstrumenter = options.useBabelInstrumenter; - this._babelPlugins = options.babelPlugins; - - this._babelOptions = options.babelOptions || {}; - - // The presence of the following babel options cause tests to fail so let's - // simply remove them from the babel config - [ - 'compileModules', - 'resolveModuleSource', - 'includePolyfill' - ].forEach(function(key) { - delete this._babelOptions[key]; - }.bind(this)); - this._templateExtensions = options.templateExtensions; + this._preCompiledExtensions = options.preCompiledExtensions; Filter.call(this, inputNode, { annotation: options.annotation @@ -89,29 +75,18 @@ CoverageInstrumenter.prototype.extensions = ['js']; CoverageInstrumenter.prototype.targetExtension = 'js'; CoverageInstrumenter.prototype.processString = function(content, relativePath) { - var instrumenter - - if (this._useBabelInstrumenter) { - instrumenter = new BabelInstrumenter({ - babel: this._babelOptions, - plugins: this._babelPlugins, - embedSource: true, - noAutoWrap: true - }); - } else { - instrumenter = new Instrumenter({ - embedSource: true, - esModules: true, - noAutoWrap: true - }); - } + var instrumenter; + instrumenter = new EmberInstrumenter({ + embedSource: true, + noAutoWrap: true + }); - relativePath = fixPath(relativePath, this._appName, this._appRoot, this._templateExtensions, this._isAddon); + relativePath = fixPath(relativePath, this._appName, this._appRoot, this._preCompiledExtensions, this._isAddon); try { return instrumenter.instrumentSync(content, relativePath); } catch (e) { - console.error('Unable to cover:', relativePath, '. Newer JS features may need Babel instrumentation to work. Try setting useBabelInstrumenter to true in your config/coverage.js.\n', e.stack); + console.error('Unable to cover:', relativePath, '. Please try to enable source maps "inline" on babel conf.\n', e.stack); } }; diff --git a/lib/babel-istanbul-instrumenter.js b/lib/ember-instrumenter.js similarity index 82% rename from lib/babel-istanbul-instrumenter.js rename to lib/ember-instrumenter.js index 2a740929..1095743c 100644 --- a/lib/babel-istanbul-instrumenter.js +++ b/lib/ember-instrumenter.js @@ -3,11 +3,12 @@ /** * This is a modified copy of the isparta instrumenter * @reference: https://github.com/douglasduteil/isparta/blob/master/src/instrumenter.js + * + * Modified again by @igbopie to make it generic (it wont transpile any code) */ var extend = require('extend'); var istanbul = require('istanbul'); -var babelTransform = require('babel-core').transform; var esprima = require('esprima'); var escodegen = require('escodegen'); var SourceMapConsumer = require('source-map').SourceMapConsumer; @@ -21,11 +22,6 @@ function Instrumenter(options) { istanbul.Instrumenter.call(this, options); // Call super constructor - this.babelOptions = extend({ - sourceMap: true - }, options && options.babel || {}); - this.plugins = options.plugins; - return this; } @@ -33,19 +29,24 @@ function Instrumenter(options) { Instrumenter.prototype = Object.create(istanbul.Instrumenter.prototype); Instrumenter.prototype.constructor = Instrumenter; +/** + * With the new modification, this code will be executed at the end of the build, so all the + * resources are available and transpiled already. That way, we will use source maps generated by + * babel/typescript to extract the original source code. + * + * This way our code won't be affected by the way we transpile our code (Only by the way the sourcemaps are generated). + * + * Babel/Typescript needs to be setup to ouput inline source maps. + */ Instrumenter.prototype.instrumentSync = function(code, fileName) { - var plugins = this.babelOptions.plugins; - // If we're running in coverage, we're assuming that this is running in CI or in some - // form of test scenario and not being built for production. So it's fine to always - // force this plugin to work. - for (var plugin of this.plugins) { - plugins.push(plugin); - } - var result = this._r = (0, babelTransform)(code, extend({}, this.babelOptions, { filename: fileName })); - this._babelMap = new SourceMapConsumer(result.map); + // Source map base64 extraction from file. Only inline supported for now. + var reg = new RegExp('# sourceMappingURL=data:application\/json;charset=utf-8;base64,(.*)$'); + var base64 = reg.exec(code)[1]; + var srcMapStr = new Buffer(base64, 'base64').toString('utf8'); + var map = JSON.parse(srcMapStr); // PARSE - var program = esprima.parse(result.code, { + var program = esprima.parse(code, { loc: true, range: true, tokens: this.opts.preserveComments, @@ -53,11 +54,13 @@ Instrumenter.prototype.instrumentSync = function(code, fileName) { sourceType: 'module' }); + this._srcMap = new SourceMapConsumer(map); + if (this.opts.preserveComments) { program = escodegen.attachComments(program, program.comments, program.tokens); } - return this.instrumentASTSync(program, fileName, code); + return this.instrumentASTSync(program, fileName, map.sourcesContent[0]); }; Instrumenter.prototype.getPreamble = function(sourceCode, emitUseStrict) { @@ -177,7 +180,7 @@ Instrumenter.prototype._getOriginalPositionsFor = function(generatedPositions) { function reducer(originalPositions, current) { var generatedPosition = current[0]; var position = current[1]; - var originalPosition = this._babelMap.originalPositionFor(generatedPosition); + var originalPosition = this._srcMap.originalPositionFor(generatedPosition); // Remove extra keys delete originalPosition.name; delete originalPosition.source; diff --git a/package.json b/package.json index e763c0e6..51147729 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ember-cli-code-coverage", - "version": "0.4.2", + "version": "0.5.0", "description": "Code coverage for ember projects using Istanbul", "directories": { "doc": "doc", @@ -55,8 +55,6 @@ "ember-addon" ], "dependencies": { - "babel-core": "^6.24.1", - "babel-plugin-transform-async-to-generator": "^6.24.1", "body-parser": "^1.15.0", "broccoli-filter": "^1.2.3", "broccoli-funnel": "^1.0.1", diff --git a/test/unit/index-test.js b/test/unit/index-test.js index 4a5cbb20..11d998b7 100644 --- a/test/unit/index-test.js +++ b/test/unit/index-test.js @@ -231,12 +231,12 @@ describe('index.js', function() { var result; beforeEach(function() { - sandbox.stub(Index, '_existsSync').returns(true); + sandbox.stub(Index, '_doesPrecompiledFileExist').returns(true); result = Index._doesTemplateFileExist('app/templates/application.js'); }); it('uses path to hbs file', function() { - expect(Index._existsSync.lastCall.args).to.eql(['app/templates/application.hbs']); + expect(Index._doesPrecompiledFileExist.lastCall.args).to.eql(['app/templates/application.js', ['hbs']]); }); it('returns true', function() { @@ -248,12 +248,102 @@ describe('index.js', function() { var result; beforeEach(function() { - sandbox.stub(Index, '_existsSync').returns(false); + sandbox.stub(Index, '_doesPrecompiledFileExist').returns(false); result = Index._doesTemplateFileExist('app/templates/application.js'); }); it('uses path to hbs file', function() { - expect(Index._existsSync.lastCall.args).to.eql(['app/templates/application.hbs']); + expect(Index._doesPrecompiledFileExist.lastCall.args).to.eql(['app/templates/application.js', ['hbs']]); + }); + + it('returns false', function() { + expect(result).to.be.false; + }); + }); + }); + + describe('_doesFileExistAsTranspilationSource', function () { + describe('when file exists', function() { + var result; + + beforeEach(function() { + sandbox.stub(Index, '_doesPrecompiledFileExist').returns(true); + sandbox.stub(Index, '_getTranspiledSourceExtensions').returns(['ts']); + result = Index._doesFileExistAsTranspilationSource('app/utils/file.js'); + }); + + it('uses path to ts file', function() { + expect(Index._doesPrecompiledFileExist.lastCall.args).to.eql(['app/utils/file.js', ['ts']]); + }); + + it('returns true', function() { + expect(result).to.be.true; + }); + }); + + describe('when file does not exist', function() { + var result; + + beforeEach(function() { + sandbox.stub(Index, '_doesPrecompiledFileExist').returns(false); + sandbox.stub(Index, '_getTranspiledSourceExtensions').returns(['ts']); + result = Index._doesFileExistAsTranspilationSource('app/utils/file.js'); + }); + + it('uses path to ts file', function() { + expect(Index._doesPrecompiledFileExist.lastCall.args).to.eql(['app/utils/file.js', ['ts']]); + }); + + it('returns true', function() { + expect(result).to.be.false; + }); + }); + }); + + describe('_doesPrecompiledFileExist', function() { + describe('when file is not precompiled', function() { + var result; + beforeEach(function() { + sandbox.stub(Index, '_existsSync').returns(false); + result = Index._doesPrecompiledFileExist('app/utils/file.js'); + }); + + it('should not check if a file exists', function() { + expect(Index._existsSync).not.to.have.been.called; + }); + + it('returns true', function() { + expect(result).to.be.false; + }); + }); + + describe('when precompiled file exists', function() { + var result; + + beforeEach(function() { + sandbox.stub(Index, '_existsSync').returns(true); + result = Index._doesPrecompiledFileExist('app/utils/file.js', ['ts']); + }); + + it('uses path to ts file', function() { + expect(Index._existsSync.lastCall.args).to.eql(['app/utils/file.ts']); + }); + + it('returns true', function() { + expect(result).to.be.true; + }); + }); + + describe('when precompiled file does not exist', function() { + var result; + + beforeEach(function() { + sandbox.stub(Index, '_existsSync').returns(false); + result = Index._doesPrecompiledFileExist('app/utils/file.js', ['ts']); + }); + + it('uses path to ts file', function() { + expect(Index._existsSync.lastCall.args).to.eql(['app/utils/file.ts']); }); it('returns false', function() { @@ -262,6 +352,37 @@ describe('index.js', function() { }); }); + describe('_getTranspiledSourceExtensions', function() { + describe('when includeTranspiledSources not defined in config', function() { + var results; + + beforeEach(function() { + sandbox.stub(Index, '_getConfig').returns({}); + results = Index._getTranspiledSourceExtensions(); + }); + + it('return no extensions', function() { + expect(results.length).to.equal(0); + }); + }); + + describe('when _getTranspiledSourceExtensions is defined in config', function() { + var results; + + beforeEach(function() { + sandbox.stub(Index, '_getConfig').returns({ + includeTranspiledSources: ['ts', 'coffee'] + }); + + results = Index._getTranspiledSourceExtensions(); + }); + + it('returns two extensions', function() { + expect(results.length).to.equal(2); + }); + }); + }); + describe('_getExcludes', function() { beforeEach(function() { sandbox.stub(Index, '_filterOutAddonFiles').returns('test'); diff --git a/tests/dummy/config/coverage-babel.js b/tests/dummy/config/coverage-babel.js index 9b29422a..13f8da9a 100644 --- a/tests/dummy/config/coverage-babel.js +++ b/tests/dummy/config/coverage-babel.js @@ -1,5 +1,5 @@ /* eslint-env node */ module.exports = { - useBabelInstrumenter: true + sourceMaps: 'inline' }; diff --git a/tests/dummy/config/coverage-nested-folder.js b/tests/dummy/config/coverage-nested-folder.js index ae5fac98..6a847518 100644 --- a/tests/dummy/config/coverage-nested-folder.js +++ b/tests/dummy/config/coverage-nested-folder.js @@ -3,5 +3,6 @@ module.exports = { coverageFolder: 'coverage/abc/easy-as/123', - parallel: true + parallel: true, + sourceMaps: 'inline' }; diff --git a/tests/dummy/config/coverage-parallel.js b/tests/dummy/config/coverage-parallel.js index a33514b6..b6e798a2 100644 --- a/tests/dummy/config/coverage-parallel.js +++ b/tests/dummy/config/coverage-parallel.js @@ -1,5 +1,6 @@ /* eslint-env node */ module.exports = { - parallel: true + parallel: true, + sourceMaps: 'inline' }; diff --git a/yarn.lock b/yarn.lock index 86076532..9c754074 100644 --- a/yarn.lock +++ b/yarn.lock @@ -683,7 +683,7 @@ babel-plugin-syntax-trailing-function-commas@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" -babel-plugin-transform-async-to-generator@^6.22.0, babel-plugin-transform-async-to-generator@^6.24.1: +babel-plugin-transform-async-to-generator@^6.22.0: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761" dependencies: