class Camping::Loader::Reloader

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.

Constants

Loaders
Reloader

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.

Attributes

file[R]

Public Class Methods

new(file=nil, &blk) click to toggle source
# File lib/camping/loader.rb, line 41
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

Public Instance Methods

==(other) click to toggle source

Checks if both scripts watches the same file.

# File lib/camping/loader.rb, line 174
def ==(other)
  @file == other.file
end
apps() click to toggle source
# File lib/camping/loader.rb, line 178
def apps
  if @app
    { name => @app }
  else
    @apps
  end
end
can_add_directory(directory) click to toggle source

verifies that we can add a directory to the loader. used for testing to prevent multiple loaders from watching the same directory.

# File lib/camping/loader.rb, line 199
def can_add_directory(directory)
  if Dir.exist?(directory)
    Loaders.each do |loader|
      return false if loader.dirs.include? directory
    end
    true
  else
    false
  end
end
folders_and_files_in(directory) click to toggle source

Splits the descendent files and folders found in a given directory for eager loading and recursion. If the given directory doesn’t exist or is empty, then nothing is returned.

# File lib/camping/loader.rb, line 212
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
full_path(req) click to toggle source

Figures out the full path of a required file.

# File lib/camping/loader.rb, line 239
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
load_everything() click to toggle source

remove_constants called inside this.

# File lib/camping/loader.rb, line 84
def load_everything()
  all_requires = $LOADED_FEATURES.dup
  all_apps = Camping::Apps.dup

  # Zeitwerk will Autoload stuff, which is great. But we don't want Zeitwerk
  # autoloading when we evaluate the camp.rb file because it will try to
  # autoload any controllers and helpers that we've defined in there from
  # the descendant /apps and /lib directory, which then make it break.
  @zeit.unload
  load_file
  @zeit.setup
  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

    if !@apps.include?(key)
      @callback.call(app) if @callback
      app.create if app.respond_to?(:create)
      app.kindling if app.respond_to?(:kindling)
    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() click to toggle source

load_file

Rack::Builder is mainly used to parse a config.ru file and to build a rack app with middleware from that.

# File lib/camping/loader.rb, line 135
def load_file
  if @file =~ /\.ru$/
    @app = Rack::Builder.parse_file(@file)
  else
    load(@file)
  end
  @requires << [@file, File.expand_path(@file)]
end
mtime() click to toggle source
# File lib/camping/loader.rb, line 232
def mtime
  @requires.map do |(path, full)|
    File.mtime(full)
  end.reject {|t| t > Time.now }.max || Time.now
end
name() click to toggle source
# File lib/camping/loader.rb, line 74
def name
  @name ||= begin
    base = @file.dup
    base = File.dirname(base) if base =~ /\bconfig\.ru$/
    base.sub!(/\.[^.]+/, '')
    File.basename(base).to_sym
  end
