module Roda::RodaPlugins::Assets

  1. lib/roda/plugins/assets.rb

The assets plugin adds support for rendering your CSS and javascript asset files on the fly in development, and compiling them to a single, compressed file in production.

This uses the render plugin for rendering the assets, and the render plugin uses tilt internally, so you can use any template engine supported by tilt for your assets. Tilt ships with support for the following asset template engines, assuming the necessary libraries are installed:

css

Less, Sass, Scss

js

CoffeeScript

You can also use opal as a javascript template engine, assuming it is installed.

Usage

When loading the plugin, use the :css and :js options to set the source file(s) to use for CSS and javascript assets:

plugin :assets, css: 'some_file.scss', js: 'some_file.coffee'

This will look for the following files:

assets/css/some_file.scss
assets/js/some_file.coffee

The values for the :css and :js options can be arrays to load multiple files. If you want to change the paths where asset files are stored, see the Options section below.

Serving

In your routes, call the r.assets method to add a route to your assets, which will make your app serve the rendered assets:

route do |r|
  r.assets
end

You should generally call r.assets inside the route block itself, and not under any branches of the routing tree.

Views

In your layout view, use the assets method to add links to your CSS and javascript assets:

<%= assets(:css) %>
<%= assets(:js) %>

You can add attributes to the tags by using an options hash:

<%= assets(:css, media: 'print') %>

The assets method will respect the application’s :add_script_name option, if it set it will automatically prefix the path with the SCRIPT_NAME for the request.

Asset Paths

If you just want the paths rather than the full tags, you can use assets_paths instead. This will return an array of the sources that the assets function would have put into tags:

assets_paths(:css)
# => ["/assets/css/foo.css", "/assets/css/app.css"]

If compilation is turned on, it will return the path to the compiled asset:

assets_paths(:css)
# => ["/assets/app.5e7b06baa1a514d8473b0eca514b806c201073b9.css"]

Asset Groups

The asset plugin supports groups for the cases where you have different css/js files for your front end and back end. To use asset groups, you pass a hash for the :css and/or :js options:

plugin :assets, css: {frontend: 'some_frontend_file.scss',
                      backend: 'some_backend_file.scss'}

This expects the following directory structure for your assets:

assets/css/frontend/some_frontend_file.scss
assets/css/backend/some_backend_file.scss

If you do not want to force that directory structure when using asset groups, you can use the group_subdirs: false option.

In your view code use an array argument in your call to assets:

<%= assets([:css, :frontend]) %>

Nesting

Asset groups also support nesting, though that should only be needed in fairly large applications. You can use a nested hash when loading the plugin:

plugin :assets,
  css: {frontend: {dashboard: 'some_frontend_file.scss'}}

and an extra entry per nesting level when creating the tags.

<%= assets([:css, :frontend, :dashboard]) %>

Caching

The assets plugin uses the caching plugin internally, and will set the Last-Modified header to the modified timestamp of the asset source file when rendering the asset.

If you have assets that include other asset files, such as using @import in a sass file, you need to specify the dependencies for your assets so that the assets plugin will correctly pick up changes. You can do this using the :dependencies option to the plugin, which takes a hash where the keys are paths to asset files, and values are arrays of paths to dependencies of those asset files:

app.plugin :assets,
  dependencies: {'assets/css/bootstrap.scss'=>Dir['assets/css/bootstrap/' '**/*.scss']}

Asset Compilation

In production, you are generally going to want to compile your assets into a single file, with you can do by calling compile_assets after loading the plugin:

plugin :assets, css: 'some_file.scss', js: 'some_file.coffee'
compile_assets

After calling compile_assets, calls to assets in your views will default to a using a single link each to your CSS and javascript compiled asset files. By default the compiled files are written to the public directory, so that they can be served by the webserver.

Asset Compression

If you have the yuicompressor gem installed and working, it will be used automatically to compress your javascript and css assets. For javascript assets, if yuicompressor is not available, the plugin will check for closure-compiler, uglifier, and minjs and use the first one that works. If no compressors are available, the assets will just be concatenated together and not compressed during compilation. You can use the :css_compressor and :js_compressor options to specify the compressor to use.

