Merge pull request #1228 from middleman/file_watcher_refactor

FileWatcher Refactor
This commit is contained in:
Thomas Reynolds 2014-07-11 09:19:45 -07:00
commit 886fe40922
12 changed files with 98 additions and 48 deletions

View file

@ -15,6 +15,8 @@ AllCops:
- 'middleman-core/fixtures/**/*' - 'middleman-core/fixtures/**/*'
- 'middleman-core/features/**/*' - 'middleman-core/features/**/*'
- 'middleman-core/spec/**/*' - 'middleman-core/spec/**/*'
DoubleNegation:
Enabled: false
LineLength: LineLength:
Enabled: false Enabled: false
MethodLength: MethodLength:

View file

@ -1,6 +1,7 @@
master master
=== ===
* New FileWatcher API.
* Remove the `partials_dir` setting. Partials should live next to content, or be addressed with absolute paths. * 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`. * 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`. * 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`.

View file

@ -194,7 +194,7 @@ module Middleman::Cli
logger.debug '== Checking for generated images' logger.debug '== Checking for generated images'
# Double-check 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! @app.sitemap.ensure_resource_list_updated!
# Sort paths to be built by the above order. This is primarily so Compass can # Sort paths to be built by the above order. This is primarily so Compass can

View file

@ -20,10 +20,15 @@ module Middleman
# Setup data files before anything else so they are available when # Setup data files before anything else so they are available when
# parsing config.rb # parsing config.rb
file_watcher.changed(data_file_matcher, &app.extensions[:data].data_store.method(:touch_file)) app.files.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.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 end
helpers do helpers do

View file

@ -6,38 +6,24 @@ module Middleman
module CoreExtensions module CoreExtensions
# API for watching file change events # API for watching file change events
class FileWatcher < Extension 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 attr_reader :api
# Before parsing config, load the data/ directory def initialize(app, config={}, &block)
def before_configuration super
app.config.define_setting :file_watcher_ignore, IGNORE_LIST, 'Regexes for paths that should be ignored when they change.' end
# Before parsing config, load the data/ directory
Contract None => Any
def before_configuration
@api = API.new(app) @api = API.new(app)
app.add_to_instance :files, &method(:api)
app.add_to_config_context :files, &method(:api) app.add_to_config_context :files, &method(:api)
end end
Contract None => Any
def after_configuration def after_configuration
app.config[:file_watcher_ignore] << %r{^#{app.config[:build_dir]}(\/|$)}
@api.reload_path('.') @api.reload_path('.')
@api.is_ready = true
end end
# Core File Change API class # Core File Change API class
@ -47,6 +33,7 @@ module Middleman
attr_reader :app attr_reader :app
attr_reader :known_paths attr_reader :known_paths
attr_accessor :is_ready
def_delegator :@app, :logger def_delegator :@app, :logger
@ -54,11 +41,40 @@ module Middleman
def initialize(app) def initialize(app)
@app = app @app = app
@known_paths = Set.new @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_change_callbacks = Set.new
@on_delete_callbacks = Set.new @on_delete_callbacks = Set.new
end 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) CallbackDescriptor = Struct.new(:proc, :matcher)
# Add callback to be run on file change # Add callback to be run on file change
@ -85,6 +101,7 @@ module Middleman
# #
# @param [Pathname] path The file that changed # @param [Pathname] path The file that changed
# @return [void] # @return [void]
Contract Or[Pathname, String] => Any
def did_change(path) def did_change(path)
path = Pathname(path) path = Pathname(path)
logger.debug "== File Change: #{path}" logger.debug "== File Change: #{path}"
@ -96,6 +113,7 @@ module Middleman
# #
# @param [Pathname] path The file that was deleted # @param [Pathname] path The file that was deleted
# @return [void] # @return [void]
Contract Or[Pathname, String] => Any
def did_delete(path) def did_delete(path)
path = Pathname(path) path = Pathname(path)
logger.debug "== File Deletion: #{path}" logger.debug "== File Deletion: #{path}"
@ -108,6 +126,7 @@ module Middleman
# @param [Pathname] path The path to reload # @param [Pathname] path The path to reload
# @param [Boolean] only_new Whether we only look for new files # @param [Boolean] only_new Whether we only look for new files
# @return [void] # @return [void]
Contract Or[String, Pathname], Maybe[Bool] => Any
def reload_path(path, only_new=false) def reload_path(path, only_new=false)
# chdir into the root directory so Pathname can work with relative paths # chdir into the root directory so Pathname can work with relative paths
Dir.chdir @app.root_path do Dir.chdir @app.root_path do
@ -132,6 +151,7 @@ module Middleman
# #
# @param [Pathname] path The path to reload # @param [Pathname] path The path to reload
# @return [void] # @return [void]
Contract Pathname => Any
def find_new_files(path) def find_new_files(path)
reload_path(path, true) reload_path(path, true)
end end
@ -149,7 +169,20 @@ module Middleman
Contract Or[String, Pathname] => Bool Contract Or[String, Pathname] => Bool
def ignored?(path) def ignored?(path)
path = path.to_s 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 end
protected protected
@ -159,6 +192,7 @@ module Middleman
# @param [Pathname] path The file that was changed # @param [Pathname] path The file that was changed
# @param [Symbol] callbacks_name The name of the callbacks method # @param [Symbol] callbacks_name The name of the callbacks method
# @return [void] # @return [void]
Contract Or[Pathname, String], Symbol => Any
def run_callbacks(path, callbacks_name) def run_callbacks(path, callbacks_name)
path = path.to_s path = path.to_s
send(callbacks_name).each do |callback| send(callbacks_name).each do |callback|

View file

@ -27,8 +27,8 @@ module Middleman::CoreExtensions
end end
def before_configuration def before_configuration
file_watcher.changed(&method(:clear_data)) app.files.changed(&method(:clear_data))
file_watcher.deleted(&method(:clear_data)) app.files.deleted(&method(:clear_data))
end end
# @return Array<Middleman::Sitemap::Resource> # @return Array<Middleman::Sitemap::Resource>
@ -96,7 +96,7 @@ module Middleman::CoreExtensions
data = {} 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) content = File.read(full_path)

View file

@ -16,11 +16,16 @@ class Middleman::CoreExtensions::Internationalization < ::Middleman::Extension
::I18n::Backend::Simple.send(:include, ::I18n::Backend::Fallbacks) ::I18n::Backend::Simple.send(:include, ::I18n::Backend::Fallbacks)
end 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) @locales_regex = convert_glob_to_regex(@locales_glob)
@maps = {} @maps = {}
@ -33,8 +38,8 @@ class Middleman::CoreExtensions::Internationalization < ::Middleman::Extension
# Don't output localizable files # Don't output localizable files
app.ignore File.join(options[:templates_dir], '**') app.ignore File.join(options[:templates_dir], '**')
file_watcher.changed(&method(:on_file_changed)) app.files.changed(&method(:on_file_changed))
file_watcher.deleted(&method(:on_file_changed)) app.files.deleted(&method(:on_file_changed))
end end
helpers do helpers do
@ -111,7 +116,7 @@ class Middleman::CoreExtensions::Internationalization < ::Middleman::Extension
if options[:langs] if options[:langs]
Array(options[:langs]).map(&:to_sym) Array(options[:langs]).map(&:to_sym)
else 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) p.to_s.match(@locales_regex) && (p.to_s.split(File::SEPARATOR).length == 2)
end end

View file

@ -179,8 +179,6 @@ module Middleman
# @return [void] # @return [void]
def_delegator :"::Middleman::Extension", :after_extension_activated def_delegator :"::Middleman::Extension", :after_extension_activated
def_delegator :"@app.extensions[:file_watcher]", :api, :file_watcher
# Extensions are instantiated when they are activated. # Extensions are instantiated when they are activated.
# @param [Middleman::Application] app The Middleman::Application instance # @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}. # @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 end
def bind_before_configuration def bind_before_configuration
return unless respond_to?(:before_configuration) @app.before_configuration(&method(:before_configuration)) if respond_to?(:before_configuration)
@app.before_configuration(&method(:before_configuration))
end end
def bind_after_configuration def bind_after_configuration

View file

@ -59,6 +59,10 @@ module Middleman
require 'middleman-core/renderers/sass_functions' require 'middleman-core/renderers/sass_functions'
end end
def before_configuration
app.files.watch :sass_cache, /(^|\/)\.sass-cache\//
end
# A SassTemplate for Tilt which outputs debug messages # A SassTemplate for Tilt which outputs debug messages
class SassPlusCSSFilenameTemplate < ::Tilt::SassTemplate class SassPlusCSSFilenameTemplate < ::Tilt::SassTemplate
def initialize(*args, &block) def initialize(*args, &block)

View file

@ -24,8 +24,8 @@ module Middleman
Contract None => Any Contract None => Any
def before_configuration def before_configuration
file_watcher.changed(&method(:touch_file)) app.files.changed(&method(:touch_file))
file_watcher.deleted(&method(:remove_file)) app.files.deleted(&method(:remove_file))
end end
# Update or add an on-disk file path # Update or add an on-disk file path

View file

@ -9,9 +9,9 @@ Then /^the file "([^\"]*)" is removed$/ do |path|
end end
Then /^the file "([^\"]*)" did change$/ do |path| Then /^the file "([^\"]*)" did change$/ do |path|
@server_inst.extensions[:file_watcher].api.did_change(path) @server_inst.files.did_change(path)
end end
Then /^the file "([^\"]*)" did delete$/ do |path| Then /^the file "([^\"]*)" did delete$/ do |path|
@server_inst.extensions[:file_watcher].api.did_delete(path) @server_inst.files.did_delete(path)
end end

View file

@ -223,14 +223,16 @@ module Middleman
def self.all_files_under(path, &ignore) def self.all_files_under(path, &ignore)
path = Pathname(path) path = Pathname(path)
return [] if ignore && ignore.call(path)
if path.directory? if path.directory?
path.children.flat_map do |child| path.children.flat_map do |child|
all_files_under(child, &ignore) all_files_under(child, &ignore)
end.compact end.compact
elsif path.file? elsif path.file?
[path] if block_given? && ignore.call(path)
[]
else
[path]
end
else else
[] []
end end