sprockets snippets

Sinatra App Template

Tagged sinatra, slim, pry, sprockets  Languages ruby

A quick and dirty template for Sinatra apps with many of the features you can find in Rails.

Supports: * Slim views * Sass stylesheets * Coffee script * Compass * Susy (susy.oddbird.net) * Asset pipeline (Sprockets) * View helpers * A console * I18n * Logging * ActiveRecord+migrations * Debugging with Pry * Flash messages * Class reloading in development mode * Security features such as CSRF, Cross Site Scripting, etc. All provided by Rack::Protection and Sinatra.

Create app.rb:

require 'slim'
require 'i18n'
require 'mysql2'
require 'sprockets'
require 'sprockets-sass'
require 'compass'
require 'susy'
require 'sinatra/base'
require 'sinatra/cookies'
require 'sinatra/content_for'
require 'sinatra/activerecord'
require 'sinatra/partial'
require 'rack-flash'

class App < Sinatra::Base
  enable :sessions, :logging
  register Sinatra::Partial

  use Rack::MethodOverride
  use Rack::Flash, :accessorize => [:info, :error, :success], :sweep => true
  use Rack::Protection::AuthenticityToken # HTML forms now require: input name="authenticity_token" value=session[:csrf] type="hidden"
  set :public_folder, File.dirname(__FILE__) + '/public'
  set :views, File.dirname(__FILE__) + '/app/views'
  set :slim, :layout_engine => :slim, :layout => :'layouts/default', :use_html_safe => true, :pretty => App.environment == :development
  set :partial_template_engine, :slim
  set :session_secret, "25729f31a6bc7c57f8575db9b79ee468...." # SecureRandom.hex(128)
  set :cookie_options, { path: '/'}

  def self.sprockets
    project_root = File.expand_path(File.dirname(__FILE__))
    assets = Sprockets::Environment.new(project_root)
    assets.append_path('app/js')
    assets.append_path('app/css')
    # Twitter Bootstrap...
    assets.append_path('lib/bootstrap/js')
    assets.append_path('lib/bootstrap/css')
    assets
  end

  helpers Sinatra::Cookies
  helpers Sinatra::ContentFor
  helpers do
    def t(*args)
      ::I18n::t(*args)
    end
    def authenticity_token
      session[:csrf] = SecureRandom.hex(128) unless session.has_key?(:csrf)
      %Q{<input type="hidden" name="authenticity_token" value="#{session[:csrf]}"/>}
    end
  end

  configure do
    # Configure logging, WTF
    set :logging, false
    class ::Logger; alias_method :write, :<<; end
    logfile = File.join(App.root, 'log', "#{App.environment}.log")
    # Send STDs to log file
    $stdout.reopen(logfile)
    $stderr.reopen(logfile)
    $stderr.sync = true
    $stdout.sync = true
    # Weekly roll
    log  = Logger.new(logfile, 'weekly')
    log.level = Logger::DEBUG
    # use Rack::CommonLogger, log
    set :log, log

    Compass.add_project_configuration(File.join(Sinatra::Application.root, 'config', 'compass.rb'))
  end

  configure :development do
    require "sinatra/reloader"
    register Sinatra::Reloader
    also_reload 'app/**/*.rb'
    also_reload 'lib/**/*.rb'
    also_reload 'conf/**/*.rb'
    set :raise_errors, true
  end

  [:error, :info, :success].each do |key|
    class_eval "
    def flash_#{key}(key, now=true)
      message(key, :#{key}, now)
    end
    "
  end

  def message(key, type=:notice, now=true)
    hash = now ? flash.now : flash
    hash[type] = I18n.t(key)
  end

  # Set view variables, e.g.:
  # - meta :title, "Page title"
  # Retrieve view variables, e.g.:
  # = meta :title
  def meta(key, value = nil)
    value ? content_for(key) { value } : yield_content(key)
  end

  # Helper method for creating HTML tags:
  # content_tag :a, 'Contact us', href: 'mailto:[email protected]'
  def content_tag(name, content, attributes = nil)
    name = html_escape(name) unless name.html_safe?
    content = html_escape(content) unless content.html_safe?
    attributes = attributes.map do |name, value|
      value = html_escape(value) unless value.html_safe?
      %Q{#{name}="#{value}"}
    end if attributes && attributes.any?
    start = [name, attributes.join(" ")].reject(&:nil?).join(' ')
    "<#{start}>#{content}</#{name}>"
  end

  def debug_something_with_pry
    Kernel.binding.pry
  end

  error do
    slim :'errors/500'
  end

  not_found do
    slim :'errors/404'
  end

  error ActiveRecord::RecordNotFound do
    slim :'errors/404'
  end

  before do
    I18n.locale = params[:locale] || I18n.default_locale
  end
end

# Require attr_accessible...
ActiveRecord::Base.send(:attr_accessible, nil)

# Move to config/init/db.rb if you like
OpenStruct.new(YAML::load(File.open('config/database.yml'))[App.environment.to_s].symbolize_keys).tap do |config|
  ActiveRecord::Base.establish_connection(
    host: config.host,
    adapter: config.adapter,
    database: config.database,
    username: config.username,
    password: config.password
  )
end

%w(models controllers concerns).each do |name|
  Dir[File.join('app', name, '**/*.rb')].each do |file|
    require_relative file
  end
end

# Move to config/init/i18n.rb if you like
Dir[File.join(App.root, 'config', 'locales', '*.yml')].each do |file|
  I18n.backend.load_translations(file)
end
I18n.default_locale = :en

# Move to app/controllers/root_controller.rb
class RootController < App
  get '/' do
    flash_alert "Thanks for nothing"
    slim :'index'
  end
end

Create Gemfile:

source :rubygems
gem 'thin'
gem 'slim'
gem 'sass'
gem 'sinatra'
gem 'sprockets'
gem 'sprockets-sass'
gem 'compass'
gem 'susy'

gem 'sinatra-partial'
gem 'coffee-script'
gem 'therubyracer'
group :development do
  gem 'sinatra-reloader'
  gem 'sinatra-activerecord'
  gem 'pry'
end
gem 'rack-flash3'
gem 'awesome_print'
gem 'mysql2'

Create Rakefile:

require 'bundler/setup'
require 'sinatra/activerecord/rake'
require 'pry'
require './app'

Dir[File.join('lib', 'tasks', '**', '*.rake')].each do |file|
  import file
end

task :console do
  binding.pry
end

# Asset pipeline (Sprockets)
namespace :assets do
  task :precompile do
    App.sprockets['application.js'].write_to('public/assets/application.js')
    App.sprockets['application.css'].write_to('public/assets/application.css')
  end
end

Create config/database.yml:

development:
  adapter: mysql2
  database: xxx_development
  username: root
  password: 
  host: localhost

Create config.ru:

require './app'
map "/" do
  run RootController
end

map "/assets" do
 run App.sprockets
end

Run:

mkdir -p app/controllers app/models app/concerns app/views/layouts app/css app/js lib tmp log public/img config/init db/migrate lib/tasks
bundle
bundle exec thin start -p 3000

Sprockets for Sinatra

Tagged sprockets, sinatra  Languages ruby

Features: * asset pipeline based on Sprockets * precompilation * versioning of assets

The "concern" that you need to include in your application:

#
# Asset pipeline for Sinatra based on Sprockets.
#
# NOTE: In production use rake assets:precompile to compile assets and generate an
# asset version file.
#
module AssetsConcern
  extend ActiveSupport::Concern

  included do
    set :assets_prefix, '/assets'
    set :assets_path, File.join(App.root, 'public', assets_prefix)
    set :assets_precompile, true
  end

  module ClassMethods
    #
    # Returns the current version.
    #
    def asset_version
      @@asset_version ||= read_asset_version
    end

    def read_asset_version
      file = 'public/assets/version'
      if File.exist?(file)
        File.read(file).to_i
      else
        nil
      end
    end

    #
    # Returns the path to the assets.
    #
    def asset_path(name, type)
      if App.asset_version
        file = "/assets/#{name}-#{App.asset_version}.#{type}"
      else
        file = "/assets/#{name}.#{type}"
      end
    end

    def sprockets
      require "yui/compressor"
      project_root = File.expand_path(File.dirname(__FILE__))
      assets = Sprockets::Environment.new(project_root)
      # Include js, css, and bower managed components
      %w(bower_components app/js app/css).each do |path|
        assets.append_path(File.join(App.root, path))
      end
      assets
    end
  end
end

The Rake task for compilication of assets:

namespace :assets do
  task :precompile do
    version = Time.now.to_i 
    App.sprockets['application.js'].write_to("public/assets/application-#{version}.js")
    App.sprockets['application.css'].write_to("public/assets/application-#{version}.css")
    File.open('public/assets/version', 'w') { |f| f << version }
    puts "Done... Version #{version}"
  end
end

The view template:

= css_tag(:application)
= js_tag(:application)

How to fix "if the extend should be able to fail" and "undefined variable" SCSS errors

Tagged extend, rails, scss, sprockets  Languages bash

If you get an error similar to one of these:

Use “@extend .navbar-inverse !optional” if the extend should be able to fail”:

Undefined variable: $some_variable

The solution might be the one explained here.

My issue was that I had the SCSS partials in the root folder like this:

❯ tree app/assets/stylesheets
app/assets/stylesheets
├── application.scss
├── _bootstrap-custom.scss
├── _bootstrap-overrides.scss
└── _theme.scss

The problem with this flat folder structure is that all the SCSS files, including partials, are compiled separately. This is the case with Rails’ Sprockets at least.

The solution for me was to move all partials into a subdirectory, like this:

❯ tree app/assets/stylesheets
app/assets/stylesheets
├── application.scss
├── partials
│   ├── _bootstrap-custom.scss
│   ├── _bootstrap-overrides.scss
│   ├── _theme.scss
└── vendor
    └── introjs.min.css

This way Sprockets, which compiles SCSS in Rails applications, will only compile the application.scss, which includes the partials.

References

https://github.com/rails/sprockets/blob/master/UPGRADING.md#manifestjs

You will have to edit the manifest.js to specify those files.

You may also find that some files that were not previously compiled as top-level targets now are. For instance, if your existing app has any js files directly at ./app/assets/javascripts or css/scss files ./app/assets/stylesheets, Rails with Sprockets 4 will now compile them as top-level targets. Since they were not previously treated as such, you probably don’t mean them to be; if they are .scss partials referencing variables meant to be defined in other files, it may even result in an error message that looks like Undefined variable: $some_variable.

To correct this, you can move these files to some subdirectory of ./app/assets/stylesheets or javascripts; or you can change the manifest.js to be more like how Rails with Sprockets 3 works, linking only the specific application files as top-level targets:

Rails log:

2019-12-19 15:52:49 +0200: Rack app error handling request { GET / }
#<ActionView::Template::Error: Error: "#nav" failed to @extend ".navbar-inverse".
       The selector ".navbar-inverse" was not found.
       Use "@extend .navbar-inverse !optional" if the extend should be able to fail.
        on line 64:11 of app/assets/stylesheets/_theme.scss
>>   @extend .navbar-inverse;

   ----------^
>
/Users/christian/projects/rails-app/app/assets/stylesheets/_theme.scss:64
/Users/christian/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sassc-2.2.1/lib/sassc/engine.rb:49:in `render'
/Users/christian/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sprockets-4.0.0/lib/sprockets/sassc_processor.rb:59:in `block in call'
/Users/christian/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sprockets-4.0.0/lib/sprockets/utils.rb:138:in `module_include'
/Users/christian/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sprockets-4.0.0/lib/sprockets/sassc_processor.rb:58:in `call'
/Users/christian/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sprockets-4.0.0/lib/sprockets/sassc_processor.rb:31:in `call'
/Users/christian/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sprockets-4.0.0/lib/sprockets/processor_utils.rb:84:in `call_processor'
/Users/christian/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sprockets-4.0.0/lib/sprockets/processor_utils.rb:66:in `block in call_processors'
/Users/christian/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sprockets-4.0.0/lib/sprockets/processor_utils.rb:65:in `reverse_each'
/Users/christian/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sprockets-4.0.0/lib/sprockets/processor_utils.rb:65:in `call_processors'
/Users/christian/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sprockets-4.0.0/lib/sprockets/processor_utils.rb:22:in `block in <class:CompositeProcessor>'
/Users/christian/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sprockets-4.0.0/lib/sprockets/processor_utils.rb:33:in `call'
/Users/christian/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sprockets-4.0.0/lib/sprockets/processor_utils.rb:84:in `call_processor'
/Users/christian/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sprockets-4.0.0/lib/sprockets/processor_utils.rb:66:in `block in call_processors'
/Users/christian/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sprockets-4.0.0/lib/sprockets/processor_utils.rb:65:in `reverse_each'
/Users/christian/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sprockets-4.0.0/lib/sprockets/processor_utils.rb:65:in `call_processors'