Thanks for this great summary, @svoop!
I’ve just replicated this here locally too, and IMO this is definitely a bug.
Per esbuild’s CSS docs, we do configure a loader for font files, but this clearly isn’t doing the job we need it to do.
To test this locally, I put a file in app/assets/fonts/
and referred to it from a CSS file just like your example above.
Then I edited my config/assets.js
to increase the level of logging from esbuild:
await assets.run({
esbuildOptionsFn: (args, esbuildOptions) => {
esbuildOptions.logLevel = "verbose";
return esbuildOptions;
},
});
And then when I ran hanami assets compile
, I saw this relevant bit of output:
[two_one] ⬥ [VERBOSE] Resolving import "../fonts/comic-mono.ttf" in directory "/Users/tim/Source/hanami/scratch/two_one/app/assets/css" of type "url-token"
[two_one]
[two_one] Checking "../fonts/comic-mono.ttf" against the external pattern "/Users/tim/Source/hanami/scratch/two_one/app/assets/images/*"
[two_one] Checking "../fonts/comic-mono.ttf" against the external pattern "/Users/tim/Source/hanami/scratch/two_one/app/assets/fonts/*"
[two_one] Read 1 entry for directory "/Users/tim/Source/hanami/scratch/two_one/app/assets/css"
[two_one] Checking "/Users/tim/Source/hanami/scratch/two_one/app/assets/fonts/comic-mono.ttf" against the external pattern "/Users/tim/Source/hanami/scratch/two_one/app/assets/images/*"
[two_one] Checking "/Users/tim/Source/hanami/scratch/two_one/app/assets/fonts/comic-mono.ttf" against the external pattern "/Users/tim/Source/hanami/scratch/two_one/app/assets/fonts/*"
[two_one] The path "/Users/tim/Source/hanami/scratch/two_one/app/assets/fonts/comic-mono.ttf" was marked as external by the user
[two_one] Read 5 entries for directory "/Users/tim/Source/hanami/scratch/two_one/app/assets"
[two_one] Read 1 entry for directory "/Users/tim/Source/hanami/scratch/two_one/app/assets/fonts"
[two_one] Primary path is "/Users/tim/Source/hanami/scratch/two_one/app/assets/fonts/comic-mono.ttf" in namespace "file"
See here how it’s mentioning that the font was “marked as external?” This is an esbuild feature that can indicate files to exclude from the build. And in assets-js, we actually mark all directories aside from css/
js/
as external. This is what allows us to then handle the files in those directories separately (such as images), and then later copy them across into the compiled assets directory as part of our hanami-assets esbuild plugin.
However, what we’re discovering is that this also means that files in those external directories cannot be referenced from within JS or CSS files and have their file references appropriately updated for the resulting bundle.
To test this for sure, I moved the font directly inside the app/assets/css/
directory, and referred to it there:
@font-face {
font-family: "comic-mono";
src: url("./comic-mono.ttf");
}
And then when I ran hanami assets compile
again, we see more encouraging output:
[two_one] ⬥ [VERBOSE] Resolving import "./comic-mono.ttf" in directory "/Users/tim/Source/hanami/scratch/two_one/app/assets/css" of type "url-token"
[two_one]
[two_one] Checking "./comic-mono.ttf" against the external pattern "/Users/tim/Source/hanami/scratch/two_one/app/assets/images/*"
[two_one] Checking "./comic-mono.ttf" against the external pattern "/Users/tim/Source/hanami/scratch/two_one/app/assets/fonts/*"
[two_one] Read 2 entries for directory "/Users/tim/Source/hanami/scratch/two_one/app/assets/css"
[two_one] Checking "/Users/tim/Source/hanami/scratch/two_one/app/assets/css/comic-mono.ttf" against the external pattern "/Users/tim/Source/hanami/scratch/two_one/app/assets/images/*"
[two_one] Checking "/Users/tim/Source/hanami/scratch/two_one/app/assets/css/comic-mono.ttf" against the external pattern "/Users/tim/Source/hanami/scratch/two_one/app/assets/fonts/*"
[two_one] Read 2 entries for directory "/Users/tim/Source/hanami/scratch/two_one/app/assets/css"
[two_one] No "browser" map found in directory "/Users/tim/Source/hanami/scratch/two_one/app/assets/css"
[two_one] Attempting to load "/Users/tim/Source/hanami/scratch/two_one/app/assets/css/comic-mono.ttf" as a file
[two_one] Checking for file "comic-mono.ttf"
[two_one] Found file "comic-mono.ttf"
[two_one] Checking "/Users/tim/Source/hanami/scratch/two_one/app/assets/css/comic-mono.ttf" against the external pattern "/Users/tim/Source/hanami/scratch/two_one/app/assets/images/*"
[two_one] Checking "/Users/tim/Source/hanami/scratch/two_one/app/assets/css/comic-mono.ttf" against the external pattern "/Users/tim/Source/hanami/scratch/two_one/app/assets/fonts/*"
[two_one] Read 2 entries for directory "/Users/tim/Source/hanami/scratch/two_one/app/assets/css"
[two_one] Primary path is "/Users/tim/Source/hanami/scratch/two_one/app/assets/css/comic-mono.ttf" in namespace "file"
Note above that it no longer says “marked as external by the user”, and instead it shows that it’s using the file loader for the font file in the “attempting to load … as a file” line.
And then if I inspect the compiled CSS, things look good too, with it referring to a properly hashed file, and that file actually existing alongside it inside public/assets/
:
@font-face{font-family:comic-mono;src:url("./comic-mono-5YVBLHHO.ttf")}
So what’s the takeaway here?
- We have a bug where files outside of the
css/
and js/
assets directories cannot be directly referenced within CSS and JS files and properly included in the bundle
- This is because we make those directories as “external”, which means esbuild doesn’t process those files
- I wonder if we even need that external config at all; what happens if we just take it away? We do currently use it inside our esbuild plugin to get the list of dirs whose assets we should manually copy, but what if we just did a direct filesystem crawl at that point (the one we currently do here), instead of using
build.initialOptions.external
? That might still give us the overall behaviour we want while avoiding this bug for files directly referenced from CSS/JS.
I think this approach is worth pursuing. @svoop — are you interested in having a go?