ember-cli / broccoli-asset-rev

Broccoli plugin to add fingerprint checksums and CDN URLs to your assets
MIT License
86 stars 83 forks source link

How to implement fingerprinting for dynamic asset url #24

Closed zetas closed 9 years ago

zetas commented 9 years ago

Hi, I'm having some trouble with broccoli-asset-rev in ember-cli.

My use-case is loading a logo from the database like so:

<img {{bind-attr src=logo}}/>

The issue is the fingerprint for that image is not appended at runtime and so in production, none of these assets work. It's looking for /assets/images/bar023.jpg but the actual file is now /assets/images/bar023-ad232dd232d23423.jpg

Of course I can disable fingerprinting, and that's what I've done in the meantime but I really would like to be able to use it, just somehow append the fingerprint in the template or maybe as a property on the controller?

Thanks, David

rickharrison commented 9 years ago

Can you clarify your use case some more. You have an image name stored in the database, but the asset is stored in your application code?

zetas commented 9 years ago

The logo property stores a relative asset URL like "/assets/images/bar-23.jpg" and that actual file is in my public/assets/images folder of my ember-cli app, which gets output to the dist folder with the fingerprints added.

If there's a better way to achieve what I'm trying to do (allowing users to modify the bars logo) I'm all ears. I'm still early on in the development process.

Thanks.

rickharrison commented 9 years ago

I'm not sure if this will work in an ember app, but here is what I do with handlebars on my server side app. I have a helper like this:

var handlebars = require('handlebars');
var assetMap = require('../../dist/assets/assetMap.json');
var prepend = assetMap.prepend || '/';

handlebars.registerHelper('asset', function (asset) {
  return prepend + assetMap.assets[asset];
});

And then in a handlebars template, I can access the asset via:

<link rel="stylesheet" href="{{asset 'path/to/filename.css'}}">

Perhaps you can use the assetMap.json in some way to look up the filename at runtime.

EmergentBehavior commented 9 years ago

I'm also building an ember project and am running into a similar issue. In a handlebars template, I bind the src attribute of an image to some string which I calculate in one of my Ember controllers. However, when I have fingerprinting/prepending enabled, these paths are not updated and the images are subsequently broken.

EmergentBehavior commented 9 years ago

I may be able to get around this by defining the CDN path in an environmental variable, but it would be nice if we could do fingerprinting too.

rickharrison commented 9 years ago

Have you tried reading the assetMap as I described above? I think that may solve your problem.

EmergentBehavior commented 9 years ago

Unless I'm missing something, an asset map isn't produced in ember-cli (or at least it doesn't appear in dist/assets after build).

rickharrison commented 9 years ago

Try passing generateAssetMap: true in as one of the options.

EmergentBehavior commented 9 years ago

Will give it a go. If it works I'll report back to the ember-cli folks :)

EmergentBehavior commented 9 years ago

Adding generateAssetMap: true in the proper place in my Brocfile did produce the asset map. However, in ember-cli I don't think I can we can use helpers the way you use them. We'd have to use {{bind-attr href=something}}.

rickharrison commented 9 years ago

Could you bind the attribute to a variable, which you can then create in your controller that reads in the asset map?

EmergentBehavior commented 9 years ago

That could work. I'll try creating a Mixin in Ember that incorporates your idea so we can update dynamic paths created inside controllers, etc.

EmergentBehavior commented 9 years ago

I can't seem to be able to access/import/require the assetMap in a controller, perhaps it's not created yet? Can ping the ember-cli folks too to see if they have ideas.

rickharrison commented 9 years ago

Can you post an example of what you are trying to accomplish? How you generate the URLs and how you use it in a template

EmergentBehavior commented 9 years ago

So in my project I'm working on generating paths for movie posters. So, for example, I may have something like this in a controller:

posterURL: function() {
        var prefix = config.environment === 'staging' ? config.cdnPath : '';
        return prefix + 'img/posters/' + this.get('id') + '.jpeg';
    }.property('id'),

I pull a CDN prefix from my environmental settings, but this doesn't do anything regarding hashes. Then I basically use the id of the movie from my current model and build the path from that.

Then, in my corresponding template, I have something like this:

<img {{bind-attr src=posterURL}} />
rickharrison commented 9 years ago

@rwjblue Do you have any ideas on the best approach to solve this problem?

chnn commented 9 years ago

This is a bit of a workaround, but you could use the customHash option instead of the default md5 hashes for the fingerprint. Then, because you know what to expect for the custom hash, you can build that assumption into your computed property:

return prefix + 'img/posters/' + this.get('id') + '-' + myCustomHash + '.jpeg';

For a custom hash, I use the current git commit SHA. It's predictable (you can look it up), and will change every time you deploy so the cache still gets broken.

rickharrison commented 9 years ago

I'm going to close this for now. If you still are having trouble with this, please let me know.

mksplg commented 9 years ago

I stumbled over the same problem. The workaround I've used now is async loading the assetMap.json and injecting a dependency in an initializer. It provides an assets.resolve(localPath) function.

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('component', 'assets', 'assetMap:main');
    application.advanceReadiness();
  });
}

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

This can then be used with this.assets.resolve('img/posters/' + this.get('id') + '.jpeg'). Hope this helps someone else.

marlonmantilla commented 9 years ago

Hi everyone, I'm new to Ember here ... Can't see assetMap.json file on my project neither in my dist folder. Any ideas of how to achieve this in ember-cli ?

rickharrison commented 9 years ago

Use generateAssetMap as shown in the readme https://github.com/rickharrison/broccoli-asset-rev

ehubbell commented 9 years ago

We're running into a similar issue. For our application, we're generating dynamic images from a helper and then injecting the correct one into a couple templates.

