Resolving fingerprinted assets using generateAssetMap

For production, I have Broccoli generate an assets map file assets/assetMap.json by including the following in my ember-cli-build.js file:

var app = new EmberApp(defaults, {
    fingerprint: {
        ...
        generateAssetMap: true
    }
});

Now I would like to define an HTMLBars helper which will take a given asset file name and convert it to its fingerprinted file name, something like this:

{{asset-resolve 'asset-name.ext'}} => 'asset-name-fingerprint.ext`

where fingerprint is the md5-string.

I guess that the best thing to do is create some kind of initializer which will load the assets/assetMap.json file and save it in some hash object which is then accessible via the helper.

Iā€™ve looked through several examples but am having problems getting my head around the nitty-gritty aspects, is there someone out there who can help me?

See also: https://github.com/rickharrison/broccoli-asset-rev/issues/24

I chose a more simplistic approach, namely the following:

Initializer:

// api/initializers/asset-map.js
import Ember from 'ember';

export function initialize(container, application) {
    application.deferReadiness();

    var AssetMap = Ember.Object.extend();

    var promise = new Ember.RSVP.Promise(function(resolve, reject) {
        Ember.$.getJSON('assets/assetMap.json', resolve).fail(reject);
    });

    promise.then(function(assetMap) {
        AssetMap.reopen({
            assetMap: assetMap,
            resolve: function(name) {
                return assetMap.assets[name];
            }
        });
    }, function() {
        AssetMap.reopen({
            resolve: function(name) {
                return name;
            }
        });
    }).then(function() {
        container.register('assetMap:main', AssetMap, {singleton: true});
        application.inject('model:post', 'assets', 'assetMap:main');
        application.advanceReadiness();
    });
}

export default {
  name: 'asset-map',
  initialize: initialize
};

Model:

/// app/models/post.js
import DS from 'ember-data';

export default DS.Model.extend({
    title: DS.attr('string'),
    ...
    photo: DS.attr('string'),

    photosrc: function() {
        var photo = this.get('photo');
        return this.assets.resolve('assets/events/' + photo);
    }.property('photo')
});

Temlate:

<img src="{{post.photosrc}}" ...  />

Hope this helps someone out there!

2 Likes

seems to work, thanks!

Glad to be of service. Perhaps thereā€™s a more elegant way to do this ā€¦

Well, Iā€™ve basically read all the issues on Github and they just say ā€œyeah we need to do it betterā€, but it hasnā€™t happened so far. :smile:

Iā€™ve only run into two problems:

  1. The assetMap is on the CDN as well in my case, so it has to be retrieved from there
  2. The initializer was timing out all my tests, so I substituted a ā€œdummyā€ service that just retuns the passed asset path in dev/testing

This is my version:

import Ember from 'ember';
import ENV from 'volders/config/environment';

export function initialize(container, application) {
  var AssetMap;

  application.deferReadiness();

  if (ENV.APP.CDN_PATH === '') {
    // use an asset map stub in development / testing
    AssetMap = Ember.Object.extend({
      resolve(name) {
        return name;
       }
    });

    container.register('assetMap:main', AssetMap, { singleton: true });
    application.inject('service:assets', 'assetMap', 'assetMap:main');
    application.advanceReadiness();
  } else {
    AssetMap = Ember.Object.extend();

    var promise = new Ember.RSVP.Promise(function(resolve, reject) {
      var assetMapURL = `${ENV.APP.CDN_PATH}assets/assetMap.json`;
      Ember.$.getJSON(assetMapURL, resolve).fail(reject);
    });

    promise.then(function(assetMap) {
      AssetMap.reopen({
        assetMap: assetMap,
        resolve: function(name) {
          // lookup asset in asset map; if not found, try the asset name itself
          return `${assetMap.prepend}${assetMap.assets[name]}` || name;
        }
      });
    }, function() {
      AssetMap.reopen({
        resolve: function(name) {
          return name;
        }
      });
    }).then(function() {
      container.register('assetMap:main', AssetMap, { singleton: true });
      application.inject('service:assets', 'assetMap', 'assetMap:main');
      application.advanceReadiness();
    });
  }
}

export default {
  name: 'asset-map',
  initialize: initialize
};

I set the ENV.APP.CDN_PATH in my config/environment.js by reading a JSON file that I also reuse in the ember-cli-build.js, so I donā€™t have to change paths in more than location.

edit: actually there was another problem in that the assetMap has a ā€œprependā€ entry that has to be prefixed to the actual asset lookup. Maybe this is something that changed recently. I updated my example.

Hereā€™s one approach w/o loading assetMap:

Add assetMapping structure to index.html:

