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.
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.
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.
You now understand custom preprocessor plugins. Next, learn practical techniques for improving Karma's performance.