December 22, 2010

#32 Code Reading - The Barista Gem

Barista is a gem that helps us use CoffeeScripts in our Rails app. It will automatically compile our CoffeeScripts into JavaScripts. We'll explore some of the code behind this gem (version 0.7.0.pre3) to see how this is accomplished.

Initialization

When the Barista module is loaded by Rails, the Integration module is autoloaded . At the bottom, Integration.setup is called. This method will determine if Rails 3 is being used, and if it is, the rails3.rb file gets loaded.
lib/barista.rb, lib/barista/integration.rb
# barista.rb
module Barista
  ...
  autoload :Integration, 'barista/integration'
  ...
  Integration.setup
end
_______________________________________________________

# integration.rb
module Barista
  module Integration
    ...
    autoload :Rails3,  'barista/integration/rails3'
    ...
    def self.setup
      setup_rails   if defined?(Rails)
      ...
    end

    def self.setup_rails
      case Rails::VERSION::MAJOR
      when 3
        Rails3
      ...

Rails::Railtie

In the Rails3 module, there is a class that inherits from Rails::Railtie. "Railtie is the core of the Rails Framework and provides several hooks to extend Rails and/or modify the initialization process." By creating a class that inherits from Railtie in a gem, the gem can place its own initialization code within the Rails startup process. Here we see Barista is adding some middleware to the Rails middleware stack. For our Rails app, the Filter middleware is only added if our Rails environment is 'test' or 'development'.
lib/barista/integration/rails3.rb, lib/barista.rb
# rails3.rb
module Barista
  module Integration
    module Rails3    
      class Railtie < Rails::Railtie
      ...
        initializer 'barista.wrap_filter' do
          config.app_middleware.use Barista::Filter if Barista.add_filter?
          config.app_middleware.use Barista::Server::Proxy
        end
        ...
_______________________________________________________

# barista.rb
module Barista
  ...
  class << self
    ...
    has_boolean_options    :verbose, :bare, :add_filter, ...
    ...
    def default_for_add_filter
      local_env?
    end

    def local_env?
      %w(test development).include? Barista.env
    end

    def env
      @env ||= default_for_env
    end

    def default_for_env
      return Rails.env.to_s if defined?(Rails.env)
      ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
    end
    ...

Barista::Filter

The Filter middleware is simply going to call Barista.compile_all!. (Since this is middleware, this compilation step will occur before our Rails app is accessed. So in development mode, any changes in our CoffeeScripts will be available when we refresh our browser.)
lib/barista/filter.rb
module Barista
  class Filter
    ...
    def _call(env)
      Barista.debug 'Compiling all scripts for barista'
      Barista.compile_all!
      @app.call env
    end
  ...

Compiling The CoffeeScripts

In the Barista.compile_all! method, there are a couple of opportunities to insert some callbacks into the compilation process via Barista.invoke_hook. The Framework class is going to grab the CoffeeScripts while Compiler will do the actual compiling.
lib/barista.rb
module Barista
  class << self
    ...
    def compile_all!(force = false, silence_error = true)
      debug "Compiling all coffeescripts"
      Barista.invoke_hook :before_full_compilation
      Framework.exposed_coffeescripts.each do |coffeescript|
        Compiler.autocompile_file coffeescript, force, silence_error
      end
      Barista.invoke_hook :all_compiled
      true
    end
  ...

Barista::Framework

"One of the other main features Barista adds (over other tools) is frameworks similar to Compass. The idea being, you add coffee scripts at runtime from gems etc." For our purposes, our Rails app will be the 'default_framework' which provides the CoffeeScripts. By default, Barista will look for our CoffeeScripts in our app/coffeescripts directory.

With the root directory defined, an instance of Framework is created and our CoffeeScript files are collected.

lib/barista/framework.rb, lib/barista.rb
module Barista
  class Framework
    ...
    def self.exposed_coffeescripts
      all(true).inject([]) do |collection, fw|
        collection + fw.exposed_coffeescripts
      end.uniq.sort_by { |f| f.length }
    end

    def self.all(include_default = false)
      (@all ||= []).dup.tap do |all|
        all.unshift default_framework if include_default
      end
    end

    def self.default_framework
      @default_framework ||= self.new(:name => "default", :root => Barista.root)
    end
    ...
_______________________________________________________

# barista.rb
module Barista
  ...
  class << self
    ...
    def root
      @root ||= app_root.join("app", "coffeescripts")
    end

    def app_root
      @app_root ||= default_for_app_root
    end

    def default_for_app_root
      if defined?(Rails.root)
        Rails.root
      else
        ...
_______________________________________________________

# framework.rb
module Barista
  class Framework
    ...
    def initialize(options, root = nil, output_prefix = nil)
      ...
      @framework_root = File.expand_path(options[:root].to_s)
    end

    def exposed_coffeescripts
      coffeescripts.map { |script| short_name(script) }
    end

    def coffeescripts
      Dir[coffeescript_glob_path]
    end

    def coffeescript_glob_path
      @coffeescript_glob_path ||= File.join(@framework_root, "**", "*.coffee")
    end

    def short_name(script)
      short_name = remove_prefix script, @framework_root
      File.join(*[@output_prefix, short_name].compact)
    end
    ...

Barista::Compiler

