I have a love-hate relationship with ESBuild. Sometimes, when working with it, it makes me want to bang my head against the wall in annoyance. This is one such occasion.
Backgroundโ
At my $DAY_JOB , I work with the Logflare codebase, and do a lot of feature development. This codebase is in Elixir, Node.JS with a healthy dose of React. A mish-mash of SASS and TailwindCSS is used for styling, and many customizations has led to us writing our own build script to leverage ESBuild.
However, one fine day during my $DAY_JOB , I noticed that TailwindCSS classes were not being picked up from our Phoenix LiveView template files. Changes would only be picked up on first build, but not on subsequent file changes. Furthermore, the ESBuild watch mode only watched for files resolved through JS, meaning that it did not rebuild on other relevant file changes (which had the .heex
extension).
This, of course, was unacceptable. Can you imagine the drop in developer productivity if we had to restart the entire dev server just to view a single style change? Well, I can, because it annoyed me so much that I decided to fix this.
The Attemptsโ
I tried a few different methods:
The first was to try to use chokidar to watch for extra files and re-run the build each time. However, as the full build took around 10s each time, this was completely unacceptable speeds.
The second attempt was to run esbuild.context()
and use the context api to trigger manual rebuilds with ctx.rebuild()
. This too, did not result in the expected end result, as ESBuild will not perform a full rebuild, and will only rebuild for changed files (despite the function name...). If files were not declared by plugins in the watchFiles
or watchDirs
setup options, then ESBuild would ignore changes to these files.
The third attempt was to have a manual plugin to add these to the watchFiles
option. However, these did not trigger the entire plugin pipeline on rebuild, only that specific plugin. So this meant that the esbuild-sass-plugin that generated the css files would not get triggered, even if ESBuild was not watching file changes correctly. Argh!
And the 4th and final attempt is as documented below.
The Final Fixโ
Of course, the beauty of open source is that one can trawl through the source code and figure out a workaround if one is so motivated. And at this point, after so many walls, I was very motivated.
Looking through the esbuild-sass-plugin source code showed that it allowed users to provide additional plugin options to the transform()
callback. This, of course, was undocumented. However, the above attempts have revealed that the only way to get ESBuild to trigger the sass plugin was to add file paths to the plugin setup.
So this resulted in the following changes:
import { globSync } from "glob";
// needed because we want to run the sass-plugin for tailwind content files
const watchPaths = tailwindConfig.content.flatMap((pattern) => {
return globSync(pattern, { ignore: "node_modules/**" });
});
let sassPostcssPlugin = sassPlugin({
async transform(source, resolveDir) {
const { css } = await postcss([autoprefixer, tailwindcss]).process(source);
// specify the loader, otherwise plugin tries to resolve the files as js
// https://github.com/glromeo/esbuild-sass-plugin/blob/main/src/plugin.ts#L86
return { loader: "css", contents: css, watchFiles: watchPaths };
},
});
First, we find all file paths declared under our tailwind.config.js
. This is because ESBuild does not understand glob paths, and we need to specific specific paths and directories otherwise ESBuild would ignore them.
Secondly, we need to specify the loader as css
, otherwise the esbuid-sass-plugin
will try to resolve the watchFiles as JS files, and may result in some unexpected errors.
And now finally, we can watch our css files based on our TailwindCSS config file.