How to prevent karma-webpack from creating source maps of vendor code?

1.1k views Asked by At

I was experiencing really slow "boot" times for Karma test runner and after profiling the run, I realised that the largest slow down was caused by the creation of source maps.

More specifically, given that I'm using karma-webpack and webpack as a pre-processor, every time a test file is loaded by karma, it is fed to webpack which generates a source map for it.

Given that I wasn't splitting/chunking my app and vendor code, each test file was getting the same vendor source map (inlined).

I thought that I could fix this by simply preventing source mapping from occurring for node_modules/ files, but realised that you can only exclude files from getting source mapped, based on final asset files, not on input source/module files.

So I found this plugin to automatically split my app and vendor code into separate chunks (as opposed to having to manually list each vendor module).

Yet I started to get this error when running Karma: ReferenceError: Can't find variable: webpackJsonp

I'm pretty sure this is caused by the fact that Karma is not picking up on the fact that the vendor and app code are being split into separate chunks and hence is only including code configured with the files option (i.e. the test files themselves and not the vendor file).

It seems as if that files config option is parsed and handled before each test file is pre-processed, meaning I don't think it's possible to specify the vendor chunk in the files option, because Karma won't know about it at the time it tries to look for it (too early).

The only solutions I can see are:

  • Change the way Karma is implemented, such that it can handle the separation of vendor and app chunk files.
  • Instead of using karma-webpack and pre-processing, build the app in a test mode and then run Karma of the test build directory (so that the vendor chunk will exist early enough).

Is there a solution that I have missed?

I find it strange that it doesn't seem to be a common problem.

Edit 1

I found this, but the people there are suggesting the use of multiple entry points in the webpack config (i.e. one for the app and one for vendor). I will try and see if this works with Karma, but it still has a big downside (in my opinion) that you have to manually keep track of what you put in the vendor array. I.e. every time you install a package you have to add it to the array and vice versa.

Edit 2

Using multiple entry points (in the webpack config) doesn't even work before I configure the source map webpack plugin to exclude vendor files (whereas it would with the webpack-split-by-path plugin).

I'm going to try and implement the "build first and then test" approach.

1

There are 1 answers

0
pleasedesktop On BEST ANSWER

If anyone else comes across this problem, I got the "build then rest" approach working, i.e. I abandoned karma-webpack and Karma pre-processing, in favour of separate build and test commands (which get run one after the other).

Here is my webpack config that is specific to testing (e.g. webpack.config.babel.test.js):

import webpack from 'webpack';
import { join, resolve, parse } from 'path';
import SplitByPathPlugin from 'webpack-split-by-path';
import file from 'file';

const plugins = [
  new SplitByPathPlugin([
    {
      name: 'vendor',
      path: resolve(__dirname, 'node_modules')
    }
  ]),

  new webpack.SourceMapDevToolPlugin({
    test: /\.jsx?$/,
    exclude: [/vendor/]
  })
];

const entryPoints = {};
const testFileRegex = new RegExp('\.test\.jsx?$');
const pathPrefixRegex = new RegExp('^src/js/?');

file.walkSync('src/js/', (dirPath, dirs, files) => {
  for (const file of files) {
    if (file.match(testFileRegex)) {
      const parsedPath = parse(file);
      const entryKey = join(
        dirPath.replace(pathPrefixRegex, ''),
        parsedPath.name
      );
      entryPoints[entryKey] = './' + join(dirPath, parsedPath.name);
    }
  }
});

const config = {
  entry: entryPoints,
  output: {
    filename: '/[name]-[chunkhash].js',
    chunkFilename: '/[name]-[chunkhash].js',
    path: resolve(__dirname, 'dist-test'),
    pathinfo: true
  },
  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        loader: 'babel-loader!eslint-loader',
        exclude: [/node_modules/],
      },
      {
        test: /\.json$/, loader: 'json'
      }
    ],
  },
  bail: true,
  resolve: {
    extensions: ['', '.js', '.jsx', '.json'],
  },
  plugins: plugins,
  stats: {
    assets: false,
    cached: false,
    children: false,
    chunks: false,
    chunkModules: false,
    chunkOrigins: false,
    hash: false,
    modules: false,
    publicPath: false,
    reasons: false,
    source: false,
    timings: false,
    version: false,
    warnings: false
  },
  node: {
    child_process: 'empty',
    fs: 'empty'
  },
  externals: {
    'react/addons': true,
    'react/lib/ExecutionEnvironment': true,
    'react/lib/ReactContext': 'window'
  }
};

export default config;

The key part is that it creates an entry point for each test file and uses the webpack-split-by-path plugin to automatically separate app from vendor code, when generating chunks.

Here is the karma config I went with:

process.env.BABEL_ENV = 'test';

function karmaConfig(config) {
  config.set({
    basePath: '',
    frameworks: ['mocha'],
    files: [
      {
        pattern: 'dist-test/vendor-*.js',
        watched: false
      },
      'dist-test/**/*.test-*js?(x)'
    ],
    exclude: [],
    reporters: ['progress'],
    port: 9876,
    colors: true,
    browsers: ['Firefox', 'Chrome'],
    singleRun: true,
    logLevel: config.LOG_ERROR
  })
}

export default karmaConfig;

The key part is that the vendor file is listed first in the files config option and is set to not be watched, followed by the test files. This ensures that the vendor code is always loaded/inserted first for each test case.

My question asks if there is another way, but this approach works quite well. The performance is much better.

Edit 1

The only drawback of this approach (which I didn't realise at first) is that you can't really achieve test watching, as you could with karma-webpack and the pre-processing (by webpack), because of the decoupling of the build and test steps.

Edit 2

This approach suffered from the problem of re-transpiling vendor code every time you change your application code (even if you didn't add/remove any vendor libraries). This slowed things down redundantly.

What you want to do to solve this problem and also to be able to start test watching again is this:

  1. Run Webpack in watch mode, to build into your dist-test/ dir whenever your application code changes.
  2. Run Karma in watch mode pointing at all the test files that are being updated in the dist-test/ dir.

This solution is perfect, except for the one drawback that you can't run both of those things in one npm/yarn command.

Note: That your output file names for your Webpack test build shouldn't contain any hashes (i.e. the file names should remain consistent between changes of their content). This is so that when you change a test file, you don't run the old and the new test code (because of Webpack not deleting the old file)..