require 'zeitwerk' require 'listen' module Camping # == The Camping Reloader # # Camping apps are generally small and predictable. Many Camping apps are # contained within a single file. Larger apps are split into a handful of # other Ruby libraries within the same directory. # # Since Camping apps (and their dependencies) are loaded with Ruby's require # method, there is a record of them in $LOADED_FEATURES. Which leaves a # perfect space for this class to manage auto-reloading an app if any of its # immediate dependencies changes. # # == Wrapping Your Apps # # Since bin/camping and the Camping::Server class already use the Reloader, # you probably don't need to hack it on your own. But, if you're rolling your # own situation, here's how. # # Rather than this: # # require 'yourapp' # # Use this: # # require 'camping/reloader' # reloader = Camping::Reloader.new('/path/to/yourapp.rb') # blog = reloader.apps[:Blog] # wiki = reloader.apps[:Wiki] # # The blog and wiki objects will behave exactly like your # Blog and Wiki, but they will update themselves if yourapp.rb changes. # # You can also give Reloader more than one script. class Loader attr_reader :file Loaders = [] def initialize(file=nil, &blk) @file = file @mtime = Time.at(0) @requires = [] @apps = {} @callback = blk @root = Dir.pwd @file = @root + '/camp.rb' if @file == nil @zeit = Zeitwerk::Loader.new Loaders << @zeit # setup Zeit for this reloader setup_zeit(@zeit) dirs = [@root] dirs << "#{@root}/apps" if Dir.exist? "#{@root}/apps" dirs << "#{@root}/lib" if Dir.exist? "#{@root}/lib" # setup recursive listener on the apps and lib directories from the source script. @listener = Listen.to(*dirs) do |modified, added, removed| @mtime = Time.now reload! end start end # pass through methods to the Listener. # for testing purposes. def processing_events?;@listener.processing? end def stop;@listener.stop end def pause;@listener.pause end def start;@listener.start end def name @name ||= begin base = @file.dup base = File.dirname(base) if base =~ /\bconfig\.ru$/ base.sub!(/\.[^.]+/, '') File.basename(base).to_sym end end # remove_constants called inside this. def load_everything() all_requires = $LOADED_FEATURES.dup all_apps = Camping::Apps.dup load_file reload_directory("#{@root}/apps") reload_directory("#{@root}/lib") Camping.make_camp ensure @requires = [] new_apps = Camping::Apps - all_apps @apps = new_apps.inject({}) do |hash, app| if file = app.options[:__FILE__] full = File.expand_path(file) @requires << [file, full] end key = app.name.to_sym hash[key] = app apps.each do |app| @callback.call(app) if @callback app.create if app.respond_to?(:create) end hash end ($LOADED_FEATURES - all_requires).each do |req| full = full_path(req) @requires << [req, full] # if dirs.any? { |x| full.index(x) == 0 } end @mtime = mtime self end # load_file # # Rack::Builder is mainly used to parse a config.ru file and to # build a rack app with middleware from that. def load_file if @file =~ /\.ru$/ @app = Rack::Builder.parse_file(@file) else load(@file) end @requires << [@file, File.expand_path(@file)] end # removes all constants recursively included using this script as a root. # so everything in /apps, and /lib in relation from this script. def remove_constants @requires.each do |(path, full)| $LOADED_FEATURES.delete(path) end @apps.each do |name, app| Camping::Apps.delete(app) Object.send :remove_const, name end.dup ensure @apps.clear @requires.clear end # Reloads the file if needed. No harm is done by calling this multiple # times, so feel free call just to be sure. def reload return if @mtime >= mtime rescue nil reload! end # Force a reload. def reload! remove_constants load_everything end # Checks if both scripts watches the same file. def ==(other) @file == other.file end def apps if @app { name => @app } else @apps end end private # sets up Zeit autoloading for the script locations. def setup_zeit(loader) loader.push_dir("#{@root}/apps") if can_add_directory "#{@root}/apps" loader.push_dir("#{@root}/lib") if can_add_directory "#{@root}/lib" loader.enable_reloading if ENV['environment'] == 'development' loader.setup end # verifies that we can add a directory to the loader. # used for testing to prevent multiple loaders from watching the same directory. def can_add_directory(directory) if Dir.exist?("#{@root}/apps") Loaders.each do |loader| return false if loader.dirs.include? directory end true else false end end # Splits the descendent files and folders found in a given directory for eager loading and recursion. def folders_and_files_in(directory) directory = directory + "/*" # unless directory [Dir.glob(directory).select {|f| !File.directory? f }, Dir.glob(directory).select {|f| File.directory? f }] end # Reloads a directory recursively. loading more shallow files before deeper files. def reload_directory(directory) files, folders = folders_and_files_in(directory) files.each {|file| @requires << [file, File.expand_path(file)] load file } folders.each {|folder| reload_directory folder } end def mtime @requires.map do |(path, full)| File.mtime(full) end.reject {|t| t > Time.now }.max || Time.now end # Figures out the full path of a required file. def full_path(req) return req if File.exist?(req) dir = $LOAD_PATH.detect { |l| File.exist?(File.join(l, req)) } if dir File.expand_path(req, File.expand_path(dir)) else req end end end Reloader = Loader end