comments 4

How do Karma, Webpack, and karma-webpack interact?

Karma is an awesome test runner for web apps. Webpack is an awesome asset compiler for web apps. Both are crazy configurable and very complex. Here’s a detailed technical rundown how they interact with each other that will either bore you to death or save you from hours of teeth gnashing.

I spent most of today wrestling with Karma and Webpack as well as karma-webpack, the Karma plugin that interfaces Karma with Webpack. What I was trying to figure out was how all three process files when the ‘webpack’ preprocessor is configured in the Karma config file.

Here’s what I found out. (Hold on to your hats. It’s going to be a doozy.)

Karma

A Karma config file has two important sections for dealing with files: files and preprocessors.

The files section is an array containing patterns that specify the types of files that you want to include in the HTML test runner that’s generated. By default, the following will happen to/with files that are listed here after starting Karma:

  1. Karma goes through each of the file patterns and creates handles to all files that match each pattern.
  2. For each matching file, a file handle, the file’s contents, and a callback function are passed to any matching preprocessor. (Preprocessors are matched with file patterns using the entries in Karma config’s preprocessors section.)
  3. The preprocessor does some work, generally on the contents of the file.
  4. The preprocessor passes the file’s (usually modified) contents to the callback.
  5. (If multiple preprocessors are chained for any matching pattern, the callback passes the file handle, contents, and callback to the next preprocessor, forming a loop that continues until no preprocessors remain.)
  6. If there are no more preprocessors waiting to be applied, the processed file is stored.
  7. If the original file pattern object had its served property set to true, the file is made available through Karma’s locally-running HTTP server. (Default served: true.)
  8. If the original file pattern object had its included property set to true, the file gets included in the generated HTML test runner’s script tag. (Default included: true.)

To get Karma to work with Webpack, you probably know that you will need to install the karma-webpack plugin. The setup instructions tell you to modify your Karma config by adding the ‘webpack’ preprocessor entry, something like this:

preprocessors: {
  './src/**/*.js': ['webpack']
}

But what effect does this entry actually have and how does the karma-webpack plugin interact with Karma and Webpack?

karma-webpack plugin

Internally, the plugin has two components: the plugin itself and the preprocessor function. Upon starting Karma, the plugin itself is loaded by instantiating karma-webpack’s Plugin class, which is in turn is exported by the karma-webpack package as a module. We’ll deal with the preprocessor function later.

Here’s what happens when you fire up Karma, assuming that the plugin is installed and configured to be loaded (either explicitly in the Karma config file’s plugins section or by default if there is no plugins section):

  1. The Plugin constructor does some initial setup and creates a Webpack compiler instance, passing in the webpack configuration object defined in the Karma config file. (It also overrides some of the configuration options passed in, most notably anything dealing with output, so don’t worry about clobbering your pre-existing bundles if your Karma config file sets up Webpack options with something like webpack: require('./webpack.config').)
  2. The Plugin constructor registers several Webpack plugins (different from the karma-webpack Karma plugin; yes, this is confusing), which are basically just callback functions that are run at different stages of Webpack’s build process. The most important one of these functions is the “make” callback, which adds assets to be compiled in addition to the dependency tree created by the configured Webpack entry point. This will be very important later.
  3. The Plugin constructor wraps the Webpack compiler instance in a WebpackMiddlewareDev instance, which provides various functionality. Most importantly, the WebpackMiddlewareDev wrapper creates a fake in-memory filesystem using Webpack’s memory-fs module and configures the Webpack compiler to output all generated files to this filesystem.
  4. Also, the WebpackMiddlewareDev wrapper class constructor starts the compilation process.

Let’s recap. The karma-webpack plugin is loaded after starting Karma, which in turn automatically runs Webpack using whatever configuration options were passed in the webpack section of your Karma config file.

At this point, none of the files that you’ve listed in the files or preprocessors section of your config file are being processed or touched in any way whatsoever. All that’s happened so far is just an old fashioned Webpack run with the important exception that all output is being routed to an in-memory filesystem instead of being saved to disk.

Once Karma finishes loading its plugins, it starts to go through the files listed in the files section. It passes each file’s handle, contents, and a callback function (described below) to any matching preprocessors, as described above. Assuming that you’ve set up the Webpack preprocessor for a given file/file pattern, this is what karma-webpack will do:

  1. The file handle, a string representing its contents, and a callback function are passed to karma-webpack’s preprocessor function, as described above.
  2. The preprocessor adds each file to an internal array and calls invalidate() on the middleware-wrapped compiler instance after adding each file. invalidate() tells the compiler that compiled asset bundles are no longer good, which triggers a recompile.
  3. Remember the “make” plugin callback that was registered when Karma was started? This time, when the compiler starts, the make plugin takes the array of files populated by the preprocessor and adds them to the compiler’s make build step.
  4. According to Webpack’s plugin documentation, the make build step allows you to “add entries to the compilation or prefetch modules.” If you read karma-webpack’s code, you’ll understand that what this means in practice is that the files passed in from the preprocessor are passed to the SingleEntryDependency constructor, and the SingleEntryDependency instances are passed in to the addEntry method, which is what actually adds the files to the compilation as dependencies. (But dependencies of what? The compilation as a whole? It’s not clear.)
  5. Because the preprocessor files have been added as separate dependencies, they are compiled and the output is saved to the in-memory filesystem as bundles in addition to the main bundle. Despite the fact that they are passed in as instances of a class called SingleEntryDependency, each file gets its own standalone compiled bundle file. (NOTE: I learned the hard way that these files will not be generated unless there is an entry point in your Webpack config that points to a file with the same extension as the files that are passed into the preprocessor. Hence if you’re using a library like Riot or React and you want to pass in .tag or .jsx files respectively, you MUST specify a .tag or .jsx file as at least one entry point. I think this is meant to be an optimization feature, but it was a real PITA at the time to figure out.)
  6. At this point, control has returned to the preprocessor, which runs a function to read the newly compiled dependency-derived bundles (not the main bundle).
  7. The contents of each read bundle are passed to the preprocessor callback.
  8. Assuming that there are no more preprocessors to be called for the file, preprocessing ends and Karma saves the contents of the bundle into a new file.

What’s important here to note is that karma-webpack is taking the files that are passed in from the files and preprocessors sections of Karma’s config file. Those files are individually added as dependencies to the compiler’s run, compiled separately from the dependency tree , and then the compiled bundles’ contents are passed back to Karma to be saved, linked to in the HTML test runner file, and served during the browser-hosted test run.

In other words, the main asset bundles that are generated by the entry points in your Webpack config are totally discarded. The entry points are discarded. Nevertheless, for each file type passed in from files and preprocessors, you must have at least one entry point ending in the same extension specified in your Webpack config or else the passed in files will not be compiled during the “make” build step. When the preprocessor tries to read the compiled files, it will then choke and Karma will crash.

Wow. That was boring.

I know. But if you’re trying to figure out a bug in your testing setup, hopefully I’ve saved you a headache.

I’d like to use this info to write a more user-friendly tutorial on setting up a testing pipeline with Karma, Webpack, and Riot.js. All in due time!

4 Comments

  1. Chris

    Thank you for saving me hours of head scratching. I was expecting webpack to run and output to the ./dist directory and then for karma to load from there.

    Now I see you just set it to the src and webpack will process it first.

  2. Matt

    Thank you so much. I struggled with this recently for few days and have deduced some of what you mentioned in this article. But your full explanation here has been immensely helpful in filling gaps in my understanding. THANK YOU!

Leave a Reply