blob: 2f3fb16fa95961532b8ed924897515a11dfcaa43 [file] [log] [blame]
require 'listen'
module Jekyll
module Watcher
extend self
# Public: Continuously watch for file changes and rebuild the site
# whenever a change is detected.
#
# If the optional site argument is populated, that site instance will be
# reused and the options Hash ignored. Otherwise, a new site instance will
# be instantiated from the options Hash and used.
#
# options - A Hash containing the site configuration
# site - The current site instance (populated starting with Jekyll 3.2)
# (optional, default: nil)
#
# Returns nothing.
def watch(options, site = nil)
ENV["LISTEN_GEM_DEBUGGING"] ||= "1" if options['verbose']
site ||= Jekyll::Site.new(options)
listener = build_listener(site, options)
listener.start
Jekyll.logger.info "Auto-regeneration:", "enabled for '#{options["source"]}'"
unless options['serving']
trap("INT") do
listener.stop
puts " Halting auto-regeneration."
exit 0
end
sleep_forever
end
rescue ThreadError
# You pressed Ctrl-C, oh my!
end
# TODO: shouldn't be public API
def build_listener(site, options)
Listen.to(
options['source'],
:ignore => listen_ignore_paths(options),
:force_polling => options['force_polling'],
&(listen_handler(site))
)
end
def listen_handler(site)
proc do |modified, added, removed|
t = Time.now
c = modified + added + removed
n = c.length
print Jekyll.logger.message("Regenerating:",
"#{n} file(s) changed at #{t.strftime("%Y-%m-%d %H:%M:%S")} ")
begin
site.process
puts "...done in #{Time.now - t} seconds."
rescue => e
puts "...error:"
Jekyll.logger.warn "Error:", e.message
Jekyll.logger.warn "Error:", "Run jekyll build --trace for more information."
end
end
end
def custom_excludes(options)
Array(options['exclude']).map { |e| Jekyll.sanitized_path(options['source'], e) }
end
def config_files(options)
%w(yml yaml toml).map do |ext|
Jekyll.sanitized_path(options['source'], "_config.#{ext}")
end
end
def to_exclude(options)
[
config_files(options),
options['destination'],
custom_excludes(options)
].flatten
end
# Paths to ignore for the watch option
#
# options - A Hash of options passed to the command
#
# Returns a list of relative paths from source that should be ignored
def listen_ignore_paths(options)
source = Pathname.new(options['source']).expand_path
paths = to_exclude(options)
paths.map do |p|
absolute_path = Pathname.new(p).expand_path
next unless absolute_path.exist?
begin
relative_path = absolute_path.relative_path_from(source).to_s
unless relative_path.start_with?('../')
path_to_ignore = Regexp.new(Regexp.escape(relative_path))
Jekyll.logger.debug "Watcher:", "Ignoring #{path_to_ignore}"
path_to_ignore
end
rescue ArgumentError
# Could not find a relative path
end
end.compact + [/\.jekyll\-metadata/]
end
def sleep_forever
loop { sleep 1000 }
end
end
end