With the CoffeeScripts collected, Compiler.autocompile_file will first check to see that a CoffeeScript compiler is available. The check is made through the Ruby coffee-script gem (see below). If a complier is present, Compiler.autocompile_file determines the full path for the CoffeeScript file ('origin_path') and the 'destination_path' for the JavaScript file. (The default destination path is 'public/javascripts' - code not shown.)

If a complied JavaScript file already exists and the original CoffeeScript file has not be changed, then this JavaScript file is returned. Otherwise a new instance of Compiler will be created and the CoffeeScript file will be compiled using CoffeeScript.compile(). Lastly, the JavaScript code is written to file with #save.

lib/barista/compiler.rb
# complier.rb
module Barista
  class Compiler
    class << self
    ...
      def autocompile_file(file, force = false, silence_error = false)
        if !check_availability!(silence_error)
          ...
          return nil
        end

        origin_path, framework = Framework.full_path_for(file)
        ...
        destination_path = framework.output_path_for(file)

        return File.read(destination_path) unless dirty?(origin_path, destination_path) || force

        Barista.debug "Compiling #{file} from framework '#{framework.name}'"
        compiler = new(origin_path, :silence_error => silence_error, :output_path => destination_path)
        content = compiler.to_js
        compiler.save
        content
      end
      ...
    end

    def initialize(context, options = {})
      ...
    end

    def to_js
      compile! unless ...
      @compiled_content
    end

    def compile!
      ...
      @compiled_content = compile(@context, location)
      ...
    end

    def compile(script, where = 'inline')
      Barista.invoke_hook :before_compilation, where
      out = CoffeeScript.compile script, :bare => Barista.bare?
      Barista.invoke_hook :compiled, where
      out
    rescue CoffeeScript::Error => e
      ...
    end

    def save(path = @options[:output_path])
      ...
      FileUtils.mkdir_p File.dirname(path)
      File.open(path, "w+") { |f| f.write @compiled_content }
      ...
    end
    ...
_______________________________________________________

# compiler.rb
module Barista
  class Compiler
    class << self
      ...
      def check_availability!(silence = false)
        available = available?        
        ...
      end

      def available?
        CoffeeScript.engine.present? && CoffeeScript.engine.supported?
      end

      def dirty?(from, to)
        File.exist?(from) && (!File.exist?(to) || File.mtime(to) < File.mtime(from))
      end
      ...

The Ruby CoffeeScript Gem

"Ruby CoffeeScript is a bridge to the official CoffeeScript compiler." "The coffee-script library will automatically choose the best JavaScript engine for your platform." If we look at the bottom of coffee_script.rb, we see how this is done.

The available engines are V8, Node.js, and JavaScriptCore. In order to set its engine, coffee-script will first call Engines::V8.supported? If the v8.rb file is not available, coffee-script will see if our system responds to the " which 'node'" command. If that fails, then it will look for JavaScriptCore's "jsc" file, which is on OSX by default. (If we have therubyracer gem installed on our system, then coffee-script will compile our scripts with V8.)

ruby-coffee-script/lib/coffee_script.rb
module CoffeeScript
  ...
  class << self
    def engine
      @engine ||= nil
    end

    def engine=(engine)
      @engine = engine
    end
    ...
    def compile(script, options = {})
      ...
      engine.compile(script, options)
    end
  end

  self.engine ||= [
    Engines::V8,
    Engines::Node,
    Engines::JavaScriptCore
  ].detect(&:supported?)
end

_______________________________________________________

module CoffeeScript
  ...
  module Engines
    module V8
      class << self
        def supported?
          require 'v8'
          true
        rescue LoadError
          false
        end

        def compile(script, options = {})
          coffee_module['compile'].call(script, Source.bare_option => options[:bare])
        rescue ::V8::JSError => e
          raise CompilationError, e.message
        end

        private
          def coffee_module
            @coffee_module ||= build_coffee_module
          end

          def build_coffee_module
            context = ::V8::Context.new
            context.eval(Source.contents)
            context['CoffeeScript']
          rescue ::V8::JSError => e
            raise EngineError, e.message
          end
      ...

    module Node
      class << self
        
        def binary
          @binary ||= 'node'
        end
        ...
        def supported?
          `which '#{binary}'`
          $?.success?
        end

        def compile(script, options = {})
          ...
    ...
    module JavaScriptCore
      BIN = "/System/Library/Frameworks/JavaScriptCore.framework/Versions/A/Resources/jsc"

      class << self
        def supported?
          File.exist?(BIN)
        end

        def compile(script, options = {})
          ...
    ...

    Comments

  1. I was just reading through this gem the other day. I wish it worked more like the integration compass has when generating stylesheets from scss... actually writing the javascript file into public/javascripts. I'm using this and compass on a new app, and it bothers me that the same concept (generating [stylesheets|javascript] from a proto-language) is handled two different ways.
    By David Bock Dec 23, 2010 15:39
  2. David, I'm not quite sure what you're referring to, but Barista does automatically compile CoffeeScripts into JavaScripts in development mode. The default is that you have CoffeeScripts in app/coffeescripts and the javascripts will be in public/javascripts (although these can be changed through configuration).
    By aRailsDemo Dec 26, 2010 16:02

(Please login to submit comments.)



View All Posts