TL;DR
Handling asset fingerprinting in Embroider still needs some polish. But there are ways to try out and use the RFC's that are eventually going to be productionized.
What is fingerprinting?
Assets
Let's start with the basics: assets. In the context of a web app this could mean a stylesheet, a javascript file, an image, a json file, etc. The browser caches these assets on the visitor's machine by default, which is great for performance! However when you're trying to make sure your users get the latest experience of your web app, you'll need a way to bust the cache and force a network request to updated assets.
Busting the cache
There are several ways to implement cache busting in your app. We'll be digging into the most common one: adding a unique identifier to your files. This technique usually involves appending a "contenthash" to all of your files. This unique identifier is calculated based on the file contents. If your file content changes, the hash will update.
Assets with unique identifiers shown in the Chrome devtools
Implementation in classic Ember apps
In classic Ember apps, asset fingerprinting is done using a "push-based" approach. This essentially means that all assets (build output and public folder) are inspected and a unique identifier is added for each asset. The references to these assets are subsequently updated to reflect the new name. Classic Ember apps make use of broccoli-asset-rev.
<img src="/images/bar.png" />
<script src="assets/appname.js">
background: url('/images/foo.png');
Gets translated into:
<img src="/images/bar-895d6c098476507e26bb40ecc8c1333d.png" />
<script src="assets/appname-342b0f87ea609e6d349c7925d86bd597.js">
background: url('/images/foo-735d6c098496507e26bb40ecc8c1394d.png');
This approach has worked in the past, but comes with a lot of caveats. There's no direct link between an asset and its reference because the package uses a series of regular expressions to identify an asset url. This can result in unexpected issues like the asset not getting fingerprinted when trying to refer to it using a dynamically built string. Embroider and ember-auto-import can help alleviate this problem, so let's have a look.
Implementation in Embroider apps
In Embroider, fingerprinting is delegated to the build tool (Webpack, Vite) and no longer embedded by the framework. This opens up possibilities for different approaches. The Ember community has worked on defining a recommended approach in the Asset importing spec RFC. This RFC recommends switching to a "pull-based" approach. By having direct references to assets in the code instead of plain strings, the build tool can determine the link up more easily. If an asset is not present, it can result in a build error.
While the RFC isn't truly implemented yet, we can achieve a similar "pull-based" approach ourselves by configuring the Webpack config in Embroider.
class extends Component {
myImage = import.meta.resolve('./hello.png');
}
<img src={{this.myImage}} />
RFC proposal on how an image could be referenced.
Configuration
Webpack actually supports importing assets out of the box. Granted you configure the correct "loader". For images you can make use of the asset/resource loader. Let's take a look at how this would be configured in Embroider:
// ember-cli-build.js
const { Webpack } = require('@embroider/webpack');
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
module.exports = function (defaults) {
const app = new EmberApp(defaults, {});
const packagerOptions = {
webpackConfig: {
module: {
rules: [
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset/resource',
},
],
},
},
};
return require('@embroider/compat').compatBuild(app, Webpack, {
packagerOptions,
});
};
In this code snippet, we've configured imports to .png
, jp(e)g
, .gif
and .svg
files to be handled by Webpack's asset/resource
loader. This means we can now do this in our app:
import FooImage from './foo.png';
class extends Component {
fooImage = FooImage;
}
{{!
the string will now resolve to the file's location
as determined by webpack!
}}
<img src={{this.fooImage}} />
Tada! Simple right? The asset/resource
loader has several configuration options which can be used to customize the exact path and filename, you can refer to the docs for more info. Similar loaders exist for other assets like stylesheets or json files.
Assets referenced from css
Plain CSS
Allright, works great for assets referenced from javascript or templates, but what about assets referenced from stylesheets? If you're using plain CSS, this should just work out of the box with Embroider. Embroider's Webpack package already pre-configures the correct loaders to handle style compilation.
Sass
If you're using a package like ember-cli-sass
to handle stylesheets, you'll need to move away from this package and switch to sass-loader in combination with css-loader instead.
Since the
app/styles
directory is reserved by Embroider for plain CSS files, you'll have to rename the directory to something else. We chosescss
for now.
Once all of this is done, you can import your app.scss
file in app.(js|ts)
and you're all set!
// app.js
import 'app-name/scss/app.scss';
Co-location vs Separation of concerns
The Asset importing spec RFC promotes co-location of assets, which is a change we have not covered so far. In essence, it means that you should put your asset as close as possible to its reference(s) as you can. Where as previously all of the assets were stored in one root public
folder, you would now store an image referenced in a component right next to the component itself.
Having a mix of both
If co-location and explicit import statements to assets might not be the ideal solution for you, there's an in-between solution available. It involves making use of a spec that's already implemented in Vite and for which there's also an Ember RFC available: Wildcard Module Import API. This RFC would introduce import.meta.glob
as an alternative to import multiple files at once using a wildcard pattern:
const assets = import.meta.glob('../static/**/*.*', {
eager: true,
});
babel-plugin-transform-vite-meta-glob
Since this RFC is not implemented yet, we'll need to set it up ourselves. This is where the babel-plugin-transform-vite-meta-glob babel plugin comes in. It will translate the import.meta.glob
statement to explicit import statements under the hood:
// app-name/utils/asset-map.js
const assets = import.meta.glob('../static/**/*.*', {
eager: true,
});
export default assets;
Becomes:
// app-name/utils/asset-map.js
import foo from "../static/images/foo.png";
const assets = {
"../static/images/foo.png": foo,
};
export default assets;
So essentially, the babel plugin will do the explicit work for us so that in turn, Webpack can process these imports.
import-asset helper
After the asset-map is automatically generated, you can write a simple import-asset
helper function on top of this to easily grab references to assets from either javascript or handlebars:
// app/helpers/import-asset
import assetMap from 'app-name/utils/asset-map';
export default function importAsset(assetPath) {
return assetMap[`../static/${assetPath}`];
}
<img src={{import-asset 'images/foo.png'}} />
import importAsset from 'app-name/helpers/import-asset';
class extends Component {
fooImage = importAsset('images/foo.png');
}
It should be noted that the babel-plugin mentioned is marked as "not safe to use in production". And some of the specification seems to be implemented incorrectly, for which I've opened a fix PR.
Conclusion
The final approach mentioned in this article minimizes the changes needed to adopt the new asset specification of Embroider, while keeping fingerprinting and a single asset folder around. However I must stress that co-location is the recommended way forward and as new features like single file components are hitting the Ember mainstream, supporting assets would look very similar to how it's done in other frameworks/libraries.
In general, it seems like Embroider still needs some polish in the area of fingerprinting and asset management. As Embroider is still actively being developed, anyone can contribute to getting the mentioned RFC's implemented. So I recommend everyone to read through them and try it out for themselves so we can gather feedback.
Top comments (0)