<script type="text/javascript">
window.assetMapping = {
   app: 'assets/app.js',
   vendor: 'assets/vendor.js'
   // Rest of your assets
};
</script>

The assetMapping structure will be ā€˜untouchedā€™ in development - meaning the asset paths will remain unchanged, but in production those asset paths will change to reflect the added fingerprint. Weā€™re exploiting fact here that all instances of ā€˜assets/some-asset.jsā€™ in our application, will be replaced with ā€˜assets/some-asset-FINGERPRINT.jsā€™. So, when built:

<script type="text/javascript">
window.assetMapping = {
   app: 'assets/app-FINGERPRINT.js',
   vendor: 'assets/vendor-FINGERPRINT.js'
   // Rest of your fingerprinted assets
};
</script>

Then, use your asset-resolve helper:

// helpers/asset-resolve.js
import Ember from 'ember';

export function assetResolve(params/*, hash*/) {
  if (!window.assetMapping || !window.assetMapping[params[0]]) {
    Ember.Logger.error('No assetMapping found for ' + params[0]);
  }
  // Return here either raw resolved assetMapping (for development) or prefixed resolved mapping with CDN url (for production).
  return window.assetMapping[params[0]];
}

export default Ember.Helper.helper(assetResolve);

You can also drop adding script tag to index.js and just add assetMapping var to asset-resolve helper.

2 Likes

That certainly has the benefit of sparing the client one more request to the asset map, but I donā€™t think itā€™s feasible to manually manage your asset map. If there was a simple method (maybe an in-repo-addon?) to generate it, it would make much more sense.

But in fact, I think the cleanest solution would be for Ember to support importing the assetMap even if it doesnā€™t exist at compile time - similar to what is done with the config/environment.js. AFAIK there was some talk about implementing thisā€¦

So, is there a ā€œhumanā€ solution exist? Iā€™m a newcomer in EmberJS (and JS too) and these workarounds confused me a littleā€¦ Iā€™m faced with the same problem and stuck on it.

To my knowledge, unfortunately, there is no straightforward, out-of-the-box solution for this problem. Sorry. :frowning:

So the simplest way for a newbie will be just switch off this mechanism at all?

Yes, unfortunately it looks like the combination of:

  • CDN + asset digests
  • dynamically generated asset paths

donā€™t work very well atm. So you either have to invest time in finding a workaround (as e.g. suggested in this thread), or you forego one of those two things.

simplest thing that worked for me was hard coding the full asset path to every image name, broccoli-asset-rev then recognizes it and rewrites my js source

This addon was released recently to try to help with this issue (GitHub - adopted-ember-addons/ember-cli-ifa: Ember CLI addon for injecting fingerprinted asset map file into Ember app) and there are a few RFCs on the same subject. Hopefully this will go away soon :wink:

This looks great :slight_smile: Thanks for posting it here!

Anyone arriving here in Nov 2022 and not using a CDN should be aware that this behavior is handled automagically by ember-cli-deploy and broccoli-asset-rev. If you simply write a URL path to a fingerprinted asset file in a JavaScript string, the ā€œblack boxā€ that is Emberā€™s fingerprinting scheme will alter your URL path without telling you. This behavior is super helpful, but since it is not clearly explained in the docs for either ember-cli-deploy or broccoli-asset-rev, it can be confusing, bordering on infuriating.

With fingerprinting enabled, if you write a URL path to a css file like this: let URLpath = 'assets/app.css'; in any JavaScript file in your Ember app, it will automagically be updated to let URLpath = 'assets/app-38e6dce5c3e69f23d0a6948945245231.css'; without your knowledge. You can turn fingerprinting on in ember-cli-build to test in development, it is only enabled by default in production: fingerprint: { enabled: true },

HOWEVER, there appears to be at least one shortcoming to this ā€œblack boxā€ scheme, because it can sometimes ignore a URL path if it is written as part of a template literal string (surrounded by grave accent marks) instead of a plain string (surrounded by double or single quotes): let URLpath = `${window.location.protocol}//${window.location.host}/assets/app.css`; This template literal string will NOT have the css file ā€œfingerprintedā€ by Ember and the request to it will result in a 404. If you write this instead: let URLpath = `${window.location.protocol}//${window.location.host}` + ā€˜/assets/app.cssā€™; the fingerprinting scheme will work.

I ran into this error, and without understanding the mysterious ā€œblack boxā€ of Ember fingerprinting, I beat my head against a wall for several hours. Thanks Ember!

@Chris_N thank you for sharing post so helpful

Iā€™ll add in here as well, because we had another use case that was not covered by Ember out of the box.

We dynamically set a custom style sheet using ember-cli-head, with the ā€œstyleā€ received from our user details request. We have to retrieve the Assetmap to get the fingerprinted stylesheet name.