Wallaby.js preprocessors can transform the content of your files before feeding it to the test runner. But if you would like to have code coverage for some file, wallaby.js has to instrument it before running any preprocessors. It means that wallaby.js has to understand the dialect of the file to be able to parse, change and then emit the file source code. Wallaby.js core supports ES6 (and earlier) plus JSX. It means that any other dialects or languages that you would like to get code coverage for need to be compiled to JavaScript before the instrumentation phase.

To allow for compilation from anything to JavaScript (ES6 or earlier), wallaby.js supports compilers that run before instrumentation and preprocessors.

Wallaby.js has three built-in compilers: TypeScript, CoffeeScript and Babel.

TypeScript and CoffeeScript compilers are turned on by default, while Babel compiler is turned off by default. This means that you don’t even have to configure the compiler at all, if are happy with the default compiler settings for TypeScript and CoffeeScript and you are not using Babel compiler.

Note that if you are using Babel compiler, you don’t need Babel preprocessor. If you are only using ES6 features and not using any experimental features, such as ES7, you may keep using Babel preprocessor without Babel compiler. However, in case of TypeScript and CoffeeScript, compilers are the only way to get coverage for such files with wallaby.js; preprocessors for TypeScript and CoffeeScript are not required.

The format of the compiler setting is very similar to that of the preprocessor: it is an object with file patterns as keys and compiler functions as values. Here is an example compiler configuration:


module.exports = function (wallaby) {
  return {
    files: [
      'src/**/*.js',
      'src/**/*.ts',
      'src/**/*.coffee'
    ],

    tests: [
      'test/**/*Spec.js'
      'test/**/*Spec.ts',
      'test/**/*Spec.coffee'
    ],

    compilers: {
      '**/*.js': wallaby.compilers.babel({
        // babel options
        // like `stage: n` for Babel 5.x or `presets: [...]` for Babel 6
        // (no need to duplicate .babelrc, if you have it, it'll be automatically loaded)
      }),

      '**/*.ts': wallaby.compilers.typeScript({
        // TypeScript compiler specific options
        // https://github.com/Microsoft/TypeScript/wiki/Compiler-Options
        // (no need to duplicate tsconfig.json, if you have it, it'll be automatically used)
      }),

      '**/*.coffee': wallaby.compilers.coffeeScript({
        // CoffeeScript compiler specific options
      })
    }
  };
};

All files that satisfy files and tests patterns will be tested against the compilers pattern, and those that match will be compiled.

Writing a custom compiler

If you would like to write your own compiler for some other language or any custom code transformation, the process is pretty straightforward. As preprocessors, a compiler function can be sync or async. It gets a file argument with content and other properties and is supposed to return an object with modified code and a source map.

The important difference between preprocessor and compiler is that a compiler must return a source map and it must transform the code to JavaScript. Another requirement is that a compiler must return a ranges property, where all coverable ranges of the original file must be listed.

For example, if some file in my hypothetical language looks like

var a = b ? c() : d();

and the conditional operator works like it does in JavaScript (and other languages), then

module.exports = function (wallaby) {
  return {
    files: [
      'src/**/*.hypotheticalLangExt'
    ],

    tests: [
      'test/**/*Spec.hypotheticalLangExt'
    ],

    compilers: {
      '**/*.hypotheticalLangExt': file => {
        // Parsing file.content

        // Traversing hypothetical language AST and collecting coverable ranges

        // Coverable ranges is an array of locations,
        // where each location is an array of the range
        // start line, start column, end line, end column.

        // In this specific case, we have a variable declaration statement
        // and 2 branches of a conditional expression within it,
        // so ranges would look like:
        // ranges = [
        //   [ 1, 0, 1, 22 ],   // whole statement
        //   [ 1, 12, 1, 15 ],  // c() execution path
        //   [ 1, 18, 1, 21 ]   // d() execution path
        // ];
        // Normally, parsers include (or have an option to include) location in AST nodes,
        // so it's just a matter of traversing the right nodes.

        // Generating JavaScript with source map

        return {
          map: sourceMap,
          code: generatedJavaScript,
          ranges: ranges
        };
      }
    }
  };
};