diff --git a/bin/middleman b/bin/middleman index b1ed6b89..e036e26e 100755 --- a/bin/middleman +++ b/bin/middleman @@ -1,30 +1,44 @@ #!/usr/bin/env ruby -require "rubygems" -require "pathname" - +# Add our lib/ directory to the path libdir = File.expand_path(File.join(File.dirname(File.dirname(__FILE__)), "lib")) $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir) -ARGV << "server" if ARGV.length < 1 +# Setup RubyGems +require "rubygems" +# Core Pathname library used for traversal +require "pathname" + +# Recursive method to find config.rb def locate_middleman_root!(cwd = Pathname.new(Dir.pwd)) return cwd.to_s if File.exists?(File.join(cwd, 'config.rb')) return false if cwd.root? locate_middleman_root!(cwd.parent) end +# Only look for config.rb if MM_ROOT isn't set if !ENV["MM_ROOT"] && found_path = locate_middleman_root! ENV["MM_ROOT"] = found_path end +# If we've found the root, try to setup Bundler if ENV["MM_ROOT"] + # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('Gemfile', ENV["MM_ROOT"]) require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) end +# Default command is server +ARGV << "server" if ARGV.length < 1 + +# Require Middleman require 'middleman' + +# Change directory to the root Dir.chdir(ENV["MM_ROOT"] || Dir.pwd) do + + # Start the CLI Middleman::Cli::Base.start end \ No newline at end of file diff --git a/lib/middleman/cli.rb b/lib/middleman/cli.rb index f5160ace..78450e36 100644 --- a/lib/middleman/cli.rb +++ b/lib/middleman/cli.rb @@ -1,16 +1,23 @@ +# Require thor since that's what the who CLI is built around require 'thor' require "thor/group" # CLI Module module Middleman::Cli + # The base task from which everything else etends class Base < Thor + desc "version", "Show version" def version require 'middleman/version' say "Middleman #{Middleman::VERSION}" end + # Override the Thor help method to find help for subtasks + # @param [Symbol, String, nil] meth + # @param [Boolean] subcommand + # @return [void] def help(meth = nil, subcommand = false) if meth && !self.respond_to?(meth) klass, task = Thor::Util.find_class_and_task_by_namespace("#{meth}:#{meth}") @@ -28,6 +35,8 @@ module Middleman::Cli end end + # Intercept missing methods and search subtasks for them + # @param [Symbol] meth def method_missing(meth, *args) meth = meth.to_s @@ -42,6 +51,7 @@ module Middleman::Cli end end +# Include the core CLI items require "middleman/cli/init" require "middleman/cli/server" require "middleman/cli/build" \ No newline at end of file diff --git a/lib/middleman/cli/build.rb b/lib/middleman/cli/build.rb index 07ab4aa2..dd648273 100644 --- a/lib/middleman/cli/build.rb +++ b/lib/middleman/cli/build.rb @@ -1,9 +1,14 @@ +# Use Rack::Test for inspecting a running server for output require "rack" require "rack/test" +# CLI Module module Middleman::Cli + + # The CLI Build class class Build < Thor include Thor::Actions + check_unknown_options! namespace :build @@ -24,6 +29,9 @@ module Middleman::Cli :aliases => "-g", :default => nil, :desc => 'Build a subset of the project' + + # Core build Thor command + # @return [void] def build if !ENV["MM_ROOT"] $stderr.puts "== Error: Could not find a Middleman project config, perhaps you are in the wrong folder?" @@ -40,22 +48,33 @@ module Middleman::Cli opts[:glob] = options["glob"] if options.has_key?("glob") opts[:clean] = options["clean"] if options.has_key?("clean") - action GlobAction.new(self, self.class.shared_instance, opts) + action GlobAction.new(self, opts) self.class.shared_instance.run_hook :after_build, self end + # Static methods class << self + + # Middleman::Base singleton + # + # @return [Middleman::Base] def shared_instance @_shared_instance ||= ::Middleman.server.inst do set :environment, :build end end - + + # Middleman::Base class singleton + # + # @return [Middleman::Base] def shared_server @_shared_server ||= shared_instance.class end - + + # Rack::Test::Session singleton + # + # @return [Rack::Test::Session] def shared_rack @_shared_rack ||= begin mock = ::Rack::MockSession.new(shared_server.to_rack_app) @@ -66,62 +85,64 @@ module Middleman::Cli end end + # Set the root path to the Middleman::Base's root source_root(shared_instance.root) - - # @private - module ThorActions - # Render a template to a file. - # @return [String] the actual destination file path that was created - def tilt_template(source, *args, &block) - config = args.last.is_a?(Hash) ? args.pop : {} - destination = args.first || source + # Render a template to a file. + # + # @param [String] source + # @param [String] destination + # @param [Hash] config + # @return [String] the actual destination file path that was created + desc "private method", :hide => true + def tilt_template(source, destination, config={}) + build_dir = self.class.shared_instance.build_dir + request_path = destination.sub(/^#{build_dir}/, "") + config[:force] = true - request_path = destination.sub(/^#{self.class.shared_instance.build_dir}/, "") + begin + destination, request_path = self.class.shared_instance.reroute_builder(destination, request_path) - begin - destination, request_path = self.class.shared_instance.reroute_builder(destination, request_path) + response = self.class.shared_rack.get(request_path.gsub(/\s/, "%20")) - response = self.class.shared_rack.get(request_path.gsub(/\s/, "%20")) + create_file(destination, response.body, config) - create_file(destination, response.body, config) - - destination - rescue - say_status :error, destination, :red - abort - end + destination + rescue + say_status :error, destination, :red + abort end end - - include ThorActions end - # @private + # A Thor Action, modular code, which does the majority of the work. class GlobAction < ::Thor::Actions::EmptyDirectory attr_reader :source - def initialize(base, app, config={}, &block) - @app = app + # Setup the action + # + # @param [Middleman::Cli::Build] base + # @param [Hash] config + def initialize(base, config={}) + @app = base.class.shared_instance source = @app.source @destination = @app.build_dir @source = File.expand_path(base.find_in_source_paths(source.to_s)) - super(base, destination, config) + super(base, @destination, config) end - + + # Execute the action + # @return [void] def invoke! queue_current_paths if cleaning? execute! clean! if cleaning? end - def revoke! - execute! - end - protected - + # Remove files which were not built in this cycle + # @return [void] def clean! files = @cleaning_queue.select { |q| File.file? q } directories = @cleaning_queue.select { |q| File.directory? q } @@ -137,14 +158,22 @@ module Middleman::Cli end end + # Whether we should clean the build + # @return [Boolean] def cleaning? @config.has_key?(:clean) && @config[:clean] end + # Whether the given directory is empty + # @param [String] directory + # @return [Boolean] def directory_empty?(directory) Dir[File.join(directory, "*")].empty? end + # Get a list of all the paths in the destination folder and save them + # for comparison against the files we build in this cycle + # @return [void] def queue_current_paths @cleaning_queue = [] Find.find(@destination) do |path| @@ -155,9 +184,15 @@ module Middleman::Cli end if File.exist?(@destination) end + # Actually build the app + # @return [void] def execute! + # Sort order, images, fonts, js/css and finally everything else. sort_order = %w(.png .jpeg .jpg .gif .bmp .svg .svgz .ico .woff .otf .ttf .eot .js .css) + # Sort paths to be built by the above order. This is primarily so Compass can + # find files in the build folder when it needs to generate sprites for the + # css files paths = @app.sitemap.all_paths.sort do |a, b| a_ext = File.extname(a) b_ext = File.extname(b) @@ -168,29 +203,27 @@ module Middleman::Cli a_idx <=> b_idx end + # Loop over all the paths and build them. paths.each do |path| file_source = path file_destination = File.join(given_destination, file_source.gsub(source, '.')) file_destination.gsub!('/./', '/') - if @app.sitemap.generic?(file_source) - # no-op - elsif @app.sitemap.proxied?(file_source) + if @app.sitemap.proxied?(file_source) file_source = @app.sitemap.page(file_source).proxied_to elsif @app.sitemap.ignored?(file_source) next end + + next if @config[:glob] && !File.fnmatch(@config[:glob], file_source) - if @config[:glob] - next unless File.fnmatch(@config[:glob], file_source) - end - - file_destination = base.tilt_template(file_source, file_destination, { :force => true }) + file_destination = base.tilt_template(file_source, file_destination) @cleaning_queue.delete(file_destination) if cleaning? end end end + # Alias "b" to "build" Base.map({ "b" => "build" }) end \ No newline at end of file diff --git a/lib/middleman/cli/init.rb b/lib/middleman/cli/init.rb index 1ba260d9..8bf1394a 100644 --- a/lib/middleman/cli/init.rb +++ b/lib/middleman/cli/init.rb @@ -1,4 +1,7 @@ +# CLI Module module Middleman::Cli + + # A thor task for creating new projects class Init < Thor check_unknown_options! @@ -6,7 +9,6 @@ module Middleman::Cli desc "init NAME [options]", "Create new project NAME" available_templates = ::Middleman::Templates.registered.keys.join(", ") - # argument :name method_option "template", :aliases => "-T", :default => "default", @@ -28,6 +30,8 @@ module Middleman::Cli :type => :boolean, :default => false, :desc => 'Create a Gemfile and use Bundler to manage gems' + # The init task + # @param [String] name def init(name) key = options[:template].to_sym unless ::Middleman::Templates.registered.has_key?(key) @@ -39,6 +43,7 @@ module Middleman::Cli end end + # Map "i", "new" and "n" to "init" Base.map({ "i" => "init", "new" => "init", diff --git a/lib/middleman/cli/server.rb b/lib/middleman/cli/server.rb index 9a0dd6ad..9d17f5a6 100644 --- a/lib/middleman/cli/server.rb +++ b/lib/middleman/cli/server.rb @@ -1,4 +1,7 @@ +# CLI Module module Middleman::Cli + + # Server thor task class Server < Thor check_unknown_options! @@ -22,6 +25,8 @@ module Middleman::Cli :type => :boolean, :default => false, :desc => 'Print debug messages' + + # Start the server def server if !ENV["MM_ROOT"] puts "== Warning: Could not find a Middleman project config.rb" @@ -42,5 +47,6 @@ module Middleman::Cli end end + # Map "s" to "server" Base.map({ "s" => "server" }) end \ No newline at end of file diff --git a/lib/middleman/guard.rb b/lib/middleman/guard.rb index cbaed0a3..04ef230e 100644 --- a/lib/middleman/guard.rb +++ b/lib/middleman/guard.rb @@ -8,7 +8,12 @@ require "net/http" # Support forking on Windows require "win32/process" if Middleman::WINDOWS +# The Guard namespace module Middleman::Guard + + # Start guard + # @param [Hash] options + # @return [void] def self.start(options={}) # Forward CLI options to Guard options_hash = options.map { |k,v| ", :#{k} => '#{v}'" }.join @@ -28,6 +33,7 @@ end # @private module Guard + # Monkeypatch Guard into being quiet module UI class << self @@ -37,17 +43,23 @@ module Guard # Guards must be in the Guard module to be picked up class Middleman < Guard + # Save the options for later def initialize(watchers = [], options = {}) super + + # Save options @options = options end # Start Middleman in a fork + # @return [void] def start @server_job = fork { bootup } end + # Start an instance of Middleman::Base + # @return [void] def bootup env = (@options[:environment] || "development").to_sym is_logging = @options.has_key?(:debug) && (@options[:debug] == "true") @@ -66,6 +78,7 @@ module Guard end # Stop the forked Middleman + # @return [void] def stop puts "== The Middleman is shutting down" Process.kill(::Middleman::WINDOWS ? :KILL : :TERM, @server_job) @@ -74,6 +87,7 @@ module Guard end # Simply stop, then start + # @return [void] def reload stop start @@ -81,6 +95,7 @@ module Guard # What to do on file change # @param [Array] paths Array of paths that changed + # @return [void] def run_on_change(paths) # See if the changed file is config.rb or lib/*.rb return reload if needs_to_reload?(paths) @@ -91,6 +106,7 @@ module Guard # What to do on file deletion # @param [Array] paths Array of paths that were removed + # @return [void] def run_on_deletion(paths) # See if the changed file is config.rb or lib/*.rb return reload if needs_to_reload?(paths) @@ -99,6 +115,8 @@ module Guard paths.each { |path| tell_server(:delete => path) } end + # What command is sent to kill instances + # @return [Symbol, Fixnum] def self.kill_command ::Middleman::WINDOWS ? 1 : :INT end @@ -115,6 +133,7 @@ module Guard # Send a message to the running server # @param [Hash] params Keys to be hashed and sent to server + # @return [void] def tell_server(params={}) uri = URI.parse("http://#{@options[:host]}:#{@options[:port]}/__middleman__") Net::HTTP.post_form(uri, {}.merge(params))