Resolving fingerprinted assets using generateAssetMap


#1

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


#2

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!


#3

seems to work, thanks!


#4

Glad to be of service. Perhaps there’s a more elegant way to do this …


#5

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.


#6

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.


#7

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…


#8

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.


#9

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


#10

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


Why i'm leaving ember
#11

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.


#12

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


#13

This addon was released recently to try to help with this issue (https://github.com/RuslanZavacky/ember-cli-ifa) and there are a few RFCs on the same subject. Hopefully this will go away soon :wink:


#14

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