Skip to content

Writing a Custom Karma Preprocessor Plugin

Custom preprocessors let you transform file contents in ways no existing karma-*-preprocessor package covers — a proprietary template format, a project-specific code generation step, or a niche transformation. This lesson covers writing one.

The Shape of a Preprocessor Plugin

A preprocessor plugin is a factory function returning a (content, file, done) function. Karma calls this function once per matching file, passing the raw file content, file metadata, and a done callback you call with the transformed content once ready.

// my-preprocessor.js
function createStripCommentsPreprocessor(logger) {
  const log = logger.create('preprocessor:strip-comments');

  return function(content, file, done) {
    log.debug('Processing %s', file.originalPath);
    const transformed = content.replace(/\/\*[\s\S]*?\*\//g, '');
    done(transformed);
  };
}

createStripCommentsPreprocessor.$inject = ['logger'];

module.exports = {
  'preprocessor:strip-comments': ['factory', createStripCommentsPreprocessor]
};

done(transformedContent) signals completion; preprocessors can be synchronous-looking but still support async work internally before calling done.

Registering and Using the Custom Preprocessor

// karma.conf.js
config.set({
  plugins: [
    'karma-jasmine',
    'karma-chrome-launcher',
    require('./my-preprocessor.js')
  ],
  preprocessors: {
    'src/**/*.spec.js': ['strip-comments']
  }
});
  • The plugin object's key follows the preprocessor:<name> convention, matching the name used in preprocessors.
  • Preprocessors chain in the order listed — a file can pass through several before being served.
  • file includes metadata like originalPath, useful for logging or path-dependent transformation logic.
  • Calling done(null, content) (with an explicit null error argument) is also supported syntax in some preprocessor conventions — check your Karma version's exact expected signature.

Custom Preprocessor Cheat Sheet

The key pieces every custom preprocessor plugin needs.

Piece Purpose
Factory function Returns the actual (content, file, done) transform function
content The raw, current content of the file being processed
file Metadata about the file, including originalPath
done(transformedContent) Signals completion, passing the transformed content onward
preprocessor:<name> key The naming convention Karma's plugin system expects

Handling Asynchronous Transformations

Because completion is signaled via the done callback rather than a return value, preprocessors naturally support asynchronous work — reading additional files, calling an external transformation service, or awaiting a promise-based compiler API.

return function(content, file, done) {
  someAsyncCompiler(content)
    .then((result) => done(result))
    .catch((err) => {
      console.error('Preprocessing failed for', file.originalPath, err);
      done(content); // fall back to original content, or handle differently
    });
};

Deciding how to handle a preprocessing error (fail the run vs. fall back) is a deliberate design choice your plugin needs to make explicitly.

Chaining a Custom Preprocessor with Built-In Ones

Custom preprocessors compose naturally with built-in ones — for example, running a custom transformation first, then handing the result to karma-coverage for instrumentation.

preprocessors: {
  'src/**/*.js': ['strip-comments', 'coverage']
}

Common Mistakes

  • Forgetting to call done() at all, which hangs the entire preprocessing pipeline for that file indefinitely.
  • Not handling errors inside asynchronous preprocessing logic, silently swallowing failures.
  • Ignoring file.originalPath, making debugging a specific file's transformation harder than necessary.
  • Writing a heavyweight custom preprocessor for a transformation an existing, well-tested package already handles.

Key Takeaways

  • A custom preprocessor plugin returns a (content, file, done) function, called once per matching file.
  • The preprocessor:<name> plugin key convention matches the name used in the preprocessors map.
  • Asynchronous transformations are naturally supported via the done callback.
  • Custom preprocessors chain naturally alongside built-in ones on the same file pattern.

Pro Tip

Always call done() inside a try/catch (or a promise .catch()) even for logic that looks like it can't fail. A preprocessor that never calls done() due to an uncaught error doesn't produce a clear error message — it just hangs the run silently, which is far more frustrating to debug.