Avoiding empty JavaScript files in CSS-only entrypoints from rspack builds
I previously wrote about my preferred way of integrating Django with rspack. Minor aspects have changed, but most of it still applies.
What changed
The largest change is probably that I have switched from HtmlWebpackPlugin to rspack’s own HtmlRspackPlugin and have stopped configuring the template generated by it. Instead, I’m pulling out link, script, and style tags the Cthulhu way1. My own mistakes while configuring rspack have led to missing or duplicated assets. Letting rspack do its thing and post-processing the result feels safer.
The rest basically stayed the same.
RemoveEmptyJsAssetsPlugin
Poking around in rspack’s compiler hooks isn’t documented all that well, so I’m documenting this plugin here. It’s very specific, but being able to change rspack’s output may be useful to others and to my future self.
When using a CSS-only entrypoint or when listing CSS files in entry, rspack sometimes decides it wants to generate an empty JavaScript file. This wouldn’t be a problem in and of itself, but since we’re also letting rspack generate the associated script tag, we will make the browser download this empty script file unnecessarily. I didn’t find a good way to avoid these empty files, and the RemoveEmptyScriptsPlugin for Webpack doesn’t work with rspack anymore. The discussion hasn’t gone anywhere yet. So, I had to use my own plugin. Since I’m already copying the rspack.library.js file into each project, adding a plugin there is no headache.
class RemoveEmptyJsAssetsPlugin {
apply(compiler) {
compiler.hooks.emit.tap(this.constructor.name, (compilation) => {
const emptyJsFiles = new Set(
Object.entries(compilation.assets)
.filter(([name, asset]) => /\.js$/.test(name) && asset.size() === 0)
.map(([name]) => name),
)
for (const file of emptyJsFiles) {
delete compilation.assets[file]
}
for (const filename of Object.keys(compilation.assets)) {
if (!filename.endsWith(".html")) continue
let html = compilation.assets[filename].source()
for (const jsFile of emptyJsFiles) {
const idx = html.indexOf(jsFile)
if (idx < 0) continue
const start = html.lastIndexOf("<script", idx)
const end = html.indexOf("</script>", idx) + "</script>".length
html = html.slice(0, start) + html.slice(end)
}
compilation.assets[filename] = new rspack.sources.RawSource(html)
}
})
}
}
The plugin has to be added to the plugins list:
plugins: [
new RemoveEmptyJsAssetsPlugin(),
// ...
],
And presto, no more empty JavaScript files!
Closing
Ideally, rspack will handle this itself at some point and make the plugin obsolete. For now, it works without issues in several projects.
It’s mostly fine in this case since the output is very limited and well-structured – we don’t have to guard against invalid HTML. ↩