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
# 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
# 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
module Barista class Filter ... def _call(env) Barista.debug 'Compiling all scripts for barista' Barista.compile_all! @app.call env end ...
Compiling The CoffeeScripts
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.
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.
# 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.)
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 = {}) ... ...
(Please login to submit comments.)
Comments