Closed yfeldblum closed 13 years ago
I would love to see this as well. Asset packaging on Heroku is a very tricky subject, as evidenced by the multitude of gems and plugins which have attempted to solve this problem. Jammit seems to be the best of the asset packagers, but the best way to get it to work cleanly with Heroku is still not very clear. Every article/discussion I've seen on the subject suggests precaching and committing to the repository, which just feels wrong to me, and everyone seems to hack together a solution in a different way.
What yfeldblum suggests here is ideal: creating the cached packages to /tmp
automatically on application startup (not relying on Jammit::Controller
), and documenation about how to link to those cached packages (which exist above the public document root) with a URL (either directly or via Jammit's helpers).
Another possible solution that occurred to me: Give Jammit a configuration option to package assets but not write them to disk. If my understanding is correct, the first time a URL like /assets/application.js?12345
is accessed, it's routed to Jammit::Controller
, which responds with the package and then writes it to disk for subsequent requests. If that second step was omitted, and far-future expiration headers were sent with the direct response, you could rely on Varnish to serve subsequent requests.
Varnish is cleared each time you deploy to Heroku, but if I'm understanding mtime-based cache busting correctly, the query string would change after a deployment anyway, so using Varnish wouldn't result in more downloading of unchanged content than the current mechanism does.
Forcing the browser to download the package again even if its contents has not changed is a separate issue altogether. Ideally Jammit would also support MD5-based cache busting strings which only change when the contents of the package change. That would probably create a problem with the helper methods, though, because in order to link to the asset file, you need the MD5 for the query string, and you couldn't get the MD5 of the package since the package doesn't exist on disk. I'm not sure how that would be solved.
Please correct me if I'm off base with any of this.
On Heroku, we would serve files in tmp/assets
with Rack::Static
or something else (e.g., with ETag support and stripping out the Last-Modified, because it might be OK to make one or two extra HTTP roundtrips to find out whether one or two whole package have changed, it's just not OK to make dozens of such roundtrips to find out whether dozens of individual scripts/styles have changed). We would set this up in a config/initializers/jammit.rb
, very simple, no need for a Jammit controller. All packages would be re-compiled to tmp/assets
, and all caches automatically cleared, on application every re-deploy in production on Heroku.
IMO this is all best practice anyway (read-only filesystem with a writable filesystem mounted at tmp
, no build artifacts in git, build at application re-deploy in production, enable ETag caching for packages, and set up a good http cache in front of your app).
But to make this happen, Jammit needs to able to be configured to write packages to tmp/assets
.
Cheers!
i'd love to see this happening!
Is there someone here that uses Heroku that wants to contribute a patch for this feature?
I saw that support for UglifyJS was merged. What is the status of therubyracer
support on Heroku? That still seems like a prerequisite to do this cleanly.
According to that ticket, the recent versions of the uglifier
gem have explicit Heroku support.
The uglifier
gem still doesn't work right on Heroku for me because it has .gemspec
dependencies on therubyracer
and therubyracer-heroku
.
This is what it does:
Gem::Specification.new do |s|
if Gem.available?("therubyracer-heroku")
s.add_runtime_dependency(%q<therubyracer-heroku>, ["~> 0.8.0"])
else
s.add_runtime_dependency(%q<therubyracer>, ["~> 0.8.0"])
end
end
Bad. I build my Gemfile
on a dev machine which doesn't have therubyracer-heroku
. So my Gemfile.lock
has a dependency on therubyracer
, which doesn't build on Heroku. And I don't want to have to add custom steps to setup a dev machine just for this odd .gemspec
which shouldn't be doing this anyway.
In that ticket, I recommended using the execjs
gem which knows how to find and load whatever JavaScript runtimes are installed, without itself having an explicit dependency on any of them. That would be ideal.
Cheers!
Ugh, that's a shame. Still, it seems like if you aren't using Bundler, you won't have any problems with uglifier
, so that's something at least.
If people who want this can +1 my uglifier
ticket, that would be great.
@yfeldblum: which ticket are you referring to?
A bit hacky way that seems to work atm; use therubyracer-heroku
locally:
gem "therubyracer-heroku", "0.8.1.pre3"
gem "jammit", :git => "git://github.com/documentcloud/jammit.git"
After bundle install delete therubyracer
gem references from the Gemfile.lock, commit changes and push to heroku.
@heikki
This way depends on the absence of therubyracer
on every developer's development box, and on all developers paying attention to this issue, or things start breaking. It's a neat workaround before a good fix is implemented (use execjs
) - but it's very hacky and depending on the absence of something is not a real solution.
It's best to put optional dependencies (or dependencies that may be satisfied by one of multiple gems) in the README
, not in the .gemspec
.
Ticket: https://github.com/lautis/uglifier/issues/closed#issue/1
Cheers!
Yes, its ugly as hell :). I needed a way to have recent Jammit haml.js support and be able to deploy to Heroku. Voted for the ticket.
I've merged @juggy's soft-dependencies branch, so neither uglifier
nor therubyracer
should be required now -- is there anything more that needs to be addressed/patched in this ticket?
50a85d0fc06be5d9f7a3484179ef1a14908b6ad8
Reading through this ticket again -- I think you should be able to get the desired behavior now by:
Uglifier was just updated fixing this issue. Thanks for your +1s!
Feature suggestion: An easy way to configure Jammit to write JS assets to
tmp/javascripts
and CSS assets totmp/stylesheets
.It's super easy to set up a
Rack::Static
middleware in Rails 3 that gets files fromtmp
. The cache in front of the Rails app would be responsible for caching and serving the assets. The obvious use case here is Heroku, and packaging assets on application startup after a new deploy.Note that this use case on Heroku would also require using
UglifyJS
. But with thetherubyracer-heroku
anduglifier
gems, that's not a problem on Heroku. You can aliascompress
tocompile
inUglifier
(or a subclass thereof) so that it's interface-compatible with what Jammit expects.Note that the key details of Heroku here are: read-only filesystem except for
tmp
which is mounted as a read-write filesystem, an http cache (Varnish) in front of every application configured automatically, and lack of a JVM. I consider the first two to be best practices (and the third an implementation detail), and I also think that packaging assets on application startup after a new deploy is a good technique for keeping build artifacts out of the repository.Cheers!