douglasduteil/isparta

gulp babel and isparta

Closed this issue · 7 comments

For some reason I can't use the normal ES6 imports with isparta in my gulpfile. If I use require it all works correctly, runs all my tests and covers all my code. But when I change over to use import I get:

$ gulp mocha
[14:54:55] Requiring external module babel/register
[14:54:56] Using gulpfile ~/Work/XXX/gulpfile.babel.js
[14:54:56] Starting 'istanbul'...

events.js:85
      throw er; // Unhandled 'error' event
            ^
Error: Unable to parse /Users/ed/Work/XXX/src/chat.js

Line 1: Unexpected reserved word

gulpfile:

import gulp from 'gulp'
import istanbul from 'gulp-istanbul'
// import isparta from 'isparta' // Doesn't work
const isparta = require('isparta') // Works
import mocha from 'gulp-mocha'

gulp.task('istanbul', cb => {
  gulp.src(['./src/**/*.js'])
    .pipe(istanbul({
      instrumenter: isparta.Instrumenter,
      includeUntested: true,
      babel: { stage: 0 }
    }))
    .pipe(istanbul.hookRequire())
    .on('finish', cb)
})

gulp.task('mocha', ['istanbul'], () => {
  return gulp.src('./test/**/*.js')
    .pipe(mocha({
      reporter: 'spec'
    }))
    .pipe(istanbul.writeReports({
      dir: './coverage',
      reportOpts: {dir: './coverage'},
      reporters: ['lcovonly']
    }))
})

I have the same problem, anyone found a solution yet?

I can import isparta from 'isparta;` without issue; however, I'm getting the same error:

events.js:85
      throw er; // Unhandled 'error' event
            ^
Error: Unable to parse Z:\Documents\GitHub\postcss-link-colors\js\plugin.js

Line 1: Unexpected token

If it helps, I'm using a gulpfile.babe.js and my test task looks like this:

import gulp from 'gulp';
import isparta from 'isparta';
import istanbul from 'gulp-istanbul';
import mocha from 'gulp-mocha';
import plumber from 'gulp-plumber';

export default function test(done) {
    gulp.src('js/**/*.js')
        .pipe(istanbul({
            instrumenter: isparta.Instrumenter,
            includeUntested: true,
            babel: { stage: 0 }
        }))
        .pipe(istanbul.hookRequire())
        .on('finish', () => {
            gulp.src(['js/**/*.spec.js'], { read: false })
                .pipe(plumber())
                .pipe(mocha({
                    reporter: 'spec',
                    clearRequireCache: true
                }))
                .pipe(istanbul.writeReports({
                    reporters: ['lcov']
                }))
                .pipe(istanbul.enforceThresholds({
                    thresholds: { global: 100 }
                }))
                .on('end', done);
        });
}

For anybody having this issue, I have found a solution for this.

Basically the problem originates from the fact that when you do import isparta from 'isparta';, the object that is returned is actually the original istanbul object, so the Instrumenter that you end up using is the original one, which does not understand ES6.

I have submitted a pull request to fix this problem.

In the meantime there is a nice workaround that you can use to make it work. Basically you have to import the instrumenter directly as an ES6 module, instead of using isparta.Instrumenter:

import { Instrumenter } from 'isparta';

gulp.src('...')
    .pipe(istanbul({ instrumenter: Instrumenter }))
    // ...

@StefanoDalpiaz is the require statement somehow different? Perhaps that's why this works?:

Yes, requiring a module is different from importing it in ES6 style. Try having a look at the babel-transpiled version of import isparta from 'isparta';. I have created one for you here.

Basically what happens is that when you import it that way, first of all the isparta module is required as you would in CommonJS style, and then that required module is passed through a function that checks if it's an ES6 module: if it is, then the variable is left as is, if not, it is wrapped in an object like { 'default': module }. This is so that we can cater at the same time for CommonJS exports like module.exports = { ... }; and ES6 default exports like export default { ... };.

Now let's have a look at the source for isparta.js:

  • first it imports istanbul, which is not an ES6 module originally, so under the hood it is wrapped in an object { 'default': istanbul } (note that in newer version of babel the function that creates this object has been refined, and it no longer wraps the istanbul module, but still, though for different reasons, eventually the instrumenter doesn't get properly imported)
  • then all its properties are cycled and added in commonJS style to the exports. In this case the only property is default (which contains the actual istanbul module, as explained in the previous point), so that's the only thing imported so far
  • then the Instrumenter class and the version are exported in ES6 style, which under the hood just adds them normally to the exports object.
  • eventually then, the exported data looks like:
    {
        'default': (original istanbul object),
        'Instrumenter': (isparta instrumenter),
        'version': (isparta version)
    }

Let's keep in mind this data and go back to the initial transpiled version of import isparta from 'isparta'. This is what happens:

  • isparta is imported via require
  • the imported module is passed through the wrapping function, which recognises that it is an ES6 module and leaves it as it is - without wrapping it
  • the actual module that is used is what is stored in the default property of the imported object, which is the original istanbul module
  • so the Instrumenter property is read from the original istanbul instead of isparta, thus the instrumentation fails.

The reason why import('isparta').Instrumenter works is that by using normal requiring, the transpiled script will not use the 'default' property of the imported module, so it will all work as expected. You can see it for yourself here.

Hope this is clear.

Wow, @StefanoDalpiaz, thanks for the in-depth explanation! It's all perfectly clear now :)

Alright Isparta is now exporting a Istanbul like structure
Closes #60