Multiple Source watchers
This commit is contained in:
parent
525e700bfa
commit
bedf235ff6
57 changed files with 989 additions and 570 deletions
|
@ -1,205 +1,75 @@
|
|||
require 'pathname'
|
||||
require 'set'
|
||||
require 'middleman-core/contracts'
|
||||
require 'middleman-core/sources'
|
||||
|
||||
module Middleman
|
||||
module CoreExtensions
|
||||
# API for watching file change events
|
||||
class FileWatcher < Extension
|
||||
attr_reader :api
|
||||
# All defined sources.
|
||||
Contract None => IsA['Middleman::Sources']
|
||||
attr_reader :sources
|
||||
|
||||
# The default list of ignores.
|
||||
IGNORES = {
|
||||
emacs_files: /(^|\/)\.?#/,
|
||||
tilde_files: /~$/,
|
||||
ds_store: /\.DS_Store$/,
|
||||
git: /(^|\/)\.git(ignore|modules|\/)/
|
||||
}
|
||||
|
||||
# Setup the extension.
|
||||
def initialize(app, config={}, &block)
|
||||
super
|
||||
|
||||
# Setup source collection.
|
||||
@sources = ::Middleman::Sources.new(app,
|
||||
disable_watcher: app.config[:watcher_disable],
|
||||
force_polling: app.config[:force_polling],
|
||||
latency: app.config[:watcher_latency])
|
||||
|
||||
# Add default ignores.
|
||||
IGNORES.each do |key, value|
|
||||
@sources.ignore key, :all, value
|
||||
end
|
||||
|
||||
# Watch current source.
|
||||
start_watching(app.config[:source])
|
||||
|
||||
# Expose API to app and config.
|
||||
app.add_to_instance(:files, &method(:sources))
|
||||
app.add_to_config_context(:files, &method(:sources))
|
||||
end
|
||||
|
||||
# Before parsing config, load the data/ directory
|
||||
# Before we config, find initial files.
|
||||
#
|
||||
# @return [void]
|
||||
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)
|
||||
@sources.find_new_files!
|
||||
end
|
||||
|
||||
# After we config, find new files since config can change paths.
|
||||
#
|
||||
# @return [void]
|
||||
Contract None => Any
|
||||
def after_configuration
|
||||
@api.reload_path('.')
|
||||
@api.is_ready = true
|
||||
if @original_source_dir != app.config[:source]
|
||||
@watcher.update_path(app.config[:source])
|
||||
end
|
||||
|
||||
@sources.start!
|
||||
@sources.find_new_files!
|
||||
end
|
||||
|
||||
# Core File Change API class
|
||||
class API
|
||||
extend Forwardable
|
||||
include Contracts
|
||||
protected
|
||||
|
||||
attr_reader :app
|
||||
attr_reader :known_paths
|
||||
attr_accessor :is_ready
|
||||
|
||||
def_delegator :@app, :logger
|
||||
|
||||
# Initialize api and internal path cache
|
||||
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
|
||||
#
|
||||
# @param [nil,Regexp] matcher A Regexp to match the change path against
|
||||
# @return [Array<Proc>]
|
||||
Contract Or[Regexp, Proc] => SetOf['Middleman::CoreExtensions::FileWatcher::API::CallbackDescriptor']
|
||||
def changed(matcher=nil, &block)
|
||||
@on_change_callbacks << CallbackDescriptor.new(block, matcher) if block_given?
|
||||
@on_change_callbacks
|
||||
end
|
||||
|
||||
# Add callback to be run on file deletion
|
||||
#
|
||||
# @param [nil,Regexp] matcher A Regexp to match the deleted path against
|
||||
# @return [Array<Proc>]
|
||||
Contract Or[Regexp, Proc] => SetOf['Middleman::CoreExtensions::FileWatcher::API::CallbackDescriptor']
|
||||
def deleted(matcher=nil, &block)
|
||||
@on_delete_callbacks << CallbackDescriptor.new(block, matcher) if block_given?
|
||||
@on_delete_callbacks
|
||||
end
|
||||
|
||||
# Notify callbacks that a file changed
|
||||
#
|
||||
# @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}"
|
||||
@known_paths << path
|
||||
run_callbacks(path, :changed)
|
||||
end
|
||||
|
||||
# Notify callbacks that a file was deleted
|
||||
#
|
||||
# @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}"
|
||||
@known_paths.delete(path)
|
||||
run_callbacks(path, :deleted)
|
||||
end
|
||||
|
||||
# Manually trigger update events
|
||||
#
|
||||
# @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
|
||||
path = Pathname(path)
|
||||
return unless path.exist?
|
||||
|
||||
glob = (path + '**').to_s
|
||||
subset = @known_paths.select { |p| p.fnmatch(glob) }
|
||||
|
||||
::Middleman::Util.all_files_under(path, &method(:ignored?)).each do |filepath|
|
||||
next if only_new && subset.include?(filepath)
|
||||
|
||||
subset.delete(filepath)
|
||||
did_change(filepath)
|
||||
end
|
||||
|
||||
subset.each(&method(:did_delete)) unless only_new
|
||||
end
|
||||
end
|
||||
|
||||
# Like reload_path, but only triggers events on new files
|
||||
#
|
||||
# @param [Pathname] path The path to reload
|
||||
# @return [void]
|
||||
Contract Pathname => Any
|
||||
def find_new_files(path)
|
||||
reload_path(path, true)
|
||||
end
|
||||
|
||||
Contract String => Bool
|
||||
def exists?(path)
|
||||
p = Pathname(path)
|
||||
p = p.relative_path_from(Pathname(@app.root)) unless p.relative?
|
||||
@known_paths.include?(p)
|
||||
end
|
||||
|
||||
# Whether this path is ignored
|
||||
# @param [Pathname] path
|
||||
# @return [Boolean]
|
||||
Contract Or[String, Pathname] => Bool
|
||||
def ignored?(path)
|
||||
path = path.to_s
|
||||
|
||||
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
|
||||
|
||||
# Notify callbacks for a file given an array of callbacks
|
||||
#
|
||||
# @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|
|
||||
next unless callback[:matcher].nil? || path.match(callback[:matcher])
|
||||
@app.instance_exec(path, &callback[:proc])
|
||||
end
|
||||
end
|
||||
# Watch the source directory.
|
||||
#
|
||||
# @return [void]
|
||||
Contract String => Any
|
||||
def start_watching(dir)
|
||||
@original_source_dir = dir
|
||||
@watcher = @sources.watch :source, path: File.join(app.root, dir)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue