class Middleman::SourceWatcher
and responds to events on changes.
The default source watcher implementation. Watches a directory on disk
def exists?(path)
def exists?(path) !find(path).nil? end
def files
def files @files.values end
def find(path, glob=false)
def find(path, glob=false) path = path.to_s.encode!('UTF-8', 'UTF-8-MAC') if RUBY_PLATFORM =~ /darwin/ p = Pathname(path) return nil if p.absolute? && !p.to_s.start_with?(@directory.to_s) p = @directory + p if p.relative? if glob @extensionless_files[p] else @files[p] end end
def find_new_files!
def find_new_files! new_files = ::Middleman::Util.all_files_under(@directory.to_s, &method(:should_not_recurse?)) .reject { |p| @files.key?(p) } update(new_files, []).flatten.map { |s| s[:full_path] } end
def initialize(parent, type, directory, options={})
def initialize(parent, type, directory, options={}) @parent = parent @options = options @type = type @directory = Pathname(directory) @files = {} @extensionless_files = {} @frontmatter = options.fetch(:frontmatter, true) @binary = options.fetch(:binary, false) @validator = options.fetch(:validator, proc { true }) @ignored = options.fetch(:ignored, proc { false }) @only = Array(options.fetch(:only, [])) @disable_watcher = app.build? @force_polling = false @latency = nil @wait_for_delay = nil @listener = nil @callbacks = ::Middleman::CallbackManager.new @callbacks.install_methods!(self, [:on_change]) @waiting_for_existence = !@directory.exist? end
def listen!
def listen! return if @disable_watcher || @listener || @waiting_for_existence config = { force_polling: @force_polling } config[:wait_for_delay] = @wait_for_delay.try(:to_f) || 0.5 config[:latency] = @latency.to_f if @latency @listener = ::Listen.to(@directory.to_s, config, &method(:on_listener_change)) @listener.ignore(/^\.sass-cache/) @listener.ignore(/^node_modules/) @listener.ignore(/^vendor\/bundle/) @listener.start end
def on_listener_change(modified, added, removed)
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
def partial?(relative_path)
def partial?(relative_path) relative_path.split(::File::SEPARATOR).any? { |p| p.start_with?('_') } end
def path_to_source_file(path, directory, type, destination_dir)
def path_to_source_file(path, directory, type, destination_dir) types = Set.new([type]) types << :no_frontmatter unless @frontmatter types << :binary if @binary relative_path = path.relative_path_from(directory) relative_path = File.join(destination_dir, relative_path) if destination_dir types << :no_frontmatter if partial?(relative_path.to_s) ::Middleman::SourceFile.new(Pathname(relative_path), path, directory, types, 0) end
def poll_once!
def poll_once! updated = ::Middleman::Util.all_files_under(@directory.to_s, &method(:should_not_recurse?)) removed = @files.keys - updated result = update(updated, removed) if @waiting_for_existence && @directory.exist? @waiting_for_existence = false listen! end result.flatten.map { |s| s[:full_path] } end
def record_file_change(f)
def record_file_change(f) if @files[f[:full_path]] @files[f[:full_path]][:version] += 1 else @files[f[:full_path]] = f @extensionless_files[strip_extensions(f[:full_path])] = f end end
def remove_file_from_cache(f)
def remove_file_from_cache(f) @files.delete(f[:full_path]) @extensionless_files.delete(strip_extensions(f[:full_path])) end
def should_not_recurse?(p)
def should_not_recurse?(p) relative_path = p.relative_path_from(@directory).to_s IGNORED_DIRECTORIES.include?(relative_path) end
def stop_listener!
def stop_listener! return unless @listener @listener.stop @listener = nil end
def strip_extensions(p)
def strip_extensions(p) p = p.sub_ext('') while Middleman::Util.tilt_class(p.to_s) || p.extname == '.html' Pathname(p.to_s + '.*') end
def to_s
messages, which can take a long time (minutes at full CPU)
where Ruby will call to_s/inspect while printing exception
Work around this bug: http://bugs.ruby-lang.org/issues/4521
def to_s "#<Middleman::SourceWatcher:0x#{object_id} type=#{@type.inspect} directory=#{@directory.inspect}>" end
def unwatch
def unwatch stop_listener! end
def update(updated_paths, removed_paths)
def update(updated_paths, removed_paths) valid_updates = updated_paths .map { |p| @files[p] || path_to_source_file(p, @directory, @type, @options[:destination_dir]) } .select(&method(:valid?)) valid_updates.each do |f| record_file_change(f) logger.debug "== Change (#{f[:types].inspect}): #{f[:relative_path]}" end related_sources = valid_updates.map { |u| u[:full_path] } + removed_paths related_updates = ::Middleman::Util.find_related_files(app, related_sources).select(&method(:valid?)) related_updates.each do |f| logger.debug "== Possible Change (#{f[:types].inspect}): #{f[:relative_path]}" end valid_updates |= related_updates valid_removes = removed_paths .select(&@files.method(:key?)) .map(&@files.method(:[])) .select(&method(:valid?)) .each do |f| remove_file_from_cache(f) logger.debug "== Deletion (#{f[:types].inspect}): #{f[:relative_path]}" end unless valid_updates.empty? && valid_removes.empty? execute_callbacks(:on_change, [ valid_updates, valid_removes, self ]) end [valid_updates, valid_removes] end
def update_config(options={})
def update_config(options={}) without_listener_running do @disable_watcher = options.fetch(:disable_watcher, false) @force_polling = options.fetch(:force_polling, false) @latency = options.fetch(:latency, nil) @wait_for_delay = options.fetch(:wait_for_delay, nil) end end
def update_path(directory)
def update_path(directory) @directory = Pathname(File.expand_path(directory, app.root)) without_listener_running do update([], @files.values.map { |source_file| source_file[:full_path] }) end poll_once! end
def valid?(file)
def valid?(file) return false unless @validator.call(file) && !globally_ignored?(file) if @only.empty? !@ignored.call(file) else @only.any? { |reg| file[:relative_path].to_s =~ reg } end end
def without_listener_running
def without_listener_running listener_running = @listener && @listener.processing? stop_listener! if listener_running yield if listener_running poll_once! listen! end end