It is also possible to use the built-in compression options in the CSS or JS compiler, assuming the compiler supports such options. For example, with sass/sassc, you can use:

plugin :assets,
  css_opts: {style: :compressed}

Source Maps (CSS)

The assets plugin does not have direct support for source maps, so it is recommended you use embedded source maps if supported by the CSS compiler. For sass/sassc, you can use:

plugin :assets,
  css_opts: {:source_map_embed=>true, source_map_contents: true, source_map_file: "."}

With Asset Groups

When using asset groups, a separate compiled file will be produced per asset group.

Unique Asset Names

When compiling assets, a unique name is given to each asset file, using the a SHA1 hash of the content of the file. This is done so that clients do not attempt to use cached versions of the assets if the asset has changed.

Serving

When compiling assets, r.assets will serve the compiled asset files. However, it is recommended to have the main webserver (e.g. nginx) serve the compiled files, instead of relying on the application.

Assuming you are using compiled assets in production mode that are served by the webserver, you can remove the serving of them by the application:

route do |r|
  r.assets unless ENV['RACK_ENV'] == 'production'
end

If you do have the application serve the compiled assets, it will use the Last-Modified header to make sure that clients do not redownload compiled assets that haven’t changed.

Asset Precompilation

If you want to precompile your assets, so they do not need to be compiled every time you boot the application, you can provide a :precompiled option when loading the plugin. The value of this option should be the filename where the compiled asset metadata is stored.

If the compiled asset metadata file does not exist when the assets plugin is loaded, the plugin will run in non-compiled mode. However, when you call compile_assets, it will write the compiled asset metadata file after compiling the assets.

If the compiled asset metadata file already exists when the assets plugin is loaded, the plugin will read the file to get the compiled asset metadata, and it will run in compiled mode, assuming that the compiled asset files already exist.

On Heroku

Heroku supports precompiling the assets when using Roda. You just need to add an assets:precompile task, similar to this:

namespace :assets do
  desc "Precompile the assets"
  task :precompile do
    require './app'
    App.compile_assets
  end
end

Postprocessing

If you pass a callable object to the :postprocessor option, it will be called before an asset is served. If the assets are to be compiled, the object will be called at compilation time.

It is passed three arguments; the name of the asset file, the type of the asset file (which is a symbol, either :css or :js), and the asset contents.

It should return the new content for the asset.

You can use this to call Autoprefixer on your CSS:

plugin :assets, {
  css: [ 'style.scss' ],
  postprocessor: lambda do |file, type, content|
    type == :css ? AutoprefixerRails.process(content).css : content
  end
}

External Assets/Assets from Gems

The assets plugin only supports loading assets files underneath the assets path. You cannot pass an absolute path to an asset file and have it work. If you would like to reference asset files that are outside the assets path, you have the following options:

  • Copy, hard link, or symlink the external assets files into the assets path.

  • Use tilt-indirect or another method of indirection (such as an erb template that loads the external asset file) so that a file inside the assets path can reference files outside the assets path.

Plugin Options

:add_suffix

Whether to append a .css or .js extension to asset routes in non-compiled mode (default: false)

:compiled_asset_host

The asset host to use for compiled assets. Should include the protocol as well as the host (e.g. “cdn.example.com”, “//cdn.example.com”)

:compiled_css_dir

Directory name in which to store the compiled css file, inside :compiled_path (default: nil)

:compiled_css_route

Route under :prefix for compiled css assets (default: :compiled_css_dir)

:compiled_js_dir

Directory name in which to store the compiled javascript file, inside :compiled_path (default: nil)

:compiled_js_route

Route under :prefix for compiled javscript assets (default: :compiled_js_dir)

:compiled_name

Compiled file name prefix (default: ‘app’)

:compiled_path

Path inside public folder in which compiled files are stored (default: :prefix)

:concat_only

Whether to just concatenate instead of concatenating and compressing files (default: false)

:css_compressor

Compressor to use for compressing CSS, either :yui, :none, or nil (the default, which will try :yui if available, but not fail if it is not available)

:css_dir

Directory name containing your css source, inside :path (default: ‘css’)

:css_headers

A hash of additional headers for your rendered css files

:css_opts

Template options to pass to the render plugin (via :template_opts) when rendering css assets

:css_route

Route under :prefix for css assets (default: :css_dir)

:dependencies

A hash of dependencies for your asset files. Keys should be paths to asset files, values should be arrays of paths your asset files depends on. This is used to detect changes in your asset files.

:early_hints

Automatically send early hints for all assets. Requires the early_hints plugin.

:group_subdirs

Whether a hash used in :css and :js options requires the assets for the related group are contained in a subdirectory with the same name (default: true)

:gzip

Store gzipped compiled assets files, and serve those to clients who accept gzip encoding.

:headers

A hash of additional headers for both js and css rendered files

:js_compressor

Compressor to use for compressing javascript, either :yui, :closure, :uglifier, :minjs, :none, or nil (the default, which will try :yui, :closure, :uglifier, then :minjs, but not fail if any of them is not available)

:js_dir

Directory name containing your javascript source, inside :path (default: ‘js’)

:js_headers

A hash of additional headers for your rendered javascript files

:js_opts

Template options to pass to the render plugin (via :template_opts) when rendering javascript assets

:js_route

Route under :prefix for javascript assets (default: :js_dir)

:path

Path to your asset source directory (default: ‘assets’). Relative paths will be considered relative to the application’s :root option.

:postprocessor

A block which should accept three arguments (asset name, asset type, content). This block can be used to hook into the asset system and make your own modifications before the asset is served. If the asset is to be compiled, the block is called at compile time.

:prefix

Prefix for assets path in your URL/routes (default: ‘assets’)

:precompiled

Path to the compiled asset metadata file. If the file exists, will use compiled mode using the metadata in the file. If the file does not exist, will use non-compiled mode, but will write the metadata to the file if compile_assets is called.

:public

Path to your public folder, in which compiled files are placed (default: ‘public’). Relative paths will be considered relative to the application’s :root option.

:relative_paths

Use relative paths instead of absolute paths when setting up link and script tags for assets.

:sri

Enables subresource integrity when setting up references to compiled assets. The value should be :sha256, :sha384, or :sha512 depending on which hash algorithm you want to use. This changes the hash algorithm that Roda will use when naming compiled asset files. The default is :sha256, you can use nil to disable subresource integrity.

:timestamp_paths

Include the timestamp of assets in asset paths in non-compiled mode. Doing this can slow down development requests due to additional requests to get last modified times, but it will make sure the paths change in development when there are modifications, which can fix issues when using a caching proxy in non-compiled mode. This can also be specified as a string to use that string to separate the timestamp from the asset. By default, / is used as the separator if timestamp paths are enabled.

Methods

Public Class

  1. configure
  2. load_dependencies

Constants

CompressorNotFound = Class.new(RodaError)  

Internal exception raised when a compressor cannot be found

DEFAULTS = { :compiled_name => 'app'.freeze, :js_dir => 'js'.freeze, :css_dir => 'css'.freeze, :prefix => 'assets'.freeze, :concat_only => false, :compiled => false, :add_suffix => false, :early_hints => false, :timestamp_paths => false, :group_subdirs => true, :compiled_css_dir => nil, :compiled_js_dir => nil, :sri => :sha256 }.freeze  

Public Class methods

configure(app, opts = {})

Setup the options for the plugin. See the Assets module RDoc for a description of the supported options.

[show source]
    # File lib/roda/plugins/assets.rb
356 def self.configure(app, opts = {})
357   if app.assets_opts
358     prev_opts = app.assets_opts[:orig_opts]
359     orig_opts = app.assets_opts[:orig_opts].merge(opts)
360     [:headers, :css_headers, :js_headers, :css_opts, :js_opts, :dependencies].each do |s|
361       if prev_opts[s]
362         if opts[s]
363           orig_opts[s] = prev_opts[s].merge(opts[s])
364         else
365           orig_opts[s] = prev_opts[s].dup
366         end
367       end
368     end
369     app.opts[:assets] = orig_opts.dup
370     app.opts[:assets][:orig_opts] = orig_opts
371   else
372     app.opts[:assets] = opts.dup
373     app.opts[:assets][:orig_opts] = opts
374   end
375   opts = app.opts[:assets]
376   opts[:path] = app.expand_path(opts[:path]||"assets").freeze
377   opts[:public] = app.expand_path(opts[:public]||"public").freeze
378 
379   # Combine multiple values into a path, ignoring trailing slashes
380   j = lambda do |*v|
381     opts.values_at(*v).
382       reject{|s| s.to_s.empty?}.
383       map{|s| s.chomp('/')}.
384       join('/').freeze
385   end
386 
387   # Same as j, but add a trailing slash if not empty
388   sj = lambda do |*v|
389     s = j.call(*v)
390     s.empty? ? s : (s + '/').freeze
391   end
392 
393   if opts[:precompiled] && !opts[:compiled] && ::File.exist?(opts[:precompiled])
394     require 'json'
395     opts[:compiled] = app.send(:_precompiled_asset_metadata, opts[:precompiled])
396   end
397 
398   if opts[:early_hints]
399     app.plugin :early_hints
400   end
401 
402   if opts[:timestamp_paths] && !opts[:timestamp_paths].is_a?(String)
403     opts[:timestamp_paths] = '/'
404   end
405 
406   DEFAULTS.each do |k, v|
407     opts[k] = v unless opts.has_key?(k)
408   end
409 
410   [
411    [:compiled_path, :prefix],
412    [:js_route, :js_dir],
413    [:css_route, :css_dir],
414    [:compiled_js_route, :compiled_js_dir],
415    [:compiled_css_route, :compiled_css_dir]
416   ].each do |k, v|
417     opts[k]  = opts[v] unless opts.has_key?(k)
418   end
419 
420   [:css_headers, :js_headers, :css_opts, :js_opts, :dependencies].each do |s|
421     opts[s] ||= {} 
422   end
423 
424   expanded_deps = opts[:expanded_dependencies] = {}
425   opts[:dependencies].each do |file, deps|
426     expanded_deps[File.expand_path(file)] = Array(deps)
427   end
428 
429   if headers = opts[:headers]
430     opts[:css_headers] = headers.merge(opts[:css_headers])
431     opts[:js_headers]  = headers.merge(opts[:js_headers])
432   end
433   opts[:css_headers][RodaResponseHeaders::CONTENT_TYPE] ||= "text/css; charset=UTF-8".freeze
434   opts[:js_headers][RodaResponseHeaders::CONTENT_TYPE]  ||= "application/javascript; charset=UTF-8".freeze
435 
436   [:css_headers, :js_headers, :css_opts, :js_opts, :dependencies, :expanded_dependencies].each do |s|
437     opts[s].freeze
438   end
439   [:headers, :css, :js].each do |s|
440     opts[s].freeze if opts[s]
441   end
442 
443   # Used for reading/writing files
444   opts[:js_path]           = sj.call(:path, :js_dir)
445   opts[:css_path]          = sj.call(:path, :css_dir)
446   opts[:compiled_js_path]  = j.call(:public, :compiled_path, :compiled_js_dir, :compiled_name)
447   opts[:compiled_css_path] = j.call(:public, :compiled_path, :compiled_css_dir, :compiled_name)
448 
449   # Used for URLs/routes
450   opts[:js_prefix]           = sj.call(:prefix, :js_route)
451   opts[:css_prefix]          = sj.call(:prefix, :css_route)
452   opts[:compiled_js_prefix]  = j.call(:prefix, :compiled_js_route, :compiled_name)
453   opts[:compiled_css_prefix] = j.call(:prefix, :compiled_css_route, :compiled_name)
454   opts[:js_suffix]           = (opts[:add_suffix] ? '.js' : '').freeze
455   opts[:css_suffix]          = (opts[:add_suffix] ? '.css' : '').freeze
456 
457   opts.freeze
458 end
load_dependencies(app, opts = OPTS)

Load the render, caching, and h plugins, since the assets plugin depends on them.

[show source]
    # File lib/roda/plugins/assets.rb
344 def self.load_dependencies(app, opts = OPTS)
345   app.plugin :render
346   app.plugin :caching
347   app.plugin :h
348 
349   if opts[:relative_paths]
350     app.plugin :relative_path
351   end
352 end