If you are migrating a Rails app to Dry-web-roda it is possible that you have your assets managed by sprockets. And, If you want to reduce the impact of the changes you will want to use sprockets in your new dry-web-roda app. Thankfully is really simple to do that.

The code was taken from my app called Focus then you want to replace it with the name of the yours.

These are the steps to make it work:

Add to your Gemfile the dependencies that you need

gem 'sprockets'
gem 'coffee-script' # only if you need to compile coffee-script
gem 'sass'          # for sass and scss files processing
gem 'uglifier'      # for js compression

Boot in your system the sprockets processor

Add the file apps/main/system/boot/sprockets.rb

Focus::Main::Container.boot :sprockets do |system|
  # "system" is the Main::Container here
  start do
    require 'sprockets'
    require 'slim'

    # system.root is "apps/main/"
    sprockets = Sprockets::Environment.new(system.root) do |env|
      env.logger = system['core.logger']
    end
    # Add the paths where the assets will be found
    sprockets.append_path(File.join(system.root, 'assets', 'javascripts'))
    sprockets.append_path(File.join(system.root, 'assets', 'stylesheets'))
    sprockets.append_path(File.join(system.root, 'assets', 'images'))

    # This will be used by the rake task that precompiles your assets
    if ENV['RACK_ENV'] == 'production'
      sprockets.js_compressor  = :uglify
      sprockets.css_compressor = :sass
    end

    # Here I added the path for assets loaded by bower
    project_root = system.root.parent.parent
    sprockets.append_path(
      File.join(project_root, 'vendor', 'assets', 'bower_components')
    )

    # This allows find the asset inside of your scss/sass file
    sprockets.context_class.class_eval do
      def asset_path(path, _options = {})
        path
      end
    end

    # I need this for slim templates used by my angular app
    sprockets.register_engine '.slim', Slim::Template, mime_type: 'text/slim',
      silence_deprecation: true

    # Register the configured sprockets into the dry-container
    register 'sprockets', sprockets
  end
end

Ask the context of the assets’ reference

Add to the view context an asset method that returns the asset’s reference. You can add this to the Main app or globally in the lib. In my case lib/focus/view/context.rb:

module Focus
  module View
    class Context
      ...
      def asset(name)
        if ENV['RACK_ENV'] == 'production'
          manifest["#{asset_prefix}__#{name}"]
        else
          "/#{asset_prefix}/assets/#{name}"
        end
      end

      private

      # In production the manifest file will map with the compiled
      # file name (idea stolen from https://github.com/icelab/berg)
      def manifest
        @manifest ||= YAML.load_file(
          "#{Focus::Container.config.root}/public/manifest.yml"
        )
      end

      def asset_prefix
        Inflecto.underscore self.class.to_s.split('::')[1]
      end
      ...
    end
  end
end

Then in your template you can use:

doctype html
html
  head
    link rel="stylesheet" href=asset("application.css")
    script rel="javascript" type="text/javascript" src=asset("application.js")
  body
    == yield

Serve the assets through sprockets

In development or test environment, delegate to sprockets the task of serving the assets.

module Focus
  module Main
    class Web < Dry::Web::Roda::Application
      route do |r|
        r.on 'main/assets' do
          r.run self.class['sprockets']
        end
        ...
      end
    end
  end
end

Moving to production

We need a rake task for precompile the assets in production. The following code has my customized needs for assets compilation (images in application assets path, bootstrap fonts from vendor file, and adding a digest to css and js files). But I’m sure that you will get the idea. Also, you can precompile your assets in development.

Then this are the changes added to my Rakefile for assets processing:

# Load sub-apps containers
app_paths = Pathname(__FILE__).dirname.join('./apps').realpath.join('*')
Dir[app_paths].each do |f|
  require "#{f}/system/focus/#{f.split('/').last}/container"
end

# These are the sub-apps that use sprockets
def sub_apps_containers
  [
    Focus::Main::Container,
    Focus::Spa::Container,
  ]
end

namespace :assets do
  desc 'compile assets'
  task :precompile do

    filenames = {}
    sub_apps_containers.each do |container|
      # Start the container to make sprockets accessible
      container.start :sprockets
      # Determine the sub-app directory (e.g.: 'main')
      sub_app_dir = container.config.default_namespace.split('.').last
      # Generate the destination directory
      outpath = File.join(Focus::Container.config.root, "public/#{sub_app_dir}/assets")
      FileUtils.mkdir_p outpath

      # obtain the images names
      assets = Dir[container.root.join('assets/images/*')].map do |img|
        img.split('/').last
      end
      # standard sprockets manifest
      assets << 'application.js'
      assets << 'application.css'

      # Bootstrap fonts, I'm including fonts in my sass file with:
      # $icon-font-path: "bootstrap-sass/assets/fonts/bootstrap/"
      # then I take the last 5 elements
      fonts = Dir[Focus::Container.root.join(
        'vendor/assets/bower_components/bootstrap-sass/assets/fonts/bootstrap/*'
      )].map do |font|
        font.split('/').last(5).join('/')
      end

      (assets + fonts).each do |filename|
        asset = container['sprockets'][filename]
        path = filename =~ /\.(js|css)$/ ? asset.digest_path : filename
        outfile = Pathname.new(outpath).join(path)

        asset.write_to(outfile)

        puts "successfully compiled #{filename} assets for #{container}"

        filenames["#{sub_app_dir}__#{filename}"] =
          "/public/#{sub_app_dir}/assets/#{asset.digest_path}"
      end
    end

    # Write the manifest file
    File.open('public/manifest.yml', 'w') do |file|
      file.write(YAML.dump(filenames))
    end
  end

  desc 'clean assets'
  task :clean do
    system('rm -rf public/*')
  end
end

Now you can precompile your assets with:

$ RACK_ENV=production rake assets:precompile

The last step use Rack::Static to serve compiled assets in production

Add to your config.ru

use Rack::Static, urls: ['/public']

require_relative 'system/boot'
run Focus::Web.freeze.app

And that’s all.