class Closure::Sources
This class is responsible for scanning source files and managing dependencies.
Constants
- BASE_JS_REGEX
Google
Closure
Library base.js is the file with no provides, no requires, and defines goog a particular way.- ENV_FLAG
Flag env so that refresh is never run more than once per request
- GOOG_REGEX_STRING
Using regular expressions may seem clunky, but the Python scripts did it this way and I've not see it fail in practice.
- PROVIDE_REGEX
- REQUIRE_REGEX
Attributes
@return (Float) Limits how often a full refresh is allowed to run. Blocked threads can trigger unneeded refreshes in rare scenarios. Also sent to browser in cache-control for frames performance. Caching, lazy loading, and flagging (of env) make up the remaining techniques for good performance.
Public Class Methods
@param (Float) dwell in seconds.
# File lib/closure/sources.rb, line 48 def initialize(dwell = 1.0) @dwell = dwell @files = {} @sources = [] @semaphore = Mutex.new @last_been_run = nil reset_all_computed_instance_vars end
Public Instance Methods
Adds a new directory of source files. @param (String) path Where to mount on the http server. @param (String) directory Filesystem location of your sources. @return (Sources
) self
# File lib/closure/sources.rb, line 71 def add(directory, path=nil) raise "immutable once used" if @last_been_run if path raise "path must start with /" unless path =~ %r{^/} path = '' if path == '/' raise "path must not end with /" if path =~ %r{/$} raise "path already exists" if @sources.find{|s|s[0]==path} end raise "directory already exists" if @sources.find{|s|s[1]==directory} @sources << [File.expand_path(directory), path] @sources.sort! {|a,b| (b[1]||'') <=> (a[1]||'')} self end
Determine the path_info and query_string for loading base_js. @return [String]
# File lib/closure/sources.rb, line 95 def base_js(env={}) if (goog = @goog) and @last_been_run return "#{goog[:base_js]}?#{goog[:base_js_mtime].to_i}" end @semaphore.synchronize do refresh(env) raise BaseJsNotFoundError unless @goog @goog[:base_js] end end
Determine the path_info for where deps_js
is located. @return [String]
# File lib/closure/sources.rb, line 109 def deps_js(env={}) # Because Server uses this on every call, it's best to never lock. # We grab a local goog so we don't need the lock if everything looks good. # This works because #refresh creates new @goog hashes instead of modifying. if (goog = @goog) and @last_been_run return goog[:deps_js] end @semaphore.synchronize do refresh(env) raise BaseJsNotFoundError unless @goog @goog[:deps_js] end end
Builds a Rack::Response to serve a dynamic deps.js @return (Rack::Response)
# File lib/closure/sources.rb, line 126 def deps_response(base, env={}) @semaphore.synchronize do refresh(env) base = Pathname.new(base) unless @deps[base] response = @deps[base] ||= Rack::Response.new response.write "// Dynamic Deps by Closure Script\n" @files.sort{|a,b|(a[1][:path]||'')<=>(b[1][:path]||'')}.each do |filename, dep| if dep[:path] path = Pathname.new(dep[:path]).relative_path_from(base) path = "#{path}?#{dep[:mtime].to_i}" response.write "goog.addDependency(#{path.dump}, #{dep[:provide].inspect}, #{dep[:require].inspect});\n" end end response.headers['Content-Type'] = 'application/javascript' response.headers['Cache-Control'] = "max-age=#{[1,@dwell.floor].max}, private, must-revalidate" response.headers['Last-Modified'] = Time.now.httpdate end mod_since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']) rescue nil if mod_since == Time.httpdate(@deps[base].headers['Last-Modified']) Rack::Response.new [], 304 # Not Modified else @deps[base] end end end
Yields path and directory for each of the added sources. @yield (path, directory)
# File lib/closure/sources.rb, line 88 def each @sources.each { |directory, path| yield directory, path } end
Calculate all required files for a namespace. @param (String) namespace @return (Array<String>) New Array of filenames.
# File lib/closure/sources.rb, line 157 def files_for(namespace, filenames=nil, env={}) ns = nil @semaphore.synchronize do refresh(env) # Pivot the deps to a namespace hash # @ns is cleared when any requires or provides changes unless @ns @ns = {} @files.each do |filename, dep| dep[:provide].each do |provide| if @ns[provide] @ns = nil raise "Namespace #{provide.dump} provided more than once." end @ns[provide] = { :filename => filename, :require => dep[:require] } end end end ns = @ns if !filenames or filenames.empty? raise BaseJsNotFoundError unless @goog filenames ||= [] filenames << @goog[:base_filename] end end # Since @ns is only unset, not modified, by another thread, we # can work with a local reference. This has been finely tuned and # runs fast, but it's still nice to release any other threads early. calcdeps(ns, namespace, filenames) end
Return all provided and required namespaces for a file. @param (String) filename @return (String)
# File lib/closure/sources.rb, line 210 def namespaces_for(filename, env={}) @semaphore.synchronize do refresh(env) file = @files[filename] raise "#{filename.dump} not found" unless file file[:provide] + file[:require] end end
Calculate the file server path for a filename @param (String) filename @return (String)
# File lib/closure/sources.rb, line 195 def src_for(filename, env={}) @semaphore.synchronize do refresh(env) file = @files[filename] unless file and file.has_key? :path raise "#{filename.dump} is not available from file server" end "#{file[:path]}?#{file[:mtime].to_i}" end end
Protected Instance Methods
Namespace recursion with two-way circular stop
# File lib/closure/sources.rb, line 232 def calcdeps(ns, namespace, filenames, prev = []) unless source = ns[namespace] msg = "#{prev.last[:filename]}: " rescue '' msg += "Namespace #{namespace.dump} not found." raise msg end if prev.include? source return else prev.push source end return if filenames.include?(source[:filename]) source[:require].each do |required| calcdeps ns, required, filenames, prev end filenames.push source[:filename] end
We can't trust anything if we see more than one goog
# File lib/closure/sources.rb, line 333 def multiple_base_js_failure reset_all_computed_instance_vars raise MultipleBaseJsError end
Lasciate ogne speranza, voi ch'intrate.
# File lib/closure/sources.rb, line 252 def refresh(env) return if env[ENV_FLAG] env[ENV_FLAG] = true # Having been run within the dwell period is good enough return if @last_been_run and Time.now - @last_been_run < @dwell # verbose loggers added_files = [] changed_files = [] deleted_files = [] # Prepare to find a moving base_js previous_goog_base_filename = @goog ? @goog[:base_filename] : nil goog = nil # Mark everything for deletion. @files.each {|f, dep| dep[:not_found] = true} # Scan filesystem for changes. @sources.each do |dir, path| dir_range = (dir.length..-1) Dir.glob(File.join(dir,'**','*.js')).each do |filename| file = (@files[filename] ||= {}) file.delete(:not_found) mtime = File.mtime(filename) if previous_goog_base_filename == filename if file[:mtime] == mtime multiple_base_js_failure if goog goog = @goog end previous_goog_base_filename = nil end if path and !file.has_key?(:path) raise unless filename.index(dir) == 0 # glob sanity file[:path] = "#{path}#{filename.slice(dir_range)}" added_files << filename unless file[:excluded] end if !path or file[:excluded] file[:excluded] = true file[:provide] = file[:require] = [] elsif file[:mtime] != mtime @deps = {} old_file_provide = file[:provide] old_file_require = file[:require] file_text = File.read filename file[:provide] = file_text.scan(PROVIDE_REGEX).flatten.uniq file[:require] = file_text.scan(REQUIRE_REGEX).flatten.uniq if old_file_provide != file[:provide] or old_file_require != file[:require] # We're changed only if the provides or requires changes. # Other edits to the files don't actually alter the dependencies. changed_files << filename end # Record goog as we pass by if file[:provide].empty? and file[:require].empty? if File.basename(filename) == 'base.js' if file_text =~ BASE_JS_REGEX multiple_base_js_failure if goog goog = {:base_filename => filename} if file[:path] goog[:base_js] = file[:path] goog[:base_js_mtime] = mtime goog[:deps_js] = file[:path].gsub(/base.js$/, 'deps.js') end end end end end file[:mtime] = mtime end end # Sweep to delete not-found files. @files.select{|f, dep| dep[:not_found]}.each do |filename, o| deleted_files << filename @files.delete(filename) end # Decide if deps has changed. if 0 < added_files.length + changed_files.length + deleted_files.length @ns = nil end # Finish @goog = goog @last_been_run = Time.now end
# File lib/closure/sources.rb, line 338 def reset_all_computed_instance_vars @deps = {} @ns = nil @goog = nil end