2014-07-16 03:01:45 +02:00
|
|
|
# Watcher Library
|
|
|
|
require 'listen'
|
|
|
|
require 'middleman-core/contracts'
|
|
|
|
|
2015-08-17 23:28:15 +02:00
|
|
|
# Monkey patch Listen silencer so `only` works on directories too
|
|
|
|
module Listen
|
|
|
|
class Silencer
|
|
|
|
# TODO: switch type and path places - and verify
|
2015-09-17 18:41:17 +02:00
|
|
|
def silenced?(relative_path, _type)
|
2015-08-17 23:28:15 +02:00
|
|
|
path = relative_path.to_s
|
|
|
|
|
|
|
|
# if only_patterns && type == :file
|
|
|
|
# return true unless only_patterns.any? { |pattern| path =~ pattern }
|
|
|
|
# end
|
|
|
|
|
2015-09-17 18:41:17 +02:00
|
|
|
return !only_patterns.any? { |pattern| path =~ pattern } if only_patterns
|
2015-08-17 23:28:15 +02:00
|
|
|
|
|
|
|
ignore_patterns.any? { |pattern| path =~ pattern }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-07-16 03:01:45 +02:00
|
|
|
module Middleman
|
|
|
|
# The default source watcher implementation. Watches a directory on disk
|
|
|
|
# and responds to events on changes.
|
|
|
|
class SourceWatcher
|
|
|
|
extend Forwardable
|
|
|
|
include Contracts
|
|
|
|
|
|
|
|
# References to parent `Sources` app and `globally_ignored?` check.
|
|
|
|
def_delegators :@parent, :app, :globally_ignored?
|
|
|
|
|
|
|
|
# Reference to the singleton logger
|
|
|
|
def_delegator :app, :logger
|
|
|
|
|
|
|
|
# The type this watcher is representing
|
2015-04-24 19:26:42 +02:00
|
|
|
Contract Symbol
|
2014-07-16 03:01:45 +02:00
|
|
|
attr_reader :type
|
|
|
|
|
|
|
|
# The directory that is being watched
|
2015-04-24 19:26:42 +02:00
|
|
|
Contract Pathname
|
2014-07-16 03:01:45 +02:00
|
|
|
attr_reader :directory
|
|
|
|
|
|
|
|
# Options for configuring the watcher
|
2015-04-24 19:26:42 +02:00
|
|
|
Contract Hash
|
2014-07-16 03:01:45 +02:00
|
|
|
attr_reader :options
|
|
|
|
|
2015-08-17 23:28:15 +02:00
|
|
|
# Reference to lower level listener
|
|
|
|
attr_reader :listener
|
|
|
|
|
2014-07-16 03:01:45 +02:00
|
|
|
# Construct a new SourceWatcher
|
|
|
|
#
|
|
|
|
# @param [Middleman::Sources] parent The parent collection.
|
|
|
|
# @param [Symbol] type The watcher type.
|
|
|
|
# @param [String] directory The on-disk path to watch.
|
|
|
|
# @param [Hash] options Configuration options.
|
|
|
|
Contract IsA['Middleman::Sources'], Symbol, String, Hash => Any
|
|
|
|
def initialize(parent, type, directory, options={})
|
|
|
|
@parent = parent
|
|
|
|
@options = options
|
|
|
|
|
|
|
|
@type = type
|
|
|
|
@directory = Pathname(directory)
|
|
|
|
|
|
|
|
@files = {}
|
2014-08-25 02:10:25 +02:00
|
|
|
@extensionless_files = {}
|
2014-07-16 03:01:45 +02:00
|
|
|
|
|
|
|
@validator = options.fetch(:validator, proc { true })
|
|
|
|
@ignored = options.fetch(:ignored, proc { false })
|
2015-02-27 02:08:40 +01:00
|
|
|
@only = Array(options.fetch(:only, []))
|
2014-07-16 03:01:45 +02:00
|
|
|
|
|
|
|
@disable_watcher = app.build? || @parent.options.fetch(:disable_watcher, false)
|
|
|
|
@force_polling = @parent.options.fetch(:force_polling, false)
|
|
|
|
@latency = @parent.options.fetch(:latency, nil)
|
|
|
|
|
|
|
|
@listener = nil
|
|
|
|
|
2015-05-04 00:38:18 +02:00
|
|
|
@callbacks = ::Middleman::CallbackManager.new
|
|
|
|
@callbacks.install_methods!(self, [:on_change])
|
2014-07-16 03:01:45 +02:00
|
|
|
|
|
|
|
@waiting_for_existence = !@directory.exist?
|
|
|
|
end
|
|
|
|
|
|
|
|
# Change the path of the watcher (if config values upstream change).
|
|
|
|
#
|
|
|
|
# @param [String] directory The new path.
|
|
|
|
# @return [void]
|
|
|
|
Contract String => Any
|
|
|
|
def update_path(directory)
|
|
|
|
@directory = Pathname(directory)
|
|
|
|
|
|
|
|
stop_listener! if @listener
|
|
|
|
|
|
|
|
update([], @files.values)
|
|
|
|
|
|
|
|
poll_once!
|
|
|
|
|
|
|
|
listen! unless @disable_watcher
|
|
|
|
end
|
|
|
|
|
|
|
|
# Stop watching.
|
|
|
|
#
|
|
|
|
# @return [void]
|
2015-04-24 19:26:42 +02:00
|
|
|
Contract Any
|
2014-07-16 03:01:45 +02:00
|
|
|
def unwatch
|
|
|
|
stop_listener!
|
|
|
|
end
|
|
|
|
|
|
|
|
# All the known files in this watcher.
|
|
|
|
#
|
|
|
|
# @return [Array<Middleman::SourceFile>]
|
2015-04-24 19:26:42 +02:00
|
|
|
Contract ArrayOf[IsA['Middleman::SourceFile']]
|
2014-07-16 03:01:45 +02:00
|
|
|
def files
|
|
|
|
@files.values
|
|
|
|
end
|
|
|
|
|
|
|
|
# Find a specific file in this watcher.
|
|
|
|
#
|
|
|
|
# @param [String, Pathname] path The search path.
|
|
|
|
# @param [Boolean] glob If the path contains wildcard characters.
|
|
|
|
# @return [Middleman::SourceFile, nil]
|
|
|
|
Contract Or[String, Pathname], Maybe[Bool] => Maybe[IsA['Middleman::SourceFile']]
|
|
|
|
def find(path, glob=false)
|
2015-09-17 22:53:43 +02:00
|
|
|
path = path.to_s.encode!('UTF-8', 'UTF-8-MAC') if RUBY_PLATFORM =~ /darwin/
|
|
|
|
|
2014-07-16 03:01:45 +02:00
|
|
|
p = Pathname(path)
|
|
|
|
|
|
|
|
return nil if p.absolute? && !p.to_s.start_with?(@directory.to_s)
|
|
|
|
|
|
|
|
p = @directory + p if p.relative?
|
|
|
|
if glob
|
2014-08-25 02:10:25 +02:00
|
|
|
@extensionless_files[p]
|
2014-07-16 03:01:45 +02:00
|
|
|
else
|
|
|
|
@files[p]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Check if a file simply exists in this watcher.
|
|
|
|
#
|
|
|
|
# @param [String, Pathname] path The search path.
|
|
|
|
# @return [Boolean]
|
|
|
|
Contract Or[String, Pathname] => Bool
|
|
|
|
def exists?(path)
|
|
|
|
!find(path).nil?
|
|
|
|
end
|
|
|
|
|
|
|
|
# Start the `listen` gem Listener.
|
|
|
|
#
|
|
|
|
# @return [void]
|
2015-04-24 19:26:42 +02:00
|
|
|
Contract Any
|
2014-07-16 03:01:45 +02:00
|
|
|
def listen!
|
|
|
|
return if @disable_watcher || @listener || @waiting_for_existence
|
|
|
|
|
2015-04-26 20:32:47 +02:00
|
|
|
config = {
|
|
|
|
force_polling: @force_polling,
|
|
|
|
wait_for_delay: 0.5
|
|
|
|
}
|
|
|
|
|
2014-07-16 03:01:45 +02:00
|
|
|
config[:latency] = @latency if @latency
|
|
|
|
|
|
|
|
@listener = ::Listen.to(@directory.to_s, config, &method(:on_listener_change))
|
2015-02-27 02:08:40 +01:00
|
|
|
|
2015-08-17 23:28:15 +02:00
|
|
|
@listener.ignore(/^\.sass-cache/)
|
2015-11-28 00:26:46 +01:00
|
|
|
# @listener.only(@only) unless @only.empty?
|
2015-08-17 23:28:15 +02:00
|
|
|
|
|
|
|
@listener.start
|
2014-07-16 03:01:45 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
# Stop the listener.
|
|
|
|
#
|
|
|
|
# @return [void]
|
2015-04-24 19:26:42 +02:00
|
|
|
Contract Any
|
2014-07-16 03:01:45 +02:00
|
|
|
def stop_listener!
|
|
|
|
return unless @listener
|
|
|
|
|
|
|
|
@listener.stop
|
|
|
|
@listener = nil
|
|
|
|
end
|
|
|
|
|
|
|
|
# Manually trigger update events.
|
|
|
|
#
|
|
|
|
# @return [void]
|
2015-04-24 19:26:42 +02:00
|
|
|
Contract Any
|
2014-07-16 03:01:45 +02:00
|
|
|
def poll_once!
|
2015-11-27 23:16:55 +01:00
|
|
|
updated = ::Middleman::Util.all_files_under(@directory.to_s)
|
|
|
|
removed = @files.keys.reject { |p| updated.include?(p) }
|
2014-07-16 03:01:45 +02:00
|
|
|
|
|
|
|
update(updated, removed)
|
|
|
|
|
|
|
|
return unless @waiting_for_existence && @directory.exist?
|
|
|
|
|
|
|
|
@waiting_for_existence = false
|
|
|
|
listen!
|
|
|
|
end
|
|
|
|
|
|
|
|
# Work around this bug: http://bugs.ruby-lang.org/issues/4521
|
|
|
|
# where Ruby will call to_s/inspect while printing exception
|
|
|
|
# messages, which can take a long time (minutes at full CPU)
|
|
|
|
# if the object is huge or has cyclic references, like this.
|
|
|
|
def to_s
|
|
|
|
"#<Middleman::SourceWatcher:0x#{object_id} type=#{@type.inspect} directory=#{@directory.inspect}>"
|
|
|
|
end
|
|
|
|
alias_method :inspect, :to_s # Ruby 2.0 calls inspect for NoMethodError instead of to_s
|
|
|
|
|
|
|
|
protected
|
|
|
|
|
|
|
|
# The `listen` gem callback.
|
|
|
|
#
|
|
|
|
# @param [Array] modified List of modified files.
|
|
|
|
# @param [Array] added List of added files.
|
|
|
|
# @param [Array] removed List of removed files.
|
|
|
|
# @return [void]
|
|
|
|
Contract Array, Array, Array => Any
|
|
|
|
def on_listener_change(modified, added, removed)
|
|
|
|
updated = (modified + added)
|
|
|
|
|
|
|
|
return if updated.empty? && removed.empty?
|
|
|
|
|
|
|
|
update(updated.map { |s| Pathname(s) }, removed.map { |s| Pathname(s) })
|
|
|
|
end
|
|
|
|
|
|
|
|
# Update our internal list of files on a change.
|
|
|
|
#
|
|
|
|
# @param [String, Pathname] path The updated file path.
|
|
|
|
# @return [void]
|
|
|
|
Contract ArrayOf[Pathname], ArrayOf[Pathname] => Any
|
|
|
|
def update(updated_paths, removed_paths)
|
2015-11-29 01:48:08 +01:00
|
|
|
valid_updates = updated_paths
|
2015-11-29 04:32:45 +01:00
|
|
|
.map { |p| ::Middleman::Util.path_to_source_file(p, @directory, @type, @options.fetch(:destination_dir, false)) }
|
2014-11-19 18:04:56 +01:00
|
|
|
.select(&method(:valid?))
|
2015-11-29 01:48:08 +01:00
|
|
|
|
|
|
|
valid_updates.each do |f|
|
|
|
|
add_file_to_cache(f)
|
|
|
|
logger.debug "== Change (#{f[:types].inspect}): #{f[:relative_path]}"
|
|
|
|
end
|
|
|
|
|
|
|
|
related_updates = ::Middleman::Util.find_related_files(app, (updated_paths + removed_paths)).select(&method(:valid?))
|
|
|
|
|
|
|
|
related_updates.each do |f|
|
|
|
|
logger.debug "== Possible Change (#{f[:types].inspect}): #{f[:relative_path]}"
|
|
|
|
end
|
|
|
|
|
|
|
|
valid_updates |= related_updates
|
2014-07-16 03:01:45 +02:00
|
|
|
|
|
|
|
valid_removes = removed_paths
|
2014-11-19 18:04:56 +01:00
|
|
|
.lazy
|
|
|
|
.select(&@files.method(:key?))
|
|
|
|
.map(&@files.method(:[]))
|
|
|
|
.select(&method(:valid?))
|
|
|
|
.to_a
|
|
|
|
.each do |f|
|
|
|
|
remove_file_from_cache(f)
|
|
|
|
logger.debug "== Deletion (#{f[:types].inspect}): #{f[:relative_path]}"
|
|
|
|
end
|
2014-10-15 21:25:06 +02:00
|
|
|
|
2015-05-04 00:38:18 +02:00
|
|
|
execute_callbacks(:on_change, [
|
2014-10-15 21:25:06 +02:00
|
|
|
valid_updates,
|
2015-05-04 00:38:18 +02:00
|
|
|
valid_removes,
|
|
|
|
self
|
|
|
|
]) unless valid_updates.empty? && valid_removes.empty?
|
2014-07-16 03:01:45 +02:00
|
|
|
end
|
|
|
|
|
2014-08-25 02:10:25 +02:00
|
|
|
def add_file_to_cache(f)
|
|
|
|
@files[f[:full_path]] = f
|
2014-11-19 18:04:56 +01:00
|
|
|
@extensionless_files[strip_extensions(f[:full_path])] = f
|
2014-08-25 02:10:25 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
def remove_file_from_cache(f)
|
|
|
|
@files.delete(f[:full_path])
|
2014-11-19 18:04:56 +01:00
|
|
|
@extensionless_files.delete(strip_extensions(f[:full_path]))
|
|
|
|
end
|
|
|
|
|
2015-02-24 20:06:28 +01:00
|
|
|
Contract Pathname => Pathname
|
2014-11-19 18:04:56 +01:00
|
|
|
def strip_extensions(p)
|
2015-04-26 20:13:29 +02:00
|
|
|
p = p.sub_ext('') while ::Tilt[p.to_s] || p.extname == '.html'
|
2014-11-19 18:04:56 +01:00
|
|
|
Pathname(p.to_s + '.*')
|
2014-08-25 02:10:25 +02:00
|
|
|
end
|
|
|
|
|
2014-07-16 03:01:45 +02:00
|
|
|
# Check if this watcher should care about a file.
|
|
|
|
#
|
|
|
|
# @param [Middleman::SourceFile] file The file.
|
|
|
|
# @return [Boolean]
|
|
|
|
Contract IsA['Middleman::SourceFile'] => Bool
|
|
|
|
def valid?(file)
|
2015-04-26 20:13:29 +02:00
|
|
|
return false unless @validator.call(file) && !globally_ignored?(file)
|
2015-02-27 02:08:40 +01:00
|
|
|
|
2015-04-26 20:13:29 +02:00
|
|
|
if @only.empty?
|
2014-11-19 18:04:56 +01:00
|
|
|
!@ignored.call(file)
|
2015-02-27 02:08:40 +01:00
|
|
|
else
|
|
|
|
@only.any? { |reg| reg.match(file[:relative_path].to_s) }
|
|
|
|
end
|
2014-07-16 03:01:45 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|