The use-case is pretty simple. We're checking the type of credit card a user has on file with us, then we're generating a path to the correct image asset (in our public folder) to display next to the card at checkout and in a user's settings.

Due to fingerprinting, our app is unable to find the asset path since we're generating the path outside our templates and stylesheets.We have a workaround for now however it'd be nice if we could fingerprint images from files outside our templates & stylesheets.

kgish commented 9 years ago

@mksplg +1

How would I register a HTMLBars helper which would do the same for me but in the templates?

{{asset-resolve 'name-of-asset'}}
wehlutyk commented 9 years ago

@mksplg did you find a way to get the assetMap at compile-time?

My use-case is with ember-cli-deploy, which sends all the assets to (e.g.) S3, and only index.html to your server, so there's no simple access to assetMap.json (deployment will send it fingerprinted to S3). I stumbled upon ember-cli-inject-asset-map but there might be some cleanup to do there.

kgish commented 9 years ago

This is my helper asset-resolve

import Ember from 'ember';

export function assetResolve(asset) {
    var fn = Ember.container.lookup('assetMap:main');
    var res = asset;
    if (fn) {
        var a = fn(asset);
        if (a) { res = a; }
    }
    return res;
}

export default Ember.Helper.helper(assetResolve);
Fryie commented 9 years ago

I have chosen a slightly different solution. I have a config/cdn.json file, and then I just read it in two places. I also use this package to read the current commit SHA and use it as a fingerprint:

// ember-cli-build.js
var cdnConfig = JSON.parse(require('fs').readFileSync('config/cdn.json'));
var cdnPath = cdnConfig[process.env.EMBER_ENV];
var isProductionLikeBuild = ... // check if production or staging or something

var app = new EmberApp(defaults, {
  fingerprint: {
    prepend: cdnPath,
    enable: isProductionLikeBuild,
    customHash: require('git-rev-sync').long()
})

// config/environment.js
var cdnConfig = JSON.parse(require('fs').readFileSync('config/cdn.json'));
ENV.APP.CDN_PATH = cdnConfig[process.env.EMBER_ENV];
ENV.APP.FINGERPRINT = require('git-rev-sync').long();

Yes, that duplicates the part of reading the file, but the stuff that is actually more likely to change, the paths themselves, are stored in one location.

With this, I can build a simple util:

import ENV from 'my-app/config/environment';
export function assetPath(asset, extension) {
  return `${ENV.APP.CDN_PATH}assets/${asset}-${ENV.APP.FINGERPRINT}.${extension}`;
}
RuslanZavacky commented 9 years ago

@Fryie so you don't use fingerprinting? But just pre-pending URL? How you deal with client side assets caching?

Fryie commented 9 years ago

I do, I just excluded it from this example because it depends on a "isProductionLikeBuild" variable. edit: updated example

Fryie commented 9 years ago

Oh I see what you mean now. Yeah, with fingerprinting you'd also need the specific asset fingerprint, so that solution won't work. I totally missed that.

Fryie commented 9 years ago

you could specify a custom fingerprint hash: http://www.ember-cli.com/user-guide/#asset-compilation that could be the git commit, for example edit: updated my example above with that

seawatts commented 8 years ago

@wehlutyk I am doing the same thing with ember-cli-deploy. Did you find a solution?

wehlutyk commented 8 years ago

@seawatts unfortunately nothing implemented yet. I think the way to go is what's explained as option 2 in this comment, but I haven't had the time to implement it. My next free slot to do it is in a month or two (but I won't make any promises -- if you know how to do it feel free :) ).

(In the meantime, since I only have 4 dynamic url assets, whenever they change I build once, then copy those particular fingerprints by hand from assetMap.json into my app code, then build again.)

Fryie commented 8 years ago

There is some discussion over here. Unfortunately, it is not at all easy to come up with a working solution. In fact, we had to go through several iterations to have a real bug-free solution.

you'll want the

fingerprint: {
      ...
      generateAssetMap: true,
      fingerprintAssetMap: true
    },

option, so your asset map is generated (and uploaded), and so it's fingerprinted as well (otherwise a client might retrieve an outdated asset map from the cache!).

Then, in your code you can retrieve the asset map via assets/assetMap.json. Ember CLI will automatically replace this string with the correct URL to the fingerprinted asset map in the production build. Then follow a similar procedure as outlined here (ignore the ENV.APP.CDN_PATH part, this turned out not to work exactly because the asset map is cached by the cliented; instead just use the assets/assetMap.json string and let Ember CLI replace it with the correct URL).

devinrhode2 commented 7 years ago

Also encountered this issue. I have image names coded directly into a plain javascript file that provides some data for the ui (app/ui-data.js), but I dynamically create the full paths for each url in my ui-data object. Just hard coding the full asset paths, and removing my code dynamically creates them, fixes the issue for me.

The full path makes broccoli-asset-rev recognize them and rewrites them, apparently in any source file anywhere in the ember app.

RuslanZavacky commented 7 years ago

@devinrhode2 I wrote an article about fingerprinting - https://medium.com/@ruslanzavacky/ember-cli-fingerprinting-and-dynamic-assets-797a298d8dc6 and also created a relevant addon that helps to solve fingerprinting issues - https://github.com/RuslanZavacky/ember-cli-ifa. Its still not ideal, but at least its doing it from the "box".

carrotalan commented 6 years ago

Sounds like there is no real resolution to this still.

Dynamic URLs are a huge part of large, production systems. We can implement the workarounds, but I would suggest this must be on a high priority of fixes for your next release (please!)

RuslanZavacky commented 6 years ago

@carrotalan you can easily use an addon that I've provided. Works well and for large production systems :) And if it does not satisfy you, author of this repo will likely be happy to get contributions that will solve the issue that you've described :)