sinatra/sinatra-contrib/lib/sinatra/reloader.rb

433 lines
14 KiB
Ruby

# frozen_string_literal: true
require 'sinatra/base'
module Sinatra
# = Sinatra::Reloader
#
# <b>DEPRECATED:<b> Please consider using an alternative like
# <tt>rerun</tt> or <tt>rack-unreloader</tt> instead.
#
# Extension to reload modified files. Useful during development,
# since it will automatically require files defining routes, filters,
# error handlers and inline templates, with every incoming request,
# but only if they have been updated.
#
# == Usage
#
# === Classic Application
#
# To enable the reloader in a classic application all you need to do is
# require it:
#
# require "sinatra"
# require "sinatra/reloader" if development?
#
# # Your classic application code goes here...
#
# === Modular Application
#
# To enable the reloader in a modular application all you need to do is
# require it, and then, register it:
#
# require "sinatra/base"
# require "sinatra/reloader"
#
# class MyApp < Sinatra::Base
# configure :development do
# register Sinatra::Reloader
# end
#
# # Your modular application code goes here...
# end
#
# == Using the Reloader in Other Environments
#
# By default, the reloader is only enabled for the development
# environment. Similar to registering the reloader in a modular
# application, a classic application requires manually enabling the
# extension for it to be available in a non-development environment.
#
# require "sinatra"
# require "sinatra/reloader"
#
# configure :production do
# enable :reloader
# end
#
# == Changing the Reloading Policy
#
# You can refine the reloading policy with +also_reload+ and
# +dont_reload+, to customize which files should, and should not, be
# reloaded, respectively. You can also use +after_reload+ to execute a
# block after any file being reloaded.
#
# === Classic Application
#
# Simply call the methods:
#
# require "sinatra"
# require "sinatra/reloader" if development?
#
# also_reload '/path/to/some/file'
# dont_reload '/path/to/other/file'
# after_reload do
# puts 'reloaded'
# end
#
# # Your classic application code goes here...
#
# === Modular Application
#
# Call the methods inside the +configure+ block:
#
# require "sinatra/base"
# require "sinatra/reloader"
#
# class MyApp < Sinatra::Base
# configure :development do
# register Sinatra::Reloader
# also_reload '/path/to/some/file'
# dont_reload '/path/to/other/file'
# after_reload do
# puts 'reloaded'
# end
# end
#
# # Your modular application code goes here...
# end
#
module Reloader
# Watches a file so it can tell when it has been updated, and what
# elements does it contain.
class Watcher
# Represents an element of a Sinatra application that may need to
# be reloaded. An element could be:
# * a route
# * a filter
# * an error handler
# * a middleware
# * inline templates
#
# Its +representation+ attribute is there to allow to identify the
# element within an application, that is, to match it with its
# Sinatra's internal representation.
class Element < Struct.new(:type, :representation)
end
# Collection of file +Watcher+ that can be associated with a
# Sinatra application. That way, we can know which files belong
# to a given application and which files have been modified. It
# also provides a mechanism to inform a Watcher of the elements
# defined in the file being watched and if its changes should be
# ignored.
class List
@app_list_map = Hash.new { |hash, key| hash[key] = new }
# Returns the +List+ for the application +app+.
def self.for(app)
@app_list_map[app]
end
# Creates a new +List+ instance.
def initialize
@path_watcher_map = Hash.new do |hash, key|
hash[key] = Watcher.new(key)
end
end
# Lets the +Watcher+ for the file located at +path+ know that the
# +element+ is defined there, and adds the +Watcher+ to the +List+,
# if it isn't already there.
def watch(path, element)
watcher_for(path).elements << element
end
# Tells the +Watcher+ for the file located at +path+ to ignore
# the file changes, and adds the +Watcher+ to the +List+, if
# it isn't already there.
def ignore(path)
watcher_for(path).ignore
end
# Adds a +Watcher+ for the file located at +path+ to the
# +List+, if it isn't already there.
def watcher_for(path)
@path_watcher_map[File.expand_path(path)]
end
alias watch_file watcher_for
# Returns an array with all the watchers in the +List+.
def watchers
@path_watcher_map.values
end
# Returns an array with all the watchers in the +List+ that
# have been updated.
def updated
watchers.find_all(&:updated?)
end
end
attr_reader :path, :elements, :mtime
# Creates a new +Watcher+ instance for the file located at +path+.
def initialize(path)
@ignore = nil
@path = path
@elements = []
update
end
# Indicates whether or not the file being watched has been modified.
def updated?
!ignore? && !removed? && mtime != File.mtime(path)
end
# Updates the mtime of the file being watched.
def update
@mtime = File.mtime(path)
end
# Indicates whether or not the file being watched has inline
# templates.
def inline_templates?
elements.any? { |element| element.type == :inline_templates }
end
# Informs that the modifications to the file being watched
# should be ignored.
def ignore
@ignore = true
end
# Indicates whether or not the modifications to the file being
# watched should be ignored.
def ignore?
!!@ignore
end
# Indicates whether or not the file being watched has been removed.
def removed?
!File.exist?(path)
end
end
MUTEX_FOR_PERFORM = Mutex.new
# Allow a block to be executed after any file being reloaded
@@after_reload = []
def after_reload(&block)
@@after_reload << block
end
# When the extension is registered it extends the Sinatra application
# +klass+ with the modules +BaseMethods+ and +ExtensionMethods+ and
# defines a before filter to +perform+ the reload of the modified files.
def self.registered(klass)
@reloader_loaded_in ||= {}
return if @reloader_loaded_in[klass]
@reloader_loaded_in[klass] = true
klass.extend BaseMethods
klass.extend ExtensionMethods
klass.set(:reloader) { klass.development? }
klass.set(:reload_templates) { klass.reloader? }
klass.before do
if klass.reloader?
MUTEX_FOR_PERFORM.synchronize { Reloader.perform(klass) }
end
end
klass.set(:inline_templates, klass.app_file) if klass == Sinatra::Application
end
# Reloads the modified files, adding, updating and removing the
# needed elements.
def self.perform(klass)
reloaded_paths = []
Watcher::List.for(klass).updated.each do |watcher|
klass.set(:inline_templates, watcher.path) if watcher.inline_templates?
watcher.elements.each { |element| klass.deactivate(element) }
# Deletes all old elements.
watcher.elements.delete_if { true }
$LOADED_FEATURES.delete(watcher.path)
require watcher.path
watcher.update
reloaded_paths << watcher.path
end
return if reloaded_paths.empty?
@@after_reload.each do |block|
block.arity.zero? ? block.call : block.call(reloaded_paths)
end
# Prevents after_reload from increasing each time it's reloaded.
@@after_reload.delete_if do |blk|
path, = blk.source_location
path && reloaded_paths.include?(path)
end
end
# Contains the methods defined in Sinatra::Base that are overridden.
module BaseMethods
# Protects Sinatra::Base.run! from being called more than once.
def run!(*args)
if settings.reloader?
super unless running?
else
super
end
end
# Does everything Sinatra::Base#route does, but it also tells the
# +Watcher::List+ for the Sinatra application to watch the defined
# route.
#
# Note: We are using #compile! so we don't interfere with extensions
# changing #route.
def compile!(verb, path, block, **options)
source_location = block.respond_to?(:source_location) ?
block.source_location.first : caller_files[1]
signature = super
watch_element(
source_location, :route, { verb: verb, signature: signature }
)
signature
end
# Does everything Sinatra::Base#inline_templates= does, but it also
# tells the +Watcher::List+ for the Sinatra application to watch the
# inline templates in +file+ or the file who made the call to this
# method.
def inline_templates=(file = nil)
file = (caller_files[1] || File.expand_path($0)) if file.nil? || file == true
watch_element(file, :inline_templates)
super
end
# Does everything Sinatra::Base#use does, but it also tells the
# +Watcher::List+ for the Sinatra application to watch the middleware
# being used.
def use(middleware, *args, &block)
path = caller_files[1] || File.expand_path($0)
watch_element(path, :middleware, [middleware, args, block])
super
end
# Does everything Sinatra::Base#add_filter does, but it also tells
# the +Watcher::List+ for the Sinatra application to watch the defined
# filter.
def add_filter(type, path = nil, **options, &block)
source_location = block.respond_to?(:source_location) ?
block.source_location.first : caller_files[1]
result = super
watch_element(source_location, :"#{type}_filter", filters[type].last)
result
end
# Does everything Sinatra::Base#error does, but it also tells the
# +Watcher::List+ for the Sinatra application to watch the defined
# error handler.
def error(*codes, &block)
path = caller_files[1] || File.expand_path($0)
result = super
codes.each do |c|
watch_element(path, :error, code: c, handler: @errors[c])
end
result
end
# Does everything Sinatra::Base#register does, but it also lets the
# reloader know that an extension is being registered, because the
# elements defined in its +registered+ method need a special treatment.
def register(*extensions, &block)
start_registering_extension
result = super
stop_registering_extension
result
end
# Does everything Sinatra::Base#register does and then registers the
# reloader in the +subclass+.
def inherited(subclass)
result = super
subclass.register Sinatra::Reloader
result
end
end
# Contains the methods that the extension adds to the Sinatra application.
module ExtensionMethods
# Removes the +element+ from the Sinatra application.
def deactivate(element)
case element.type
when :route
verb = element.representation[:verb]
signature = element.representation[:signature]
(routes[verb] ||= []).delete(signature)
when :middleware
@middleware.delete(element.representation)
when :before_filter
filters[:before].delete(element.representation)
when :after_filter
filters[:after].delete(element.representation)
when :error
code = element.representation[:code]
handler = element.representation[:handler]
@errors.delete(code) if @errors[code] == handler
end
end
# Indicates with a +glob+ which files should be reloaded if they
# have been modified. It can be called several times.
def also_reload(*glob)
Dir[*glob].each { |path| Watcher::List.for(self).watch_file(path) }
end
# Indicates with a +glob+ which files should not be reloaded even if
# they have been modified. It can be called several times.
def dont_reload(*glob)
Dir[*glob].each { |path| Watcher::List.for(self).ignore(path) }
end
private
# attr_reader :register_path warn on -w (private attribute)
def register_path; @register_path ||= nil; end
# Indicates an extesion is being registered.
def start_registering_extension
@register_path = caller_files[2]
end
# Indicates the extesion has already been registered.
def stop_registering_extension
@register_path = nil
end
# Indicates whether or not an extension is being registered.
def registering_extension?
!register_path.nil?
end
# Builds a Watcher::Element from +type+ and +representation+ and
# tells the Watcher::List for the current application to watch it
# in the file located at +path+.
#
# If an extension is being registered, it also tells the list to
# watch it in the file where the extension has been registered.
# This prevents the duplication of the elements added by the
# extension in its +registered+ method with every reload.
def watch_element(path, type, representation = nil)
list = Watcher::List.for(self)
element = Watcher::Element.new(type, representation)
list.watch(path, element)
list.watch(register_path, element) if registering_extension?
end
end
end
register Reloader
Delegator.delegate :also_reload, :dont_reload
end