end
pause(= @listener.pause) click to toggle source
# File lib/camping/loader.rb, line 71
    def pause = @listener.pause
    def start = @listener.start

    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

      # Zeitwerk will Autoload stuff, which is great. But we don't want Zeitwerk
      # autoloading when we evaluate the camp.rb file because it will try to
      # autoload any controllers and helpers that we've defined in there from
      # the descendant /apps and /lib directory, which then make it break.
      @zeit.unload
      load_file
      @zeit.setup
      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

        if !@apps.include?(key)
          @callback.call(app) if @callback
          app.create if app.respond_to?(:create)
          app.kindling if app.respond_to?(:kindling)
        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) unless path.match? "concurrent-ruby"
        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.ignore("#{@root}/lib/camping-unabridged.rb")
            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?(directory)
        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.
    # If the given directory doesn't exist or is empty, then nothing is returned.
    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.
    # If the given directory doesn't exist or is empty, then nothing happens.
    def reload_directory(directory)
      files, folders = folders_and_files_in(directory)
      files.each {|file|
        next if file.include? "unabridged"
        @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
processing_events?(= @listener.processing?) click to toggle source

pass through methods to the Listener. for testing purposes.

# File lib/camping/loader.rb, line 69
  def processing_events? = @listener.processing?
  def stop = @listener.stop
  def pause = @listener.pause
  def start = @listener.start

  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

    # Zeitwerk will Autoload stuff, which is great. But we don't want Zeitwerk
    # autoloading when we evaluate the camp.rb file because it will try to
    # autoload any controllers and helpers that we've defined in there from
    # the descendant /apps and /lib directory, which then make it break.
    @zeit.unload
    load_file
    @zeit.setup
    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

      if !@apps.include?(key)
        @callback.call(app) if @callback
        app.create if app.respond_to?(:create)
        app.kindling if app.respond_to?(:kindling)
      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) unless path.match? "concurrent-ruby"
      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.ignore("#{@root}/lib/camping-unabridged.rb")
          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?(directory)
      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.
  # If the given directory doesn't exist or is empty, then nothing is returned.
  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.
  # If the given directory doesn't exist or is empty, then nothing happens.
  def reload_directory(directory)
    files, folders = folders_and_files_in(directory)
    files.each {|file|
      next if file.include? "unabridged"
      @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 = 
reload() click to toggle source

Reloads the file if needed. No harm is done by calling this multiple times, so feel free call just to be sure.

# File lib/camping/loader.rb, line 162
def reload
  return if @mtime >= mtime rescue nil
  reload!
end
reload!() click to toggle source

Force a reload.

# File lib/camping/loader.rb, line 168
def reload!
  remove_constants
  load_everything
end
reload_directory(directory) click to toggle source

Reloads a directory recursively. loading more shallow files before deeper files. If the given directory doesn’t exist or is empty, then nothing happens.

# File lib/camping/loader.rb, line 220
def reload_directory(directory)
  files, folders = folders_and_files_in(directory)
  files.each {|file|
    next if file.include? "unabridged"
    @requires << [file, File.expand_path(file)]
    load file
  }
  folders.each {|folder|
    reload_directory folder
  }
end
remove_constants() click to toggle source

removes all constants recursively included using this script as a root. so everything in /apps, and /lib in relation from this script.

# File lib/camping/loader.rb, line 146
def remove_constants
    @requires.each do |(path, full)|
            $LOADED_FEATURES.delete(path) unless path.match? "concurrent-ruby"
    end

  @apps.each do |name, app|
    Camping::Apps.delete(app)
    Object.send :remove_const, name
  end.dup
ensure
  @apps.clear
  @requires.clear
end
setup_zeit(loader) click to toggle source

sets up Zeit autoloading for the script locations.

# File lib/camping/loader.rb, line 189
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.ignore("#{@root}/lib/camping-unabridged.rb")
        loader.enable_reloading if ENV['environment'] == 'development'
        loader.setup
end
start(= @listener.start) click to toggle source
# File lib/camping/loader.rb, line 72
  def start = @listener.start

  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

    # Zeitwerk will Autoload stuff, which is great. But we don't want Zeitwerk
    # autoloading when we evaluate the camp.rb file because it will try to
    # autoload any controllers and helpers that we've defined in there from
    # the descendant /apps and /lib directory, which then make it break.
    @zeit.unload
    load_file
    @zeit.setup
    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

      if !@apps.include?(key)
        @callback.call(app) if @callback
        app.create if app.respond_to?(:create)
        app.kindling if app.respond_to?(:kindling)
      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) unless path.match? "concurrent-ruby"
      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.ignore("#{@root}/lib/camping-unabridged.rb")
          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?(directory)
      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.
  # If the given directory doesn't exist or is empty, then nothing is returned.
  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.
  # If the given directory doesn't exist or is empty, then nothing happens.
  def reload_directory(directory)
    files, folders = folders_and_files_in(directory)
    files.each {|file|
      next if file.include? "unabridged"
      @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
stop(= @listener.stop) click to toggle source
# File lib/camping/loader.rb, line 70
  def stop = @listener.stop
  def pause = @listener.pause
  def start = @listener.start

  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

    # Zeitwerk will Autoload stuff, which is great. But we don't want Zeitwerk
    # autoloading when we evaluate the camp.rb file because it will try to
    # autoload any controllers and helpers that we've defined in there from
    # the descendant /apps and /lib directory, which then make it break.
    @zeit.unload
    load_file
    @zeit.setup
    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

      if !@apps.include?(key)
        @callback.call(app) if @callback
        app.create if app.respond_to?(:create)
        app.kindling if app.respond_to?(:kindling)
      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) unless path.match? "concurrent-ruby"
      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.ignore("#{@root}/lib/camping-unabridged.rb")
          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?(directory)
      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.
  # If the given directory doesn't exist or is empty, then nothing is returned.
  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.
  # If the given directory doesn't exist or is empty, then nothing happens.
  def reload_directory(directory)
    files, folders = folders_and_files_in(directory)
    files.each {|file|
      next if file.include? "unabridged"
      @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