From 9ae8a3128b47eebf44dd21f5f032940730e40f6c Mon Sep 17 00:00:00 2001 From: Thomas Reynolds Date: Tue, 25 Mar 2014 16:54:16 -0700 Subject: [PATCH] Refactor FileWatcher --- .rubocop.yml | 2 + CHANGELOG.md | 1 + middleman-cli/lib/middleman-cli/build.rb | 2 +- .../middleman-core/core_extensions/data.rb | 11 ++- .../core_extensions/file_watcher.rb | 82 +++++++++++++------ .../core_extensions/front_matter.rb | 6 +- .../middleman-core/core_extensions/i18n.rb | 17 ++-- .../lib/middleman-core/extension.rb | 5 +- .../lib/middleman-core/renderers/sass.rb | 4 + .../sitemap/extensions/on_disk.rb | 4 +- .../step_definitions/middleman_steps.rb | 4 +- middleman-core/lib/middleman-core/util.rb | 8 +- 12 files changed, 98 insertions(+), 48 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 100c188c..671d371d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -15,6 +15,8 @@ AllCops: - 'middleman-core/fixtures/**/*' - 'middleman-core/features/**/*' - 'middleman-core/spec/**/*' +DoubleNegation: + Enabled: false LineLength: Enabled: false MethodLength: diff --git a/CHANGELOG.md b/CHANGELOG.md index 22e2750e..8de8854c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ master === +* New FileWatcher API. * Remove the `partials_dir` setting. Partials should live next to content, or be addressed with absolute paths. * Partials must be named with a leading underscore. `_my_snippet.html.erb`, not `my_snippet.html.erb`. * Removed the `proxy` and `ignore` options for the `page` command in `config.rb`. Use the `proxy` and `ignore` commands instead of passing these options to `page`. diff --git a/middleman-cli/lib/middleman-cli/build.rb b/middleman-cli/lib/middleman-cli/build.rb index 87b8ea35..3918319d 100644 --- a/middleman-cli/lib/middleman-cli/build.rb +++ b/middleman-cli/lib/middleman-cli/build.rb @@ -194,7 +194,7 @@ module Middleman::Cli logger.debug '== Checking for generated images' # Double-check for generated images - @app.extensions[:file_watcher].api.find_new_files((@source_dir + @app.config[:images_dir]).relative_path_from(@app.root_path)) + @app.files.find_new_files((@source_dir + @app.config[:images_dir]).relative_path_from(@app.root_path)) @app.sitemap.ensure_resource_list_updated! # Sort paths to be built by the above order. This is primarily so Compass can diff --git a/middleman-core/lib/middleman-core/core_extensions/data.rb b/middleman-core/lib/middleman-core/core_extensions/data.rb index 02af521a..95fabdcd 100644 --- a/middleman-core/lib/middleman-core/core_extensions/data.rb +++ b/middleman-core/lib/middleman-core/core_extensions/data.rb @@ -20,10 +20,15 @@ module Middleman # Setup data files before anything else so they are available when # parsing config.rb - file_watcher.changed(data_file_matcher, &app.extensions[:data].data_store.method(:touch_file)) - file_watcher.deleted(data_file_matcher, &app.extensions[:data].data_store.method(:remove_file)) + app.files.changed(data_file_matcher, &app.extensions[:data].data_store.method(:touch_file)) + app.files.deleted(data_file_matcher, &app.extensions[:data].data_store.method(:remove_file)) - file_watcher.reload_path(app.config[:data_dir]) + # Tell the file watcher to observe the :data_dir + app.files.watch :data do |path, _app| + path.match data_file_matcher + end + + app.files.reload_path(app.config[:data_dir]) end helpers do diff --git a/middleman-core/lib/middleman-core/core_extensions/file_watcher.rb b/middleman-core/lib/middleman-core/core_extensions/file_watcher.rb index fca753ab..c8be0d89 100644 --- a/middleman-core/lib/middleman-core/core_extensions/file_watcher.rb +++ b/middleman-core/lib/middleman-core/core_extensions/file_watcher.rb @@ -6,38 +6,24 @@ module Middleman module CoreExtensions # API for watching file change events class FileWatcher < Extension - # Regexes in this list will filter out filenames of files that shouldn't cause file change notifications. - IGNORE_LIST = [ - /^bin(\/|$)/, - /^\.bundle(\/|$)/, - /^vendor(\/|$)/, - /^node_modules(\/|$)/, - /^\.sass-cache(\/|$)/, - /^\.cache(\/|$)/, - /^\.git(\/|$)/, - /^\.gitignore$/, - /\.DS_Store/, - /^\.rbenv-.*$/, - /^Gemfile$/, - /^Gemfile\.lock$/, - /~$/, - /(^|\/)\.?#/, - /^tmp\// - ] - attr_reader :api - # Before parsing config, load the data/ directory - def before_configuration - app.config.define_setting :file_watcher_ignore, IGNORE_LIST, 'Regexes for paths that should be ignored when they change.' + def initialize(app, config={}, &block) + super + end + # Before parsing config, load the data/ directory + Contract None => Any + def before_configuration @api = API.new(app) + app.add_to_instance :files, &method(:api) app.add_to_config_context :files, &method(:api) end + Contract None => Any def after_configuration - app.config[:file_watcher_ignore] << %r{^#{app.config[:build_dir]}(\/|$)} @api.reload_path('.') + @api.is_ready = true end # Core File Change API class @@ -47,6 +33,7 @@ module Middleman attr_reader :app attr_reader :known_paths + attr_accessor :is_ready def_delegator :@app, :logger @@ -54,11 +41,40 @@ module Middleman def initialize(app) @app = app @known_paths = Set.new + @is_ready = false + + @watchers = { + source: proc { |path, _| path.match(/^#{app.config[:source]}\//) }, + library: /^(lib|helpers)\/.*\.rb$/ + } + + @ignores = { + emacs_files: /(^|\/)\.?#/, + tilde_files: /~$/, + ds_store: /\.DS_Store\//, + git: /(^|\/)\.git(ignore|modules|\/)/ + } @on_change_callbacks = Set.new @on_delete_callbacks = Set.new end + # Add a proc to watch paths + Contract Symbol, Or[Regexp, Proc] => Any + def watch(name, regex=nil, &block) + @watchers[name] = block_given? ? block : regex + + reload_path('.') if @is_ready + end + + # Add a proc to ignore paths + Contract Symbol, Or[Regexp, Proc] => Any + def ignore(name, regex=nil, &block) + @ignores[name] = block_given? ? block : regex + + reload_path('.') if @is_ready + end + CallbackDescriptor = Struct.new(:proc, :matcher) # Add callback to be run on file change @@ -85,6 +101,7 @@ module Middleman # # @param [Pathname] path The file that changed # @return [void] + Contract Or[Pathname, String] => Any def did_change(path) path = Pathname(path) logger.debug "== File Change: #{path}" @@ -96,6 +113,7 @@ module Middleman # # @param [Pathname] path The file that was deleted # @return [void] + Contract Or[Pathname, String] => Any def did_delete(path) path = Pathname(path) logger.debug "== File Deletion: #{path}" @@ -108,6 +126,7 @@ module Middleman # @param [Pathname] path The path to reload # @param [Boolean] only_new Whether we only look for new files # @return [void] + Contract Or[String, Pathname], Maybe[Bool] => Any def reload_path(path, only_new=false) # chdir into the root directory so Pathname can work with relative paths Dir.chdir @app.root_path do @@ -132,6 +151,7 @@ module Middleman # # @param [Pathname] path The path to reload # @return [void] + Contract Pathname => Any def find_new_files(path) reload_path(path, true) end @@ -149,7 +169,20 @@ module Middleman Contract Or[String, Pathname] => Bool def ignored?(path) path = path.to_s - app.config[:file_watcher_ignore].any? { |r| path =~ r } + + watched = @watchers.values.any? { |validator| matches?(validator, path) } + not_ignored = @ignores.values.none? { |validator| matches?(validator, path) } + + !(watched && not_ignored) + end + + Contract Or[Regexp, RespondTo[:call]], String => Bool + def matches?(validator, path) + if validator.is_a? Regexp + !!validator.match(path) + else + !!validator.call(path, @app) + end end protected @@ -159,6 +192,7 @@ module Middleman # @param [Pathname] path The file that was changed # @param [Symbol] callbacks_name The name of the callbacks method # @return [void] + Contract Or[Pathname, String], Symbol => Any def run_callbacks(path, callbacks_name) path = path.to_s send(callbacks_name).each do |callback| diff --git a/middleman-core/lib/middleman-core/core_extensions/front_matter.rb b/middleman-core/lib/middleman-core/core_extensions/front_matter.rb index cd96eb38..4dbb7082 100644 --- a/middleman-core/lib/middleman-core/core_extensions/front_matter.rb +++ b/middleman-core/lib/middleman-core/core_extensions/front_matter.rb @@ -27,8 +27,8 @@ module Middleman::CoreExtensions end def before_configuration - file_watcher.changed(&method(:clear_data)) - file_watcher.deleted(&method(:clear_data)) + app.files.changed(&method(:clear_data)) + app.files.deleted(&method(:clear_data)) end # @return Array @@ -96,7 +96,7 @@ module Middleman::CoreExtensions data = {} - return [data, nil] if !file_watcher.exists?(full_path) || ::Middleman::Util.binary?(full_path) + return [data, nil] if !app.files.exists?(full_path) || ::Middleman::Util.binary?(full_path) content = File.read(full_path) diff --git a/middleman-core/lib/middleman-core/core_extensions/i18n.rb b/middleman-core/lib/middleman-core/core_extensions/i18n.rb index 32f3f2b0..e7d94f69 100644 --- a/middleman-core/lib/middleman-core/core_extensions/i18n.rb +++ b/middleman-core/lib/middleman-core/core_extensions/i18n.rb @@ -16,11 +16,16 @@ class Middleman::CoreExtensions::Internationalization < ::Middleman::Extension ::I18n::Backend::Simple.send(:include, ::I18n::Backend::Fallbacks) end - app.config.define_setting :locales_dir, 'locales', 'The directory holding your locale configurations' + locales_file_path = options[:data] - file_watcher.reload_path(app.config[:locales_dir] || options[:data]) + # Tell the file watcher to observe the :locales_dir + app.files.watch :locales do |path, _app| + path.match(/^#{locales_file_path}\/.*(yml|yaml)$/) + end - @locales_glob = File.join(app.config[:locales_dir] || options[:data], '**', '*.{rb,yml,yaml}') + app.files.reload_path(locales_file_path) + + @locales_glob = File.join(locales_file_path, '**', '*.{rb,yml,yaml}') @locales_regex = convert_glob_to_regex(@locales_glob) @maps = {} @@ -33,8 +38,8 @@ class Middleman::CoreExtensions::Internationalization < ::Middleman::Extension # Don't output localizable files app.ignore File.join(options[:templates_dir], '**') - file_watcher.changed(&method(:on_file_changed)) - file_watcher.deleted(&method(:on_file_changed)) + app.files.changed(&method(:on_file_changed)) + app.files.deleted(&method(:on_file_changed)) end helpers do @@ -111,7 +116,7 @@ class Middleman::CoreExtensions::Internationalization < ::Middleman::Extension if options[:langs] Array(options[:langs]).map(&:to_sym) else - known_langs = file_watcher.known_paths.select do |p| + known_langs = app.files.known_paths.select do |p| p.to_s.match(@locales_regex) && (p.to_s.split(File::SEPARATOR).length == 2) end diff --git a/middleman-core/lib/middleman-core/extension.rb b/middleman-core/lib/middleman-core/extension.rb index cb875e89..d52a1fae 100644 --- a/middleman-core/lib/middleman-core/extension.rb +++ b/middleman-core/lib/middleman-core/extension.rb @@ -179,8 +179,6 @@ module Middleman # @return [void] def_delegator :"::Middleman::Extension", :after_extension_activated - def_delegator :"@app.extensions[:file_watcher]", :api, :file_watcher - # Extensions are instantiated when they are activated. # @param [Middleman::Application] app The Middleman::Application instance # @param [Hash] options_hash The raw options hash. Subclasses should not manipulate this directly - it will be turned into {#options}. @@ -242,8 +240,7 @@ module Middleman end def bind_before_configuration - return unless respond_to?(:before_configuration) - @app.before_configuration(&method(:before_configuration)) + @app.before_configuration(&method(:before_configuration)) if respond_to?(:before_configuration) end def bind_after_configuration diff --git a/middleman-core/lib/middleman-core/renderers/sass.rb b/middleman-core/lib/middleman-core/renderers/sass.rb index 05d0b8e5..5ec93b9a 100644 --- a/middleman-core/lib/middleman-core/renderers/sass.rb +++ b/middleman-core/lib/middleman-core/renderers/sass.rb @@ -59,6 +59,10 @@ module Middleman require 'middleman-core/renderers/sass_functions' end + def before_configuration + app.files.watch :sass_cache, /(^|\/)\.sass-cache\// + end + # A SassTemplate for Tilt which outputs debug messages class SassPlusCSSFilenameTemplate < ::Tilt::SassTemplate def initialize(*args, &block) diff --git a/middleman-core/lib/middleman-core/sitemap/extensions/on_disk.rb b/middleman-core/lib/middleman-core/sitemap/extensions/on_disk.rb index 0a42198c..66acb8df 100644 --- a/middleman-core/lib/middleman-core/sitemap/extensions/on_disk.rb +++ b/middleman-core/lib/middleman-core/sitemap/extensions/on_disk.rb @@ -24,8 +24,8 @@ module Middleman Contract None => Any def before_configuration - file_watcher.changed(&method(:touch_file)) - file_watcher.deleted(&method(:remove_file)) + app.files.changed(&method(:touch_file)) + app.files.deleted(&method(:remove_file)) end # Update or add an on-disk file path diff --git a/middleman-core/lib/middleman-core/step_definitions/middleman_steps.rb b/middleman-core/lib/middleman-core/step_definitions/middleman_steps.rb index b2101df4..70dbcf44 100644 --- a/middleman-core/lib/middleman-core/step_definitions/middleman_steps.rb +++ b/middleman-core/lib/middleman-core/step_definitions/middleman_steps.rb @@ -9,9 +9,9 @@ Then /^the file "([^\"]*)" is removed$/ do |path| end Then /^the file "([^\"]*)" did change$/ do |path| - @server_inst.extensions[:file_watcher].api.did_change(path) + @server_inst.files.did_change(path) end Then /^the file "([^\"]*)" did delete$/ do |path| - @server_inst.extensions[:file_watcher].api.did_delete(path) + @server_inst.files.did_delete(path) end diff --git a/middleman-core/lib/middleman-core/util.rb b/middleman-core/lib/middleman-core/util.rb index 36dd881d..aa618ed9 100644 --- a/middleman-core/lib/middleman-core/util.rb +++ b/middleman-core/lib/middleman-core/util.rb @@ -223,14 +223,16 @@ module Middleman def self.all_files_under(path, &ignore) path = Pathname(path) - return [] if ignore && ignore.call(path) - if path.directory? path.children.flat_map do |child| all_files_under(child, &ignore) end.compact elsif path.file? - [path] + if block_given? && ignore.call(path) + [] + else + [path] + end else [] end