Source maps are a boon for web developers, especially as the usage of bundlers, preprocessors, and transpilers has skyrocketed. Browser developer tools can consume source maps, allowing for inspection and debugging of the original source rather than the compiled output actually run in the browser.
Generally speaking, source maps contain metadata mapping bundled or compiled JS and CSS to their respective original, un-mangled source files. For those interested, I recommend reading this introduction to source maps by Matt Zeunert. Additionally, Tobias Koppers and Paul Irish built a neat source map visualization tool that illustrates how text spans are mapped from output to source.
With static CSS, it’s easy to inspect a particular element in Chrome DevTools using the elements pane and with one click be taken to the original source containing the relevant styles.
Not surprisingly, this workflow can be disrupted when using CSS-to-JS, as the true source location for a given style is actually located in a JS file. Without a source map, clicking these styles merely results in being shown the corresponding line in the generated CSS in the page — far less useful than with traditional CSS source maps.
Restoring this functionality for CSS-in-JS can be a tricky problem for a host of reasons, including:
To address this problem, I built css-to-js-sourcemap, a low-level library for CSS-in-JS framework authors to generate useful source maps. Below is demonstration of how it’s used with Styletron.
In development, each styled component is also rendered with a no-op debug CSS class that has an associated source map pointing to the JS source location of the component definition. This makes it a single click to be taken to the style source from the elements pane, just like with CSS.
All this actually happens at runtime (no build tooling needed) with the heavy lifting being performed asynchronously off the main thread inside a web worker.
See the below overview of how this works:
Both parsing and generation of source maps is rather CPU intensive, so a web worker is the perfect use case for offloading this work, keeping the main thread unblocked. Furthermore, the source-map library (used in the web worker) was recently ported to WebAssembly, so for development purposes, doing this at runtime is quite viable.
Like all web workers, communication between the source map worker and the main thread happens asynchronously via postMessage
calls. In short, stack traces and class names are passed in, and CSS (with an inlined source map to JS) is sent back. Because these source maps are purely for development purposes, it’s totally fine for this to be asynchronous.
Usage of css-to-js-sourcemap looks like this (note that a CSS-in-JS library should abstract this away):
// Create the worker
const worker = new Worker(
"https://unpkg.com/css-to-js-sourcemap-worker/worker.js",
);
// Add a message handler to render CSS
worker.onmessage = msg => {
const { id, css } = msg.data;
if (id === "render_css") {
const style = document.createElement("style");
style.appendChild(document.createTextNode(css));
document.head.appendChild(style);
}
};
// Provide wasm binary to worker
worker.postMessage({
id: "init_wasm",
url: "https://unpkg.com/css-to-js-sourcemap-worker/mappings.wasm",
});
// Tell worker to post back new css (if any) every 120ms
worker.postMessage({
id: "set_render_interval",
interval: 120,
});
// Finally, give a class name and associated JS location
worker.postMessage({
id: "add_mapped_class",
className: "__debug-1",
stackInfo: new Error("An error object, usually passed from elsewhere"),
stackIndex: 2, // Index of the trace to use for JS location
});
Careful readers may have noticed the path in the demo video, src/home.js?n=0
. Hot reloading inline source maps is a bit problematic in browsers, so the query at the end is used as workaround to invalidate the old source maps. By changing the query, the browser effectively treats it as a new source.
The following code following tells the worker to regenerate the source maps when the source code has been changed.
if (module.hot) {
module.hot.addStatusHandler(status => {
if (status === "dispose") {
worker.postMessage({ id: "invalidate" });
}
});
}
Once the source map has been re-generated, the worker will post back the updated CSS to the main thread.
I hope that css-to-js-sourcemap (or at least a similar approach) could be adopted by other CSS-in-JS libraries to provide a better debugging experience. While I think this is a good start, depending on the abstraction, I think there’s definitely room for improving the source maps so they are even more useful.
Special thanks to the maintainers of the following libraries used in css-to-js-sourcemap:
mappings
property of the generated source maps.