require 'tilt' require 'sprockets' module Tay ## # Takes a Tay::Specification and builds it. It compiles the assets, # writes the manifest, and copies everything to the output path. class Builder ## # Pointer to the relevant Tay::Specification attr_reader :spec ## # Set to true for debug output attr_accessor :debug ## # Create a new builder. You must pass the specification, full path to the # source directory and an optional output directory which defaults to # base_dir + '/build' def initialize(specification, base_dir, output_dir = nil) @spec = specification @base_dir = Pathname.new(base_dir) @output_dir = output_dir ? Pathname.new(output_dir) : @base_dir.join('build') create_sprockets_environment end ## # Do the building. This simply delegates to the private methods # in this class. def build! create_output_directory spec.source_directories.each { |d| simple_compile_directory(d) } compile_files(spec.all_javascript_paths) compile_files(spec.all_stylesheet_paths) write_manifest end ## # This is sub-optimal but the mustache/js gem does not require its tilt # template automatically. def self.try_to_load_mustache_trimmer begin require 'tilt/mustache_js_template' Sprockets.register_engine '.mustache', Tilt::MustacheJsTemplate rescue LoadError end end protected ## # Given a path, run it through tilt and return the compiled version. # If there's no known engine for it, just return the content verbatim. # If we know the type buy are missing the gem, raise an exception. def get_compiled_file_content(path) begin Tilt.new(path.to_s).render({}, { :spec => spec }) rescue RuntimeError File.read(path) end end ## # Create the output directory if it does not exist def create_output_directory FileUtils.mkdir_p @output_dir end ## # Copy all the files from a directory to the output, compiling # them if they are familiar to us. Does not do any sprocketing. def simple_compile_directory(directory) if directory.is_a?(String) # If we just have a single dirname, assume it's under src from_directory = (directory[/\//] ? '' : 'src/') + directory directory = { :from => from_directory, :as => directory } end directory[:use_tilt] |= true Dir[@base_dir.join(directory[:from], '**/*')].each do |path| file_in_path = Pathname.new(path) next unless file_in_path.file? file_out_path = remap_path_to_build_directory(path, directory) if directory[:use_tilt] content = get_compiled_file_content(file_in_path) file_out_path = asset_output_filename(file_out_path, Tilt.mappings.keys) else content = File.read(file_in_path) end FileUtils.mkdir_p(file_out_path.dirname) File.open(file_out_path, 'w') do |f| f.write content end end end ## # Process all the files in the directory through sprockets before writing # them to the output directory def compile_files(files) files.each do |base_path| # We do this second glob in case the path provided in the tayfile # references a compiled version Dir[@base_dir.join('src', base_path + '*')].each do |path| path = Pathname.new(path).relative_path_from(@base_dir.join('src')) file_in_path = @base_dir.join('src', path) file_out_path = asset_output_filename(@output_dir.join(path), @sprockets.engines.keys) if @sprockets.extensions.include?(path.extname) content = @sprockets[file_in_path].to_s else content = File.read(file_in_path) end FileUtils.mkdir_p(file_out_path.dirname) File.open(file_out_path, 'w') do |f| f.write content end end end end ## # Generate the manifest from the spec and write it to disk def write_manifest generator = ManifestGenerator.new(spec) File.open(@output_dir.join('manifest.json'), 'w') do |f| f.write JSON.pretty_generate(generator.spec_as_json) end end ## # Set up the sprockets environment for munging all the things def create_sprockets_environment @sprockets = Sprockets::Environment.new @sprockets.append_path(@base_dir.join('src/javascripts').to_s) @sprockets.append_path(@base_dir.join('src/templates').to_s) @sprockets.append_path(@base_dir.join('src/stylesheets').to_s) @sprockets.append_path(@base_dir.join('src').to_s) @sprockets.append_path(@base_dir.to_s) end ## # Debug message helper def dbg(msg) puts dbg if debug end ## # Helper function that converts a source path to an equivalent path in the # output directory def remap_path_to_build_directory(path, directory_info) base_directory = @base_dir.join(directory_info[:from]).to_s @output_dir.join(path.to_s.sub(/\A#{base_directory}\/?/, directory_info[:as] + '/')) end ## # Helper function to convert the filenames of assets requiring pre- # processing to their compiled extension. However, if the file only # has one extension, it will be left alone regardless. Examples: # # * "foobar.module.js.coffee" => "foobar.module.js" # * "index.html.haml" => "index.html" # * "style.scss" => "style.scss" def asset_output_filename(path, processed_extensions) path = Pathname.new(path) if path.is_a?(String) return path if path.basename.to_s.split('.').length == 2 extension = path.extname processed_extensions.map! { |ext| (ext[0] != '.' ? '.' : '') + ext } if processed_extensions.include?(extension) asset_output_filename(path.to_s.sub(/#{extension}\Z/, ''), processed_extensions) else path end end end Builder.try_to_load_mustache_trimmer end